Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 43 additions & 17 deletions service_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Comment thread
djdv marked this conversation as resolved.
return StatusStopped, nil
} else if !errors.Is(err, os.ErrNotExist) {
return StatusUnknown, err
}

// Otherwise assume the service is not installed.
return StatusUnknown, ErrNotInstalled
}

Expand All @@ -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()
Expand Down
115 changes: 60 additions & 55 deletions service_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
package service

import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"log/syslog"
"os/exec"
"strings"
"syscall"
)

Expand Down Expand Up @@ -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
}