diff --git a/service.go b/service.go index edda510..5176e70 100644 --- a/service.go +++ b/service.go @@ -88,6 +88,16 @@ const ( optionLaunchdConfig = "LaunchdConfig" ) +// Status represents service status as an byte value +type Status byte + +// Status of service represented as an byte +const ( + StatusUnknown Status = iota // Status is unable to be determined due to an error or it was not installed. + StatusRunning + StatusStopped +) + // Config provides the setup for a Service. The Name field is required. type Config struct { Name string // Required name of the service. No spaces suggested. @@ -132,10 +142,12 @@ var ( ) var ( - // ErrNameFieldRequired is returned when Conifg.Name is empty. + // ErrNameFieldRequired is returned when Config.Name is empty. ErrNameFieldRequired = errors.New("Config.Name field is required.") // ErrNoServiceSystemDetected is returned when no system was detected. ErrNoServiceSystemDetected = errors.New("No service system detected.") + // ErrNotInstalled is returned when the service is not installed + ErrNotInstalled = errors.New("the service is not installed") ) // New creates a new service based on a service interface and configuration. @@ -334,6 +346,9 @@ type Service interface { // String displays the name of the service. The display name if present, // otherwise the name. String() string + + // Status returns the current service status. + Status() (Status, error) } // ControlAction list valid string texts to use in Control. diff --git a/service_darwin.go b/service_darwin.go index 1bad9df..c0a4673 100644 --- a/service_darwin.go +++ b/service_darwin.go @@ -11,6 +11,8 @@ import ( "os/signal" "os/user" "path/filepath" + "regexp" + "strings" "syscall" "text/template" "time" @@ -175,6 +177,32 @@ func (s *darwinLaunchdService) Uninstall() error { return os.Remove(confPath) } +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 + } + } + + re := regexp.MustCompile(`"PID" = ([0-9]+);`) + matches := re.FindStringSubmatch(out) + if len(matches) == 2 { + return StatusRunning, nil + } + + confPath, err := s.getServiceFilePath() + if err != nil { + return StatusUnknown, err + } + + if _, err = os.Stat(confPath); err == nil { + return StatusStopped, nil + } + + return StatusUnknown, ErrNotInstalled +} + func (s *darwinLaunchdService) Start() error { confPath, err := s.getServiceFilePath() if err != nil { diff --git a/service_systemd_linux.go b/service_systemd_linux.go index f66c2dd..9016be2 100644 --- a/service_systemd_linux.go +++ b/service_systemd_linux.go @@ -8,10 +8,10 @@ import ( "errors" "fmt" "os" - "os/exec" "os/signal" "regexp" "strconv" + "strings" "syscall" "text/template" ) @@ -57,13 +57,13 @@ func (s *systemd) configPath() (cp string, err error) { } func (s *systemd) getSystemdVersion() int64 { - out, err := exec.Command("/usr/bin/systemctl", "--version").Output() + _, out, err := runWithOutput("systemctl", "--version") if err != nil { return -1 } re := regexp.MustCompile(`systemd ([0-9]+)`) - matches := re.FindStringSubmatch(string(out)) + matches := re.FindStringSubmatch(out) if len(matches) != 2 { return -1 } @@ -189,6 +189,24 @@ func (s *systemd) Run() (err error) { return s.i.Stop(s) } +func (s *systemd) Status() (Status, error) { + exitCode, out, err := runWithOutput("systemctl", "is-active", s.Name) + if exitCode == 0 && err != nil { + return StatusUnknown, err + } + + switch { + case strings.HasPrefix(out, "active"): + return StatusRunning, nil + case strings.HasPrefix(out, "inactive"): + return StatusStopped, nil + case strings.HasPrefix(out, "failed"): + return StatusUnknown, errors.New("service in failed state") + default: + return StatusUnknown, ErrNotInstalled + } +} + func (s *systemd) Start() error { return run("systemctl", "start", s.Name+".service") } diff --git a/service_sysv_linux.go b/service_sysv_linux.go index 0283cd8..27da15d 100644 --- a/service_sysv_linux.go +++ b/service_sysv_linux.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "os/signal" + "strings" "syscall" "text/template" "time" @@ -143,6 +144,22 @@ func (s *sysv) Run() (err error) { return s.i.Stop(s) } +func (s *sysv) Status() (Status, error) { + _, out, err := runWithOutput("service", s.Name, "status") + if err != nil { + return StatusUnknown, err + } + + switch { + case strings.HasPrefix(out, "Running"): + return StatusRunning, nil + case strings.HasPrefix(out, "Stopped"): + return StatusStopped, nil + default: + return StatusUnknown, ErrNotInstalled + } +} + func (s *sysv) Start() error { return run("service", s.Name, "start") } diff --git a/service_unix.go b/service_unix.go index bf4e2bd..58a3f20 100644 --- a/service_unix.go +++ b/service_unix.go @@ -8,9 +8,11 @@ package service import ( "fmt" + "io" "io/ioutil" "log/syslog" "os/exec" + "syscall" ) func newSysLogger(name string, errs chan<- error) (Logger, error) { @@ -53,20 +55,43 @@ func (s sysLogger) Infof(format string, a ...interface{}) error { } func run(command string, arguments ...string) error { + _, _, err := runCommand(command, false, arguments...) + return err +} + +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 + + 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) + } + } + // Connect pipe to read Stderr stderr, err := cmd.StderrPipe() if err != nil { // Failed to connect pipe - return fmt.Errorf("%q failed to connect stderr pipe: %v", command, err) + return 0, "", fmt.Errorf("%q failed to connect stderr pipe: %v", command, err) } // Do not use cmd.Run() if err := cmd.Start(); err != nil { // Problem while copying stdin, stdout, or stderr - return fmt.Errorf("%q failed: %v", command, err) + return 0, "", fmt.Errorf("%q failed: %v", command, err) } // Zero exit status @@ -75,14 +100,39 @@ func run(command string, arguments ...string) error { if command == "launchctl" { slurp, _ := ioutil.ReadAll(stderr) if len(slurp) > 0 { - return fmt.Errorf("%q failed with stderr: %s", command, slurp) + return 0, "", fmt.Errorf("%q failed with stderr: %s", command, slurp) + } + } + + 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) } } if err := cmd.Wait(); err != nil { - // Command didn't exit with a zero exit status. - return fmt.Errorf("%q failed: %v", command, err) + exitStatus, ok := isExitError(err) + if ok { + // Command didn't exit with a zero exit status. + return exitStatus, output, err + } + + // An error occurred and there is no exit status. + return 0, output, fmt.Errorf("%q failed: %v", command, err) } - return nil + 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 + } + } + + return 0, false } diff --git a/service_upstart_linux.go b/service_upstart_linux.go index 2167612..800db35 100644 --- a/service_upstart_linux.go +++ b/service_upstart_linux.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "os" - "os/exec" "os/signal" "regexp" "strings" @@ -21,8 +20,8 @@ func isUpstart() bool { return true } if _, err := os.Stat("/sbin/initctl"); err == nil { - if out, err := exec.Command("/sbin/initctl", "--version").Output(); err == nil { - if strings.Contains(string(out), "initctl (upstart") { + if _, out, err := runWithOutput("/sbin/initctl", "--version"); err == nil { + if strings.Contains(out, "initctl (upstart") { return true } } @@ -96,13 +95,13 @@ func (s *upstart) hasSetUIDStanza() bool { } func (s *upstart) getUpstartVersion() []int { - out, err := exec.Command("/sbin/initctl", "--version").Output() + _, out, err := runWithOutput("/sbin/initctl", "--version") if err != nil { return nil } re := regexp.MustCompile(`initctl \(upstart (\d+.\d+.\d+)\)`) - matches := re.FindStringSubmatch(string(out)) + matches := re.FindStringSubmatch(out) if len(matches) != 2 { return nil } @@ -194,6 +193,22 @@ func (s *upstart) Run() (err error) { return s.i.Stop(s) } +func (s *upstart) Status() (Status, error) { + exitCode, out, err := runWithOutput("initctl", "status", s.Name) + if exitCode == 0 && err != nil { + return StatusUnknown, err + } + + switch { + case strings.HasPrefix(out, fmt.Sprintf("%s start/running", s.Name)): + return StatusRunning, nil + case strings.HasPrefix(out, fmt.Sprintf("%s stop/waiting", s.Name)): + return StatusStopped, nil + default: + return StatusUnknown, ErrNotInstalled + } +} + func (s *upstart) Start() error { return run("initctl", "start", s.Name) } diff --git a/service_windows.go b/service_windows.go index f081dd0..bb53445 100644 --- a/service_windows.go +++ b/service_windows.go @@ -275,6 +275,46 @@ func (ws *windowsService) Run() error { return ws.i.Stop(ws) } +func (ws *windowsService) Status() (Status, error) { + m, err := mgr.Connect() + if err != nil { + return StatusUnknown, err + } + defer m.Disconnect() + + s, err := m.OpenService(ws.Name) + if err != nil { + if err.Error() == "The specified service does not exist as an installed service." { + return StatusUnknown, ErrNotInstalled + } + return StatusUnknown, err + } + + status, err := s.Query() + if err != nil { + return StatusUnknown, err + } + + switch status.State { + case svc.StartPending: + fallthrough + case svc.Running: + return StatusRunning, nil + case svc.PausePending: + fallthrough + case svc.Paused: + fallthrough + case svc.ContinuePending: + fallthrough + case svc.StopPending: + fallthrough + case svc.Stopped: + return StatusStopped, nil + default: + return StatusUnknown, fmt.Errorf("unknown status %s", status) + } +} + func (ws *windowsService) Start() error { m, err := mgr.Connect() if err != nil {