install_darwin.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build go1.19
  4. package main
  5. import (
  6. "errors"
  7. "fmt"
  8. "io"
  9. "io/fs"
  10. "os"
  11. "os/exec"
  12. "path/filepath"
  13. )
  14. func init() {
  15. installSystemDaemon = installSystemDaemonDarwin
  16. uninstallSystemDaemon = uninstallSystemDaemonDarwin
  17. }
  18. // darwinLaunchdPlist is the launchd.plist that's written to
  19. // /Library/LaunchDaemons/com.tailscale.tailscaled.plist or (in the
  20. // future) a user-specific location.
  21. //
  22. // See man launchd.plist.
  23. const darwinLaunchdPlist = `
  24. <?xml version="1.0" encoding="UTF-8"?>
  25. <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  26. <plist version="1.0">
  27. <dict>
  28. <key>Label</key>
  29. <string>com.tailscale.tailscaled</string>
  30. <key>ProgramArguments</key>
  31. <array>
  32. <string>/usr/local/bin/tailscaled</string>
  33. </array>
  34. <key>RunAtLoad</key>
  35. <true/>
  36. </dict>
  37. </plist>
  38. `
  39. const sysPlist = "/Library/LaunchDaemons/com.tailscale.tailscaled.plist"
  40. const targetBin = "/usr/local/bin/tailscaled"
  41. const service = "com.tailscale.tailscaled"
  42. func uninstallSystemDaemonDarwin(args []string) (ret error) {
  43. if len(args) > 0 {
  44. return errors.New("uninstall subcommand takes no arguments")
  45. }
  46. plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output()
  47. _ = plist // parse it? https://github.com/DHowett/go-plist if we need something.
  48. running := err == nil
  49. if running {
  50. out, err := exec.Command("launchctl", "stop", "com.tailscale.tailscaled").CombinedOutput()
  51. if err != nil {
  52. fmt.Printf("launchctl stop com.tailscale.tailscaled: %v, %s\n", err, out)
  53. ret = err
  54. }
  55. out, err = exec.Command("launchctl", "unload", sysPlist).CombinedOutput()
  56. if err != nil {
  57. fmt.Printf("launchctl unload %s: %v, %s\n", sysPlist, err, out)
  58. if ret == nil {
  59. ret = err
  60. }
  61. }
  62. }
  63. if err := os.Remove(sysPlist); err != nil {
  64. if os.IsNotExist(err) {
  65. err = nil
  66. }
  67. if ret == nil {
  68. ret = err
  69. }
  70. }
  71. // Do not delete targetBin if it's a symlink, which happens if it was installed via
  72. // Homebrew.
  73. if isSymlink(targetBin) {
  74. return ret
  75. }
  76. if err := os.Remove(targetBin); err != nil {
  77. if os.IsNotExist(err) {
  78. err = nil
  79. }
  80. if ret == nil {
  81. ret = err
  82. }
  83. }
  84. return ret
  85. }
  86. func installSystemDaemonDarwin(args []string) (err error) {
  87. if len(args) > 0 {
  88. return errors.New("install subcommand takes no arguments")
  89. }
  90. defer func() {
  91. if err != nil && os.Getuid() != 0 {
  92. err = fmt.Errorf("%w; try running tailscaled with sudo", err)
  93. }
  94. }()
  95. // Best effort:
  96. uninstallSystemDaemonDarwin(nil)
  97. exe, err := os.Executable()
  98. if err != nil {
  99. return fmt.Errorf("failed to find our own executable path: %w", err)
  100. }
  101. same, err := sameFile(exe, targetBin)
  102. if err != nil {
  103. return err
  104. }
  105. // Do not overwrite targetBin with the binary file if it it's already
  106. // pointing to it. This is primarily to handle Homebrew that writes
  107. // /usr/local/bin/tailscaled is a symlink to the actual binary.
  108. if !same {
  109. if err := copyBinary(exe, targetBin); err != nil {
  110. return err
  111. }
  112. }
  113. if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
  114. return err
  115. }
  116. if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil {
  117. return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out)
  118. }
  119. if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil {
  120. return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out)
  121. }
  122. return nil
  123. }
  124. // copyBinary copies binary file `src` into `dst`.
  125. func copyBinary(src, dst string) error {
  126. if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
  127. return err
  128. }
  129. tmpBin := dst + ".tmp"
  130. f, err := os.Create(tmpBin)
  131. if err != nil {
  132. return err
  133. }
  134. srcf, err := os.Open(src)
  135. if err != nil {
  136. f.Close()
  137. return err
  138. }
  139. _, err = io.Copy(f, srcf)
  140. srcf.Close()
  141. if err != nil {
  142. f.Close()
  143. return err
  144. }
  145. if err := f.Close(); err != nil {
  146. return err
  147. }
  148. if err := os.Chmod(tmpBin, 0755); err != nil {
  149. return err
  150. }
  151. if err := os.Rename(tmpBin, dst); err != nil {
  152. return err
  153. }
  154. return nil
  155. }
  156. func isSymlink(path string) bool {
  157. fi, err := os.Lstat(path)
  158. return err == nil && (fi.Mode()&os.ModeSymlink == os.ModeSymlink)
  159. }
  160. // sameFile returns true if both file paths exist and resolve to the same file.
  161. func sameFile(path1, path2 string) (bool, error) {
  162. dst1, err := filepath.EvalSymlinks(path1)
  163. if err != nil && !errors.Is(err, fs.ErrNotExist) {
  164. return false, fmt.Errorf("EvalSymlinks(%s): %w", path1, err)
  165. }
  166. dst2, err := filepath.EvalSymlinks(path2)
  167. if err != nil && !errors.Is(err, fs.ErrNotExist) {
  168. return false, fmt.Errorf("EvalSymlinks(%s): %w", path2, err)
  169. }
  170. return dst1 == dst2, nil
  171. }