clientupdate: change Mac App Store support (#9891)

In the sandboxed app from the app store, we cannot check
`/Library/Preferences/com.apple.commerce.plist` or run `softwareupdate`.
We can at most print a helpful message and open the app store page.

Also, reenable macsys update function to mark it as supporting c2n
updates. macsys support in `tailscale update` was fixed.

Updates #755

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
pull/9896/head
Andrew Lytvynov 1 year ago committed by GitHub
parent eced054796
commit 70f9c8a6ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -73,9 +73,6 @@ type Arguments struct {
// //
// Leaving this empty is the same as using CurrentTrack. // Leaving this empty is the same as using CurrentTrack.
Version string 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 is a logger for update progress messages.
Logf logger.Logf Logf logger.Logf
// Stdout and Stderr should be used for output instead of os.Stdout and // Stdout and Stderr should be used for output instead of os.Stdout and
@ -182,14 +179,12 @@ func (up *Updater) getUpdateFunction() updateFunction {
} }
case "darwin": case "darwin":
switch { switch {
case !up.Arguments.AppStore && !version.IsSandboxedMacOS(): case version.IsMacAppStore():
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:
return up.updateMacAppStore return up.updateMacAppStore
case version.IsMacSysExt():
return up.updateMacSys
default:
return nil
} }
case "freebsd": case "freebsd":
return up.updateFreeBSD return up.updateFreeBSD
@ -625,55 +620,17 @@ func (up *Updater) updateMacSys() error {
} }
func (up *Updater) updateMacAppStore() error { func (up *Updater) updateMacAppStore() error {
out, err := exec.Command("defaults", "read", "/Library/Preferences/com.apple.commerce.plist", "AutoUpdate").CombinedOutput() // We can't trigger the update via App Store from the sandboxed app. At
if err != nil { // most, we can open the App Store page for them.
return fmt.Errorf("can't check App Store auto-update setting: %w, output: %q", err, string(out)) 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...")
}
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).")
}
out, err = exec.Command("softwareupdate", "--list").CombinedOutput() out, err := exec.Command("open", "https://apps.apple.com/us/app/tailscale/id1475387142").CombinedOutput()
if err != nil { if err != nil {
return fmt.Errorf("can't check App Store for available updates: %w, output: %q", err, string(out)) return fmt.Errorf("can't open the Tailscale page in App Store: %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 nil 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 // 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 // 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 // tailscale.exe process from running before the msiexec process runs and tries

@ -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) { func TestUpdateYUMRepoTrack(t *testing.T) {
tests := []struct { tests := []struct {
desc string desc string

@ -26,7 +26,6 @@ var updateCmd = &ffcli.Command{
fs := newFlagSet("update") fs := newFlagSet("update")
fs.BoolVar(&updateArgs.yes, "yes", false, "update without interactive prompts") 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.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 // These flags are not supported on several systems that only provide
// the latest version of Tailscale: // the latest version of Tailscale:
// //
@ -42,11 +41,10 @@ var updateCmd = &ffcli.Command{
} }
var updateArgs struct { var updateArgs struct {
yes bool yes bool
dryRun bool dryRun bool
appStore bool track string // explicit track; empty means same as current
track string // explicit track; empty means same as current version string // explicit version; empty means auto
version string // explicit version; empty means auto
} }
func runUpdate(ctx context.Context, args []string) error { func runUpdate(ctx context.Context, args []string) error {
@ -61,12 +59,11 @@ func runUpdate(ctx context.Context, args []string) error {
ver = updateArgs.track ver = updateArgs.track
} }
err := clientupdate.Update(clientupdate.Arguments{ err := clientupdate.Update(clientupdate.Arguments{
Version: ver, Version: ver,
AppStore: updateArgs.appStore, Logf: func(f string, a ...any) { printf(f+"\n", a...) },
Logf: func(f string, a ...any) { printf(f+"\n", a...) }, Stdout: Stdout,
Stdout: Stdout, Stderr: Stderr,
Stderr: Stderr, Confirm: confirmUpdate,
Confirm: confirmUpdate,
}) })
if errors.Is(err, errors.ErrUnsupported) { 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") return errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/s/client-updates")

@ -47,35 +47,40 @@ var isSandboxedMacOS lazy.SyncValue[bool]
// and macsys (System Extension) version on macOS, and false for // and macsys (System Extension) version on macOS, and false for
// tailscaled-on-macOS. // tailscaled-on-macOS.
func IsSandboxedMacOS() bool { 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" { if runtime.GOOS != "darwin" {
return false return false
} }
return isSandboxedMacOS.Get(func() bool { return isMacSysExt.Get(func() bool {
if IsMacSysExt() {
return true
}
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
return false 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 // IsMacAppStore whether this binary is from the App Store version of Tailscale
// Extension" (a.k.a. "macsys") version of Tailscale for macOS. // for macOS.
func IsMacSysExt() bool { func IsMacAppStore() bool {
if runtime.GOOS != "darwin" { if runtime.GOOS != "darwin" {
return false return false
} }
return isMacSysExt.Get(func() bool { return isMacAppStore.Get(func() bool {
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
return false 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")
}) })
} }

Loading…
Cancel
Save