diff --git a/clientupdate/clientupdate.go b/clientupdate/clientupdate.go index 9f1bc441f..1fbcdd4dd 100644 --- a/clientupdate/clientupdate.go +++ b/clientupdate/clientupdate.go @@ -73,9 +73,6 @@ type Arguments struct { // // Leaving this empty is the same as using CurrentTrack. Version string - // AppStore forces a local app store check, even if the current binary was - // not installed via an app store. TODO(cpalmer): Remove this. - AppStore bool // Logf is a logger for update progress messages. Logf logger.Logf // Stdout and Stderr should be used for output instead of os.Stdout and @@ -182,14 +179,12 @@ func (up *Updater) getUpdateFunction() updateFunction { } case "darwin": switch { - case !up.Arguments.AppStore && !version.IsSandboxedMacOS(): - return nil - case !up.Arguments.AppStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"): - // TODO(noncombatant): return up.updateMacSys when we figure out why - // Sparkle update doesn't work when running "tailscale update". - return nil - default: + case version.IsMacAppStore(): return up.updateMacAppStore + case version.IsMacSysExt(): + return up.updateMacSys + default: + return nil } case "freebsd": return up.updateFreeBSD @@ -625,55 +620,17 @@ func (up *Updater) updateMacSys() error { } func (up *Updater) updateMacAppStore() error { - out, err := exec.Command("defaults", "read", "/Library/Preferences/com.apple.commerce.plist", "AutoUpdate").CombinedOutput() - if err != nil { - return fmt.Errorf("can't check App Store auto-update setting: %w, output: %q", err, string(out)) - } - const on = "1\n" - if string(out) != on { - up.Logf("NOTE: Automatic updating for App Store apps is turned off. You can change this setting in System Settings (search for ‘update’).") - } + // We can't trigger the update via App Store from the sandboxed app. At + // most, we can open the App Store page for them. + up.Logf("Please use the App Store to update Tailscale.\nConsider enabling Automatic Updates in the App Store Settings, if you haven't already.\nOpening the Tailscale app page...") - out, err = exec.Command("softwareupdate", "--list").CombinedOutput() + out, err := exec.Command("open", "https://apps.apple.com/us/app/tailscale/id1475387142").CombinedOutput() if err != nil { - return fmt.Errorf("can't check App Store for available updates: %w, output: %q", err, string(out)) - } - - newTailscale := parseSoftwareupdateList(out) - if newTailscale == "" { - up.Logf("no Tailscale update available") - return nil - } - - newTailscaleVer := strings.TrimPrefix(newTailscale, "Tailscale-") - if !up.confirm(newTailscaleVer) { - return nil - } - - cmd := exec.Command("sudo", "softwareupdate", "--install", newTailscale) - cmd.Stdout = up.Stdout - cmd.Stderr = up.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("can't install App Store update for Tailscale: %w", err) + return fmt.Errorf("can't open the Tailscale page in App Store: %w, output: %q", err, string(out)) } return nil } -var macOSAppStoreListPattern = regexp.MustCompile(`(?m)^\s+\*\s+Label:\s*(Tailscale-\d[\d\.]+)`) - -// parseSoftwareupdateList searches the output of `softwareupdate --list` on -// Darwin and returns the matching Tailscale package label. If there is none, -// returns the empty string. -// -// See TestParseSoftwareupdateList for example inputs. -func parseSoftwareupdateList(stdout []byte) string { - matches := macOSAppStoreListPattern.FindSubmatch(stdout) - if len(matches) < 2 { - return "" - } - return string(matches[1]) -} - // winMSIEnv is the environment variable that, if set, is the MSI file for the // update command to install. It's passed like this so we can stop the // tailscale.exe process from running before the msiexec process runs and tries diff --git a/clientupdate/clientupdate_test.go b/clientupdate/clientupdate_test.go index 334292c33..dc300341f 100644 --- a/clientupdate/clientupdate_test.go +++ b/clientupdate/clientupdate_test.go @@ -84,84 +84,6 @@ func TestUpdateDebianAptSourcesListBytes(t *testing.T) { } } -func TestParseSoftwareupdateList(t *testing.T) { - tests := []struct { - name string - input []byte - want string - }{ - { - name: "update-at-end-of-list", - input: []byte(` - Software Update Tool - - Finding available software - Software Update found the following new or updated software: - * Label: MacBookAirEFIUpdate2.4-2.4 - Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart, - * Label: ProAppsQTCodecs-1.0 - Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES, - * Label: Tailscale-1.23.4 - Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES, -`), - want: "Tailscale-1.23.4", - }, - { - name: "update-in-middle-of-list", - input: []byte(` - Software Update Tool - - Finding available software - Software Update found the following new or updated software: - * Label: MacBookAirEFIUpdate2.4-2.4 - Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart, - * Label: Tailscale-1.23.5000 - Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES, - * Label: ProAppsQTCodecs-1.0 - Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES, -`), - want: "Tailscale-1.23.5000", - }, - { - name: "update-not-in-list", - input: []byte(` - Software Update Tool - - Finding available software - Software Update found the following new or updated software: - * Label: MacBookAirEFIUpdate2.4-2.4 - Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart, - * Label: ProAppsQTCodecs-1.0 - Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES, -`), - want: "", - }, - { - name: "decoy-in-list", - input: []byte(` - Software Update Tool - - Finding available software - Software Update found the following new or updated software: - * Label: MacBookAirEFIUpdate2.4-2.4 - Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart, - * Label: Malware-1.0 - Title: * Label: Tailscale-0.99.0, Version: 1.0, Size: 968K, Recommended: NOT REALLY TBH, -`), - want: "", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got := parseSoftwareupdateList(test.input) - if test.want != got { - t.Fatalf("got %q, want %q", got, test.want) - } - }) - } -} - func TestUpdateYUMRepoTrack(t *testing.T) { tests := []struct { desc string diff --git a/cmd/tailscale/cli/update.go b/cmd/tailscale/cli/update.go index 84742a7d6..020ac4d59 100644 --- a/cmd/tailscale/cli/update.go +++ b/cmd/tailscale/cli/update.go @@ -26,7 +26,6 @@ var updateCmd = &ffcli.Command{ fs := newFlagSet("update") fs.BoolVar(&updateArgs.yes, "yes", false, "update without interactive prompts") fs.BoolVar(&updateArgs.dryRun, "dry-run", false, "print what update would do without doing it, or prompts") - fs.BoolVar(&updateArgs.appStore, "app-store", false, "HIDDEN: check the App Store for updates, even if this is not an App Store install (for testing only)") // These flags are not supported on several systems that only provide // the latest version of Tailscale: // @@ -42,11 +41,10 @@ var updateCmd = &ffcli.Command{ } var updateArgs struct { - yes bool - dryRun bool - appStore bool - track string // explicit track; empty means same as current - version string // explicit version; empty means auto + yes bool + dryRun bool + track string // explicit track; empty means same as current + version string // explicit version; empty means auto } func runUpdate(ctx context.Context, args []string) error { @@ -61,12 +59,11 @@ func runUpdate(ctx context.Context, args []string) error { ver = updateArgs.track } err := clientupdate.Update(clientupdate.Arguments{ - Version: ver, - AppStore: updateArgs.appStore, - Logf: func(f string, a ...any) { printf(f+"\n", a...) }, - Stdout: Stdout, - Stderr: Stderr, - Confirm: confirmUpdate, + Version: ver, + Logf: func(f string, a ...any) { printf(f+"\n", a...) }, + Stdout: Stdout, + Stderr: Stderr, + Confirm: confirmUpdate, }) if errors.Is(err, errors.ErrUnsupported) { return errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/s/client-updates") diff --git a/version/prop.go b/version/prop.go index daccab29e..138734c30 100644 --- a/version/prop.go +++ b/version/prop.go @@ -47,35 +47,40 @@ var isSandboxedMacOS lazy.SyncValue[bool] // and macsys (System Extension) version on macOS, and false for // tailscaled-on-macOS. func IsSandboxedMacOS() bool { + return IsMacAppStore() || IsMacSysExt() +} + +var isMacSysExt lazy.SyncValue[bool] + +// IsMacSysExt whether this binary is from the standalone "System +// Extension" (a.k.a. "macsys") version of Tailscale for macOS. +func IsMacSysExt() bool { if runtime.GOOS != "darwin" { return false } - return isSandboxedMacOS.Get(func() bool { - if IsMacSysExt() { - return true - } + return isMacSysExt.Get(func() bool { exe, err := os.Executable() if err != nil { return false } - return filepath.Base(exe) == "io.tailscale.ipn.macsys.network-extension" || strings.HasSuffix(exe, "/Contents/MacOS/Tailscale") || strings.HasSuffix(exe, "/Contents/MacOS/IPNExtension") + return filepath.Base(exe) == "io.tailscale.ipn.macsys.network-extension" }) } -var isMacSysExt lazy.SyncValue[bool] +var isMacAppStore lazy.SyncValue[bool] -// IsMacSysExt whether this binary is from the standalone "System -// Extension" (a.k.a. "macsys") version of Tailscale for macOS. -func IsMacSysExt() bool { +// IsMacAppStore whether this binary is from the App Store version of Tailscale +// for macOS. +func IsMacAppStore() bool { if runtime.GOOS != "darwin" { return false } - return isMacSysExt.Get(func() bool { + return isMacAppStore.Get(func() bool { exe, err := os.Executable() if err != nil { return false } - return filepath.Base(exe) == "io.tailscale.ipn.macsys.network-extension" + return strings.HasSuffix(exe, "/Contents/MacOS/Tailscale") || strings.HasSuffix(exe, "/Contents/MacOS/IPNExtension") }) }