github.com/sohaha/zlsgo@v1.7.13-0.20240501141223-10dd1a906f76/zutil/daemon/daemon_darwin.go (about) 1 package daemon 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "os/user" 9 "path/filepath" 10 "strings" 11 "text/template" 12 "time" 13 14 "github.com/sohaha/zlsgo/zerror" 15 ) 16 17 type ( 18 darwinSystem struct{} 19 darwinLaunchdService struct { 20 i Iface 21 *Config 22 userService bool 23 } 24 ) 25 26 const version = "darwin-launchd" 27 28 var interactive = false 29 30 func (darwinSystem) String() string { 31 return version 32 } 33 34 func (darwinSystem) Detect() bool { 35 return true 36 } 37 38 func (darwinSystem) Interactive() bool { 39 return interactive 40 } 41 42 func (darwinSystem) New(i Iface, c *Config) (s ServiceIface, err error) { 43 userService := optionUserServiceDefault 44 if s, ok := c.Options[optionUserService]; ok { 45 userService, _ = s.(bool) 46 } 47 48 if c.Context == nil { 49 c.Context = context.Background() 50 } 51 52 s = &darwinLaunchdService{ 53 i: i, 54 Config: c, 55 userService: userService, 56 } 57 58 if !userService { 59 err = isSudo() 60 } 61 return s, err 62 } 63 64 func init() { 65 var err error 66 chooseSystem(darwinSystem{}) 67 interactive, err = isInteractive() 68 zerror.Panic(err) 69 } 70 71 func isInteractive() (bool, error) { 72 return os.Getppid() != 1, nil 73 } 74 75 func (s *darwinLaunchdService) String() string { 76 if len(s.DisplayName) > 0 { 77 return s.DisplayName 78 } 79 return s.Name 80 } 81 82 func (s *darwinLaunchdService) getHomeDir() (string, error) { 83 u, err := user.Current() 84 if err == nil { 85 return u.HomeDir, nil 86 } 87 88 homeDir := os.Getenv("HOME") 89 if homeDir == "" { 90 return "", errors.New("user home directory not found") 91 } 92 return homeDir, nil 93 } 94 95 func (s *darwinLaunchdService) getServiceFilePath() (string, error) { 96 if s.userService { 97 homeDir, err := s.getHomeDir() 98 if err != nil { 99 return "", err 100 } 101 return homeDir + "/Library/LaunchAgents/" + s.Name + ".plist", nil 102 } 103 return "/Library/LaunchDaemons/" + s.Name + ".plist", nil 104 } 105 106 func (s *darwinLaunchdService) Install() error { 107 confPath, err := s.getServiceFilePath() 108 if err != nil { 109 return err 110 } 111 _, err = os.Stat(confPath) 112 if err == nil { 113 return fmt.Errorf("init already exists: %s", confPath) 114 } 115 116 if s.userService { 117 // ~/Library/LaunchAgents exists 118 err = os.MkdirAll(filepath.Dir(confPath), 0700) 119 if err != nil { 120 return err 121 } 122 } 123 124 f, err := os.Create(confPath) 125 if err != nil { 126 return err 127 } 128 defer f.Close() 129 130 keepAlive := optionKeepAliveDefault 131 if v, ok := s.Options[optionKeepAlive]; ok { 132 keepAlive, _ = v.(bool) 133 } 134 load := isServiceRestart(s.Config) 135 sessionCreate := optionSessionCreateDefault 136 if v, ok := s.Options[optionSessionCreate]; ok { 137 sessionCreate, _ = v.(bool) 138 } 139 140 path := s.execPath() 141 to := &struct { 142 *Config 143 Path string 144 145 KeepAlive, RunAtLoad bool 146 SessionCreate bool 147 }{ 148 Config: s.Config, 149 Path: path, 150 KeepAlive: keepAlive, 151 RunAtLoad: load, 152 SessionCreate: sessionCreate, 153 } 154 155 functions := template.FuncMap{ 156 "bool": func(v bool) string { 157 if v { 158 return "true" 159 } 160 return "false" 161 }, 162 } 163 t := template.Must(template.New("launchdConfig").Funcs(functions).Parse(launchdConfig)) 164 return t.Execute(f, to) 165 } 166 167 func (s *darwinLaunchdService) Uninstall() error { 168 var ( 169 err error 170 confPath string 171 ) 172 if err = s.Stop(); err != nil { 173 return err 174 } 175 if confPath, err = s.getServiceFilePath(); err != nil { 176 return err 177 } 178 return os.Remove(confPath) 179 } 180 181 func (s *darwinLaunchdService) Start() error { 182 confPath, err := s.getServiceFilePath() 183 if err != nil { 184 return err 185 } 186 err = run("launchctl", "load", confPath) 187 188 return err 189 } 190 191 func (s *darwinLaunchdService) Stop() error { 192 confPath, err := s.getServiceFilePath() 193 if err != nil { 194 return err 195 } 196 _ = run("launchctl", "stop", confPath) 197 for { 198 err = run("launchctl", "unload", confPath) 199 if err == nil || (strings.Contains(err.Error(), "Could not find specified service") || !strings.Contains(err.Error(), "Operation now in progress")) { 200 err = nil 201 break 202 } 203 time.Sleep(500 * time.Millisecond) 204 } 205 return err 206 } 207 208 func (s *darwinLaunchdService) Status() string { 209 res, _ := runGrep(s.Name+"$", "launchctl", "list") 210 if res != "" { 211 return "Running" 212 } 213 return "Stop" 214 } 215 216 func (s *darwinLaunchdService) Restart() error { 217 err := s.Stop() 218 if err != nil { 219 return err 220 } 221 time.Sleep(50 * time.Millisecond) 222 return s.Start() 223 } 224 225 func (s *darwinLaunchdService) Run() error { 226 err := s.i.Start(s) 227 if err != nil { 228 return err 229 } 230 runWait := func() { 231 select { 232 case <-SingleKillSignal(): 233 case <-s.Config.Context.Done(): 234 } 235 } 236 if v, ok := s.Options[optionRunWait]; ok { 237 runWait, _ = v.(func()) 238 } 239 240 runWait() 241 return s.i.Stop(s) 242 } 243 244 var launchdConfig = `<?xml version='1.0' encoding='UTF-8'?> 245 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" 246 "http://www.apple.com/DTDs/PropertyList-1.0.dtd" > 247 <plist version='1.0'> 248 <dict> 249 <key>Label</key><string>{{html .Name}}</string> 250 <key>ProgramArguments</key> 251 <array> 252 <string>{{html .Path}}</string> 253 {{range .Config.Arguments}} 254 <string>{{html .}}</string> 255 {{end}} 256 </array> 257 {{if .UserName}}<key>UserName</key><string>{{html .UserName}}</string>{{end}} 258 {{if .RootDir}}<key>RootDirectory</key><string>{{html .RootDir}}</string>{{end}} 259 {{if .WorkingDir}}<key>WorkingDirectory</key><string>{{html .WorkingDir}}</string>{{end}} 260 <key>SessionCreate</key><{{bool .SessionCreate}}/> 261 <key>KeepAlive</key><{{bool .KeepAlive}}/> 262 <key>RunAtLoad</key><{{bool .RunAtLoad}}/> 263 <key>Disabled</key><false/> 264 </dict> 265 </plist> 266 ` 267 268 // <key>StandardOutPath</key> 269 // <string>/tmp/zlsgo/{{html .Name}}.log</string> 270 // <key>StandardErrorPath</key> 271 // <string>/tmp/zlsgo/{{html .Name}}.err</string>