diff --git a/service_darwin.go b/service_darwin.go index 237d7902..d0f1153c 100644 --- a/service_darwin.go +++ b/service_darwin.go @@ -18,9 +18,11 @@ import ( "time" ) -const maxPathSize = 32 * 1024 - -const version = "darwin-launchd" +const ( + maxPathSize = 32 * 1024 + version = "darwin-launchd" + controlCmd = "launchctl" +) type darwinSystem struct{} @@ -96,14 +98,15 @@ func (s *darwinLaunchdService) getHomeDir() (string, error) { } func (s *darwinLaunchdService) getServiceFilePath() (string, error) { + serviceFile := s.Name + ".plist" if s.userService { homeDir, err := s.getHomeDir() if err != nil { return "", err } - return homeDir + "/Library/LaunchAgents/" + s.Name + ".plist", nil + return filepath.Join(homeDir, "/Library/LaunchAgents/", serviceFile), nil } - return "/Library/LaunchDaemons/" + s.Name + ".plist", nil + return filepath.Join("/Library/LaunchDaemons/", serviceFile), nil } func (s *darwinLaunchdService) template() *template.Template { @@ -184,28 +187,45 @@ func (s *darwinLaunchdService) Uninstall() error { } func (s *darwinLaunchdService) Status() (Status, error) { - exitCode, out, err := runWithOutput("launchctl", "list", s.Name) - if exitCode == 0 && err != nil { - if !strings.Contains(err.Error(), "failed with stderr") { - return StatusUnknown, err + var ( + // Scan output for a valid PID. + re = regexp.MustCompile(`"PID" = ([0-9]+);`) + isRunning = func(out string) bool { + matches := re.FindStringSubmatch(out) + return len(matches) == 2 // `PID = 1234` } - } + canIgnore = func(err error) bool { + return err == nil || + strings.Contains(err.Error(), + "Could not find service") + } + ) - re := regexp.MustCompile(`"PID" = ([0-9]+);`) - matches := re.FindStringSubmatch(out) - if len(matches) == 2 { + // Get output from `list`. + controlArgs := []string{"list", s.Name} + _, out, err := runWithOutput(controlCmd, controlArgs...) + if !canIgnore(err) { + return StatusUnknown, err + } + if isRunning(out) { return StatusRunning, nil } + // `list` will always return a "job" entry if the job is "loaded" + // (see launchd documentation for keyword semantics) + // We'll check if the plist actually exist to determine if it's installed or not. + // And since it's not running, we can assume it's stopped (but still installed). confPath, err := s.getServiceFilePath() if err != nil { return StatusUnknown, err } - - if _, err = os.Stat(confPath); err == nil { + if _, err := os.Stat(confPath); err == nil { return StatusStopped, nil + } else if !errors.Is(err, os.ErrNotExist) { + return StatusUnknown, err } + // Otherwise assume the service is not installed. return StatusUnknown, ErrNotInstalled } @@ -214,14 +234,20 @@ func (s *darwinLaunchdService) Start() error { if err != nil { return err } - return run("launchctl", "load", confPath) + if err := run(controlCmd, "load", confPath); err != nil { + return err + } + if !s.Option.bool(optionRunAtLoad, optionRunAtLoadDefault) { + return run(controlCmd, "start", s.Name) + } + return nil } func (s *darwinLaunchdService) Stop() error { confPath, err := s.getServiceFilePath() if err != nil { return err } - return run("launchctl", "unload", confPath) + return run(controlCmd, "unload", confPath) } func (s *darwinLaunchdService) Restart() error { err := s.Stop() diff --git a/service_unix.go b/service_unix.go index 69596224..5130d225 100644 --- a/service_unix.go +++ b/service_unix.go @@ -7,12 +7,13 @@ package service import ( - "bytes" + "errors" "fmt" "io" "io/ioutil" "log/syslog" "os/exec" + "strings" "syscall" ) @@ -64,76 +65,80 @@ func runWithOutput(command string, arguments ...string) (int, string, error) { return runCommand(command, true, arguments...) } -func runCommand(command string, readStdout bool, arguments ...string) (int, string, error) { - cmd := exec.Command(command, arguments...) - - var output string - var stdout io.ReadCloser - var err error - +func runCommand(command string, readStdout bool, arguments ...string) (exitStatus int, stdout string, err error) { + var ( + cmd = exec.Command(command, arguments...) + cmdErr = fmt.Errorf("exec `%s` failed", strings.Join(cmd.Args, " ")) + stdoutPipe, stderrPipe io.ReadCloser + ) + if stderrPipe, err = cmd.StderrPipe(); err != nil { + err = fmt.Errorf("%s to connect stderr pipe: %w", cmdErr, err) + return + } if readStdout { - // Connect pipe to read Stdout - stdout, err = cmd.StdoutPipe() - - if err != nil { - // Failed to connect pipe - return 0, "", fmt.Errorf("%q failed to connect stdout pipe: %v", command, err) + if stdoutPipe, err = cmd.StdoutPipe(); err != nil { + err = fmt.Errorf("%s to connect stdout pipe: %w", cmdErr, err) + return } } - // Connect pipe to read Stderr - stderr, err := cmd.StderrPipe() - - if err != nil { - // Failed to connect pipe - return 0, "", fmt.Errorf("%q failed to connect stderr pipe: %v", command, err) + // Execute the command. + if err = cmd.Start(); err != nil { + err = fmt.Errorf("%s: %w", cmdErr, err) + return } - // Do not use cmd.Run() - if err := cmd.Start(); err != nil { - // Problem while copying stdin, stdout, or stderr - return 0, "", fmt.Errorf("%q failed: %v", command, err) - } + // Process command outputs. + var ( + pipeErr = fmt.Errorf("%s while attempting to read", cmdErr) + stdoutErr = fmt.Errorf("%s from stdout", pipeErr) + stderrErr = fmt.Errorf("%s from stderr", pipeErr) - // Zero exit status - // Darwin: launchctl can fail with a zero exit status, - // so check for emtpy stderr - if command == "launchctl" { - slurp, _ := ioutil.ReadAll(stderr) - if len(slurp) > 0 && !bytes.HasSuffix(slurp, []byte("Operation now in progress\n")) { - return 0, "", fmt.Errorf("%q failed with stderr: %s", command, slurp) - } + errBuffer, readErr = ioutil.ReadAll(stderrPipe) + stderr = strings.TrimSuffix(string(errBuffer), "\n") + + haveStdErr = len(stderr) != 0 + ) + + // Always read stderr. + if readErr != nil { + err = fmt.Errorf("%s: %w", stderrErr, readErr) + return } + // Maybe read stdout. if readStdout { - out, err := ioutil.ReadAll(stdout) - if err != nil { - return 0, "", fmt.Errorf("%q failed while attempting to read stdout: %v", command, err) - } else if len(out) > 0 { - output = string(out) + outBuffer, readErr := ioutil.ReadAll(stdoutPipe) + if readErr != nil { + err = fmt.Errorf("%s: %w", stdoutErr, readErr) + return } + stdout = string(outBuffer) } - if err := cmd.Wait(); err != nil { - exitStatus, ok := isExitError(err) - if ok { - // Command didn't exit with a zero exit status. - return exitStatus, output, err + // Wait for command to finish. + if runErr := cmd.Wait(); runErr != nil { + var execErr *exec.ExitError + if errors.As(runErr, &execErr) { + if status, ok := execErr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + } } - - // An error occurred and there is no exit status. - return 0, output, fmt.Errorf("%q failed: %v", command, err) + err = fmt.Errorf("%w: %s", cmdErr, runErr) + if haveStdErr { + err = fmt.Errorf("%w with stderr: %s", err, stderr) + } + return } - return 0, output, nil -} - -func isExitError(err error) (int, bool) { - if exiterr, ok := err.(*exec.ExitError); ok { - if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { - return status.ExitStatus(), true - } + // Darwin: launchctl can fail with a zero exit status, + // so stderr must be inspected. + systemIsDarwin := command == "launchctl" + if systemIsDarwin && + haveStdErr && + !strings.HasSuffix(stderr, "Operation now in progress") { + err = fmt.Errorf("%w with stderr: %s", cmdErr, stderr) } - return 0, false + return }