github.com/mutagen-io/mutagen@v0.18.0-rc1/pkg/daemon/register_darwin.go (about) 1 package daemon 2 3 // The implementation of daemon registration is largely based on these two 4 // articles: 5 // https://developer.apple.com/library/content/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html 6 // https://developer.apple.com/library/content/technotes/tn2083/_index.html#//apple_ref/doc/uid/DTS10003794-CH1-SUBSECTION44 7 8 import ( 9 "bytes" 10 "errors" 11 "fmt" 12 "os" 13 "os/exec" 14 "path/filepath" 15 16 "github.com/mutagen-io/mutagen/pkg/filesystem" 17 ) 18 19 // RegistrationSupported indicates whether or not daemon registration is 20 // supported on this platform. 21 const RegistrationSupported = true 22 23 const launchdPlistTemplate = `<?xml version="1.0" encoding="UTF-8"?> 24 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 25 <plist version="1.0"> 26 <dict> 27 <key>Label</key> 28 <string>io.mutagen.mutagen</string> 29 <key>ProgramArguments</key> 30 <array> 31 <string>%s</string> 32 <string>daemon</string> 33 <string>run</string> 34 </array> 35 <key>LimitLoadToSessionType</key> 36 <string>Aqua</string> 37 <key>KeepAlive</key> 38 <true/> 39 </dict> 40 </plist> 41 ` 42 43 const ( 44 // libraryDirectoryName is the name of the Library directory inside the 45 // user's home directory. 46 libraryDirectoryName = "Library" 47 // libraryDirectoryPermissions are the permissions to use for Library 48 // directory creation in the event that it does not exist. 49 libraryDirectoryPermissions = 0700 50 51 // launchAgentsDirectoryName is the name of the LaunchAgents directory 52 // inside the Library directory. 53 launchAgentsDirectoryName = "LaunchAgents" 54 // launchAgentsDirectoryPermissions are the permissions to use for 55 // LaunchAgents directory creation in the event that it does not exist. 56 launchAgentsDirectoryPermissions = 0755 57 58 // launchdPlistName is the name of the launchd property list file to create 59 // to register the daemon for automatic startup. 60 launchdPlistName = "io.mutagen.mutagen.plist" 61 // launchdPlistPermissions are the permissions to use for the launchd 62 // property list file. 63 launchdPlistPermissions = 0644 64 ) 65 66 // Register performs automatic daemon startup registration. 67 func Register() error { 68 // If we're already registered, don't do anything. 69 if registered, err := registered(); err != nil { 70 return fmt.Errorf("unable to determine registration status: %w", err) 71 } else if registered { 72 return nil 73 } 74 75 // Acquire the daemon lock to ensure the daemon isn't running. We switch the 76 // start and stop mechanism depending on whether or not we're registered, so 77 // we need to make sure we don't try to stop a daemon started using a 78 // different mechanism. 79 lock, err := AcquireLock() 80 if err != nil { 81 return errors.New("unable to alter registration while daemon is running") 82 } 83 defer lock.Release() 84 85 // Compute the path to the user's home directory. 86 homeDirectory, err := os.UserHomeDir() 87 if err != nil { 88 return fmt.Errorf("unable to compute path to home directory: %w", err) 89 } 90 91 // Ensure the user's Library directory exists. 92 targetPath := filepath.Join(homeDirectory, libraryDirectoryName) 93 if err := os.MkdirAll(targetPath, libraryDirectoryPermissions); err != nil { 94 return fmt.Errorf("unable to create Library directory: %w", err) 95 } 96 97 // Ensure the LaunchAgents directory exists. 98 targetPath = filepath.Join(targetPath, launchAgentsDirectoryName) 99 if err := os.MkdirAll(targetPath, launchAgentsDirectoryPermissions); err != nil { 100 return fmt.Errorf("unable to create LaunchAgents directory: %w", err) 101 } 102 103 // Compute the path to the current executable. 104 executablePath, err := os.Executable() 105 if err != nil { 106 return fmt.Errorf("unable to determine executable path: %w", err) 107 } 108 109 // Format a launchd plist. 110 plist := fmt.Sprintf(launchdPlistTemplate, executablePath) 111 112 // Attempt to write the launchd plist. 113 targetPath = filepath.Join(targetPath, launchdPlistName) 114 if err := filesystem.WriteFileAtomic(targetPath, []byte(plist), launchdPlistPermissions); err != nil { 115 return fmt.Errorf("unable to write launchd agent plist: %w", err) 116 } 117 118 // Success. 119 return nil 120 } 121 122 // Unregister performs automatic daemon startup de-registration. 123 func Unregister() error { 124 // If we're not registered, don't do anything. 125 if registered, err := registered(); err != nil { 126 return fmt.Errorf("unable to determine registration status: %w", err) 127 } else if !registered { 128 return nil 129 } 130 131 // Acquire the daemon lock to ensure the daemon isn't running. We switch the 132 // start and stop mechanism depending on whether or not we're registered, so 133 // we need to make sure we don't try to stop a daemon started using a 134 // different mechanism. 135 lock, err := AcquireLock() 136 if err != nil { 137 return errors.New("unable to alter registration while daemon is running") 138 } 139 defer lock.Release() 140 141 // Compute the path to the user's home directory. 142 homeDirectory, err := os.UserHomeDir() 143 if err != nil { 144 return fmt.Errorf("unable to compute path to home directory: %w", err) 145 } 146 147 // Compute the launchd plist path. 148 targetPath := filepath.Join( 149 homeDirectory, 150 libraryDirectoryName, 151 launchAgentsDirectoryName, 152 launchdPlistName, 153 ) 154 155 // Attempt to remove the launchd plist. 156 if err := os.Remove(targetPath); err != nil { 157 if !os.IsNotExist(err) { 158 return fmt.Errorf("unable to remove launchd agent plist: %w", err) 159 } 160 } 161 162 // Success. 163 return nil 164 } 165 166 // launchctlSpuriousErrorFragment is a fragment of text that appears in spurious 167 // launchctl load/unload command errors when the daemon run command exits due to 168 // an existing daemon or the launchctl-hosted daemon isn't running. Ignoring 169 // this fragment is important for user-friendly idempotency when using launchd 170 // hosting of the daemon. 171 const launchctlSpuriousErrorFragment = "failed: 5: Input/output error" 172 173 // runLaunchctlIgnoringSpuriousErrors runs a launchctl command and only prints 174 // out error text if it doesn't contain launchctlSpuriousErrorFragment. The 175 // standard error stream for the command must not be set. 176 func runLaunchctlIgnoringSpuriousErrors(command *exec.Cmd) error { 177 err := command.Run() 178 if err != nil { 179 if exitErr, ok := err.(*exec.ExitError); ok { 180 if bytes.Contains(exitErr.Stderr, []byte(launchctlSpuriousErrorFragment)) { 181 return nil 182 } 183 } 184 } 185 return err 186 } 187 188 // registered determines whether or not automatic daemon startup is currently 189 // registered. 190 func registered() (bool, error) { 191 // Compute the path to the user's home directory. 192 homeDirectory, err := os.UserHomeDir() 193 if err != nil { 194 return false, fmt.Errorf("unable to compute path to home directory: %w", err) 195 } 196 197 // Compute the launchd plist path. 198 targetPath := filepath.Join( 199 homeDirectory, 200 libraryDirectoryName, 201 launchAgentsDirectoryName, 202 launchdPlistName, 203 ) 204 205 // Check if it exists and is what's expected. 206 if info, err := os.Lstat(targetPath); err != nil { 207 if os.IsNotExist(err) { 208 return false, nil 209 } 210 return false, fmt.Errorf("unable to query launchd agent plist: %w", err) 211 } else if !info.Mode().IsRegular() { 212 return false, errors.New("unexpected contents at launchd agent plist path") 213 } 214 215 // Success. 216 return true, nil 217 } 218 219 // RegisteredStart potentially handles daemon start operations if the daemon is 220 // registered for automatic start with the system. It returns false if the start 221 // operation was not handled and should be handled by the normal start command. 222 func RegisteredStart() (bool, error) { 223 // Check if we're registered. If not, we don't handle the start request. 224 if registered, err := registered(); err != nil { 225 return false, fmt.Errorf("unable to determine daemon registration status: %w", err) 226 } else if !registered { 227 return false, nil 228 } 229 230 // Compute the path to the user's home directory. 231 homeDirectory, err := os.UserHomeDir() 232 if err != nil { 233 return false, fmt.Errorf("unable to compute path to home directory: %w", err) 234 } 235 236 // Compute the launchd plist path. 237 targetPath := filepath.Join( 238 homeDirectory, 239 libraryDirectoryName, 240 launchAgentsDirectoryName, 241 launchdPlistName, 242 ) 243 244 // Attempt to load the daemon. 245 load := exec.Command("launchctl", "load", targetPath) 246 load.Stdout = os.Stdout 247 if err := runLaunchctlIgnoringSpuriousErrors(load); err != nil { 248 return false, fmt.Errorf("unable to load launchd plist: %w", err) 249 } 250 251 // Success. 252 return true, nil 253 } 254 255 // RegisteredStop potentially handles stop start operations if the daemon is 256 // registered for automatic start with the system. It returns false if the stop 257 // operation was not handled and should be handled by the normal stop command. 258 func RegisteredStop() (bool, error) { 259 // Check if we're registered. If not, we don't handle the stop request. 260 if registered, err := registered(); err != nil { 261 return false, fmt.Errorf("unable to determine daemon registration status: %w", err) 262 } else if !registered { 263 return false, nil 264 } 265 266 // Compute the path to the user's home directory. 267 homeDirectory, err := os.UserHomeDir() 268 if err != nil { 269 return false, fmt.Errorf("unable to compute path to home directory: %w", err) 270 } 271 272 // Compute the launchd plist path. 273 targetPath := filepath.Join( 274 homeDirectory, 275 libraryDirectoryName, 276 launchAgentsDirectoryName, 277 launchdPlistName, 278 ) 279 280 // Attempt to unload the daemon. 281 unload := exec.Command("launchctl", "unload", targetPath) 282 unload.Stdout = os.Stdout 283 if err := runLaunchctlIgnoringSpuriousErrors(unload); err != nil { 284 return false, fmt.Errorf("unable to unload launchd plist: %w", err) 285 } 286 287 // Success. 288 return true, nil 289 }