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
This commit is contained in:
committed by
Daniel Theophanes
parent
4cdeddd6f4
commit
45244176fc
+16
-1
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
+56
-6
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user