diff --git a/cmd/tailscaled/install_darwin.go b/cmd/tailscaled/install_darwin.go index 0b3a1fee2..ca930a91d 100644 --- a/cmd/tailscaled/install_darwin.go +++ b/cmd/tailscaled/install_darwin.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "os/exec" "path/filepath" @@ -83,6 +84,13 @@ func uninstallSystemDaemonDarwin(args []string) (ret error) { ret = err } } + + // Do not delete targetBin if it's a symlink, which happens if it was installed via + // Homebrew. + if isSymlink(targetBin) { + return ret + } + if err := os.Remove(targetBin); err != nil { if os.IsNotExist(err) { err = nil @@ -107,26 +115,56 @@ func installSystemDaemonDarwin(args []string) (err error) { // Best effort: uninstallSystemDaemonDarwin(nil) - // Copy ourselves to /usr/local/bin/tailscaled. - if err := os.MkdirAll(filepath.Dir(targetBin), 0755); err != nil { - return err - } exe, err := os.Executable() if err != nil { return fmt.Errorf("failed to find our own executable path: %w", err) } - tmpBin := targetBin + ".tmp" + + same, err := sameFile(exe, targetBin) + if err != nil { + return err + } + + // Do not overwrite targetBin with the binary file if it it's already + // pointing to it. This is primarily to handle Homebrew that writes + // /usr/local/bin/tailscaled is a symlink to the actual binary. + if !same { + if err := copyBinary(exe, targetBin); err != nil { + return err + } + } + if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil { + return err + } + + if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil { + return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out) + } + + if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil { + return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out) + } + + return nil +} + +// copyBinary copies binary file `src` into `dst`. +func copyBinary(src, dst string) error { + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return err + } + tmpBin := dst + ".tmp" f, err := os.Create(tmpBin) if err != nil { return err } - self, err := os.Open(exe) + srcf, err := os.Open(src) if err != nil { f.Close() return err } - _, err = io.Copy(f, self) - self.Close() + _, err = io.Copy(f, srcf) + srcf.Close() if err != nil { f.Close() return err @@ -137,21 +175,27 @@ func installSystemDaemonDarwin(args []string) (err error) { if err := os.Chmod(tmpBin, 0755); err != nil { return err } - if err := os.Rename(tmpBin, targetBin); err != nil { + if err := os.Rename(tmpBin, dst); err != nil { return err } - if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil { - return err - } + return nil +} - if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil { - return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out) - } +func isSymlink(path string) bool { + fi, err := os.Lstat(path) + return err == nil && (fi.Mode()&os.ModeSymlink == os.ModeSymlink) +} - if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil { - return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out) +// sameFile returns true if both file paths exist and resolve to the same file. +func sameFile(path1, path2 string) (bool, error) { + dst1, err := filepath.EvalSymlinks(path1) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return false, fmt.Errorf("EvalSymlinks(%s): %w", path1, err) } - - return nil + dst2, err := filepath.EvalSymlinks(path2) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return false, fmt.Errorf("EvalSymlinks(%s): %w", path2, err) + } + return dst1 == dst2, nil }