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>