Skip to content

Commit

Permalink
cmd/tailscale/cli: make tailscale update query softwareupdate (ta…
Browse files Browse the repository at this point in the history
…ilscale#8641)

* cmd/tailscale/cli: make `tailscale update` query `softwareupdate`

Even on macOS when Tailscale was installed via the App Store, we can check for
and even install new versions if people ask explicitly. Also, warn if App Store
AutoUpdate is not turned on.

Updates tailscale#6995

Signed-off-by: Alex Paguis <alex@windscribe.com>
  • Loading branch information
noncombatant authored and alexelisenko committed Feb 15, 2024
1 parent 8f6d5c7 commit d425f30
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 7 deletions.
69 changes: 62 additions & 7 deletions cmd/tailscale/cli/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,19 @@ 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, "check the App Store for updates, even if this is not an App Store install (for testing only!)")
fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable" or "unstable" (dev); empty means same as current`)
fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`)
return fs
})(),
}

var updateArgs struct {
yes bool
dryRun bool
track string // explicit track; empty means same as current
version string // explicit version; empty means auto
yes bool
dryRun bool
appStore bool
track string // explicit track; empty means same as current
version string // explicit version; empty means auto
}

// winMSIEnv is the environment variable that, if set, is the MSI file for the
Expand Down Expand Up @@ -140,12 +142,12 @@ func newUpdater() (*updater, error) {
}
case "darwin":
switch {
case !version.IsSandboxedMacOS():
case !updateArgs.appStore && !version.IsSandboxedMacOS():
return nil, errors.New("The 'update' command is not yet supported on this platform; see https://github.com/tailscale/tailscale/wiki/Tailscaled-on-macOS/ for now")
case strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
case !updateArgs.appStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
up.update = up.updateMacSys
default:
return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/s/unstable-clients to use TestFlight or to install the non-App Store version")
up.update = up.updateMacAppStore
}
}
if up.update == nil {
Expand Down Expand Up @@ -333,6 +335,59 @@ func (up *updater) updateMacSys() error {
return errors.New("The 'update' command is not yet implemented on macOS.")
}

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 {
fmt.Fprintln(os.Stderr, "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()
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 == "" {
fmt.Println("no Tailscale update available")
return nil
}

newTailscaleVer := strings.TrimPrefix(newTailscale, "Tailscale-")
if up.currentOrDryRun(newTailscaleVer) {
return nil
}
if err := up.confirm(newTailscaleVer); err != nil {
return err
}

cmd := exec.Command("sudo", "softwareupdate", "--install", newTailscale)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("can't install App Store update for Tailscale: %w", err)
}
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])
}

var (
verifyAuthenticode func(string) error // or nil on non-Windows
markTempFileFunc func(string) error // or nil on non-Windows
Expand Down
78 changes: 78 additions & 0 deletions cmd/tailscale/cli/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,81 @@ 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)
}
})
}
}

0 comments on commit d425f30

Please sign in to comment.