Permalink
Browse files

Add support for retrieving service status (#143)

* update command execution to support returing error codes, and stdout

add function to service interface to support returning service status

clean up places where strings were being converted to strings

* Spelling fix

* Simply service Status method

Add stopped detection to launchd
Switch to case statements
Return ErrNotInstalled in situations where we should
  • Loading branch information...
SteelPhase authored and kardianos committed Aug 22, 2018
1 parent 4cdeddd commit 45244176fc183034f15c4f1c2f801da5e25828b7
Showing with 198 additions and 15 deletions.
  1. +16 −1 service.go
  2. +28 −0 service_darwin.go
  3. +21 −3 service_systemd_linux.go
  4. +17 −0 service_sysv_linux.go
  5. +56 −6 service_unix.go
  6. +20 −5 service_upstart_linux.go
  7. +40 −0 service_windows.go
View
@@ -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.
View
@@ -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 {
View
@@ -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")
}
View
@@ -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")
}
View
@@ -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 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 nil
return 0, false
}
View
@@ -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)
}
View
@@ -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 {

0 comments on commit 4524417

Please sign in to comment.