Compare commits

..

4 Commits

Author SHA1 Message Date
d610b8cb61 static: update timestamp 2019-07-07 01:34:17 -06:00
AJ ONeal
75ae3eea07 slightly more friendly service start output 2019-07-07 06:55:05 +00:00
AJ ONeal
04e162b20f Ubuntu: start on install 2019-07-07 06:48:25 +00:00
2b7148c3ae MacOS: start on install 2019-07-05 12:48:58 -06:00
11 changed files with 497 additions and 55 deletions

View File

@ -172,6 +172,7 @@ move serviceman.exe %userprofile%\bin\serviceman.exe
**All Others**
```
chmod a+x ./serviceman
sudo mv ./serviceman /usr/local/bin/
```

View File

@ -12,18 +12,20 @@
# sudo journalctl {{ if not .System -}} --user {{ end -}} -xefu {{ .Name }}
[Unit]
Description={{ .Title }} - {{ .Desc }}
Description={{ .Title }} {{ if .Desc }}- {{ .Desc }}{{ end }}
{{ if .URL -}}
Documentation={{ .URL }}
{{ end -}}
{{ if .System -}}
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service
{{- end }}
{{ end -}}
[Service]
# Restart on crash (bad signal), but not on 'clean' failure (error exit code)
# Allow up to 3 restarts within 10 seconds
# (it's unlikely that a user or properly-running script will do this)
Restart=on-abnormal
Restart=always
StartLimitInterval=10
StartLimitBurst=3
@ -36,7 +38,7 @@ Group={{ .Group }}
{{ if .Workdir -}}
WorkingDirectory={{ .Workdir }}
{{ end -}}
ExecStart={{if .Interpreter }}{{ .Interpreter }} {{ end }}{{ .Exec }} {{- range $arg := .Argv }}{{ $arg }} {{- end }}
ExecStart={{if .Interpreter }}{{ .Interpreter }} {{ end }}{{ .Exec }}{{ range $arg := .Argv }} {{ $arg }}{{ end }}
ExecReload=/bin/kill -USR1 $MAINPID
{{if .Production -}}

View File

@ -6,13 +6,75 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"
"git.rootprojects.org/root/go-serviceman/manager/static"
"git.rootprojects.org/root/go-serviceman/service"
)
const (
srvExt = ".plist"
srvSysPath = "/Library/LaunchDaemons"
srvUserPath = "Library/LaunchAgents"
)
var srvLen int
func init() {
srvLen = len(srvExt)
}
func start(system bool, home string, name string) error {
sys, user, err := getMatchingSrvs(home, name)
if nil != err {
return err
}
var service string
if system {
service, err = getOneSysSrv(sys, user, name)
if nil != err {
return err
}
service = filepath.Join(srvSysPath, service)
} else {
service, err = getOneUserSrv(home, sys, user, name)
if nil != err {
return err
}
service = filepath.Join(home, srvUserPath, service)
}
cmds := []Runnable{
Runnable{
Exec: "launchctl",
Args: []string{"unload", "-w", service},
Must: false,
},
Runnable{
Exec: "launchctl",
Args: []string{"load", "-w", service},
Must: true,
},
}
cmds = adjustPrivs(system, cmds)
fmt.Println()
fmt.Println("Starting launchd service...")
for i := range cmds {
exe := cmds[i]
fmt.Println("\t" + exe.String())
err := exe.Run()
if nil != err {
return err
}
}
fmt.Println()
return nil
}
func install(c *service.Service) error {
// Darwin-specific config options
if c.PrivilegedPorts {
@ -20,9 +82,9 @@ func install(c *service.Service) error {
return fmt.Errorf("You must use root-owned LaunchDaemons (not user-owned LaunchAgents) to use priveleged ports on OS X")
}
}
plistDir := "/Library/LaunchDaemons/"
plistDir := srvSysPath
if !c.System {
plistDir = filepath.Join(c.Home, "Library/LaunchAgents")
plistDir = filepath.Join(c.Home, srvUserPath)
}
// Check paths first
@ -53,19 +115,18 @@ func install(c *service.Service) error {
plistName := c.ReverseDNS + ".plist"
plistPath := filepath.Join(plistDir, plistName)
if err := ioutil.WriteFile(plistPath, rw.Bytes(), 0644); err != nil {
return fmt.Errorf("Error writing %s: %v", plistPath, err)
}
return fmt.Errorf("ioutil.WriteFile error: %v", err)
// TODO --no-start
err = start(c.System, c.Home, c.ReverseDNS)
if nil != err {
fmt.Printf("If things don't go well you should be able to get additional logging from launchctl:\n")
fmt.Printf("\tsudo launchctl log level debug\n")
fmt.Printf("\ttail -f /var/log/system.log\n")
return err
}
fmt.Printf("Installed. To start '%s' run the following:\n", c.Name)
// TODO template config file
if "" != c.Home {
plistPath = strings.Replace(plistPath, c.Home, "~", 1)
}
sudo := ""
if c.System {
sudo = "sudo "
}
fmt.Printf("\t%slaunchctl load -w %s\n", sudo, plistPath)
fmt.Printf("Added and started '%s' as a launchctl service.\n", c.Name)
return nil
}

View File

@ -12,6 +12,102 @@ import (
"git.rootprojects.org/root/go-serviceman/service"
)
var (
srvLen int
srvExt = ".service"
srvSysPath = "/etc/systemd/system"
// Not sure which of these it's supposed to be...
// * ~/.local/share/systemd/user/watchdog.service
// * ~/.config/systemd/user/watchdog.service
// https://wiki.archlinux.org/index.php/Systemd/User
// This seems to work on Ubuntu
srvUserPath = ".config/systemd/user"
)
func init() {
srvLen = len(srvExt)
}
func start(system bool, home string, name string) error {
sys, user, err := getMatchingSrvs(home, name)
if nil != err {
return err
}
var service string
if system {
service, err = getOneSysSrv(sys, user, name)
if nil != err {
return err
}
service = filepath.Join(srvSysPath, service)
} else {
service, err = getOneUserSrv(home, sys, user, name)
if nil != err {
return err
}
service = filepath.Join(home, srvUserPath, service)
}
var cmds []Runnable
if system {
cmds = []Runnable{
Runnable{
Exec: "systemctl",
Args: []string{"daemon-reload"},
Must: false,
},
Runnable{
Exec: "systemctl",
Args: []string{"stop", name + ".service"},
Must: false,
},
Runnable{
Exec: "systemctl",
Args: []string{"start", name + ".service"},
Badwords: []string{"not found", "failed"},
Must: true,
},
}
} else {
cmds = []Runnable{
Runnable{
Exec: "systemctl",
Args: []string{"--user", "daemon-reload"},
Must: false,
},
Runnable{
Exec: "systemctl",
Args: []string{"stop", "--user", name + ".service"},
Must: false,
},
Runnable{
Exec: "systemctl",
Args: []string{"start", "--user", name + ".service"},
Badwords: []string{"not found", "failed"},
Must: true,
},
}
}
cmds = adjustPrivs(system, cmds)
fmt.Println()
fmt.Println("Starting systemd service unit...")
for i := range cmds {
exe := cmds[i]
fmt.Println("\t" + exe.String())
err := exe.Run()
if nil != err {
return err
}
}
fmt.Println()
return nil
}
func install(c *service.Service) error {
// Linux-specific config options
if c.System {
@ -22,17 +118,12 @@ func install(c *service.Service) error {
if "" == c.Group {
c.Group = c.User
}
serviceDir := "/etc/systemd/system/"
// Check paths first
serviceName := c.Name + ".service"
serviceDir := srvSysPath
if !c.System {
// Not sure which of these it's supposed to be...
// * ~/.local/share/systemd/user/watchdog.service
// * ~/.config/systemd/user/watchdog.service
// https://wiki.archlinux.org/index.php/Systemd/User
serviceDir = filepath.Join(c.Home, ".local/share/systemd/user")
err := os.MkdirAll(filepath.Dir(serviceDir), 0755)
serviceDir = filepath.Join(c.Home, srvUserPath)
err := os.MkdirAll(serviceDir, 0755)
if nil != err {
return err
}
@ -56,22 +147,27 @@ func install(c *service.Service) error {
}
// Write the file out
serviceName := c.Name + ".service"
servicePath := filepath.Join(serviceDir, serviceName)
if err := ioutil.WriteFile(servicePath, rw.Bytes(), 0644); err != nil {
return fmt.Errorf("ioutil.WriteFile error: %v", err)
return fmt.Errorf("Error writing %s: %v", servicePath, err)
}
// TODO template this as well?
userspace := ""
sudo := "sudo "
if !c.System {
userspace = "--user "
sudo = ""
// TODO --no-start
err = start(c.System, c.Home, c.Name)
if nil != err {
sudo := ""
// --user-unit rather than --user --unit for older systemd
unit := "--user-unit"
if c.System {
sudo = "sudo "
unit = "--unit"
}
fmt.Printf("System service installed as '%s'.\n", servicePath)
fmt.Printf("Run the following to start '%s':\n", c.Name)
fmt.Printf("\t" + sudo + "systemctl " + userspace + "daemon-reload\n")
fmt.Printf("\t"+sudo+"systemctl "+userspace+"restart %s.service\n", c.Name)
fmt.Printf("\t"+sudo+"journalctl "+userspace+"-xefu %s\n", c.Name)
fmt.Printf("If things don't go well you should be able to get additional logging from journalctl:\n")
fmt.Printf("\t%sjournalctl -xe %s %s.service\n", sudo, unit, c.Name)
return err
}
fmt.Printf("Added and started '%s' as a systemd service.\n", c.Name)
return nil
}

View File

@ -14,6 +14,17 @@ import (
"golang.org/x/sys/windows/registry"
)
var (
srvLen int
srvExt = ".json"
srvSysPath = "/opt/serviceman/etc"
srvUserPath = ".local/opt/serviceman/etc"
)
func init() {
srvLen = len(srvExt)
}
// TODO nab some goodness from https://github.com/takama/daemon
// TODO system service requires elevated privileges

221
manager/start.go Normal file
View File

@ -0,0 +1,221 @@
package manager
import (
"fmt"
"io/ioutil"
"os/exec"
"path/filepath"
"strings"
)
// Runnable defines a command to run, along with its arguments,
// and whether or not failing to exit successfully matters.
// It also defines whether certains words must exist (or not exist)
// in its output, apart from existing successfully, to determine
// whether or not it was actually successful.
type Runnable struct {
Exec string
Args []string
Must bool
Keywords []string
Badwords []string
}
func (x Runnable) Run() error {
cmd := exec.Command(x.Exec, x.Args...)
out, err := cmd.CombinedOutput()
if !x.Must {
return nil
}
good := true
str := string(out)
for j := range x.Keywords {
if !strings.Contains(str, x.Keywords[j]) {
good = false
break
}
}
if good && 0 != len(x.Badwords) {
for j := range x.Badwords {
if "" != x.Badwords[j] && !strings.Contains(str, x.Badwords[j]) {
good = false
break
}
}
}
if nil != err {
var comment string
if len(x.Keywords) > 0 {
comment += "# output must match all of:\n"
comment += "# \t" + strings.Join(x.Keywords, "\n#\t") + "\n"
}
if len(x.Badwords) > 0 {
comment += "# output must not match any of:\n"
comment += "# \t" + strings.Join(x.Badwords, "\n#\t") + "\n"
}
return fmt.Errorf("Failed to run %s %s\n%s\n%s\n", x.Exec, strings.Join(x.Args, " "), str, comment)
}
return nil
}
func (x Runnable) String() string {
var must = "true"
if x.Must {
must = "exit"
}
return strings.TrimSpace(fmt.Sprintf(
"%s %s || %s\n",
x.Exec,
strings.Join(x.Args, " "),
must,
))
}
func getSrvs(dir string) ([]string, error) {
plists := []string{}
infos, err := ioutil.ReadDir(dir)
if nil != err {
return nil, err
}
for i := range infos {
x := infos[i]
fname := strings.ToLower(x.Name())
if strings.HasSuffix(fname, srvExt) {
plists = append(plists, x.Name())
}
}
return plists, nil
}
func getSystemSrvs() ([]string, error) {
return getSrvs(srvSysPath)
}
func getUserSrvs(home string) ([]string, error) {
dir := filepath.Join(home, srvUserPath)
return getSrvs(dir)
}
func filterMatchingSrvs(plists []string, name string) []string {
filtered := []string{}
for i := range plists {
pname := plists[i]
lname := strings.ToLower(pname)
n := len(lname)
if strings.HasSuffix(lname[:n-srvLen], strings.ToLower(name)) {
filtered = append(filtered, pname)
}
}
return filtered
}
func getMatchingSrvs(home string, name string) ([]string, []string, error) {
sysPlists, err := getSystemSrvs()
if nil != err {
return nil, nil, err
}
var userPlists []string
if "" != home {
userPlists, err = getUserSrvs(home)
if nil != err {
return nil, nil, err
}
}
return filterMatchingSrvs(sysPlists, name), filterMatchingSrvs(userPlists, name), nil
}
func getExactSrvMatch(srvs []string, name string) string {
for i := range srvs {
srv := srvs[i]
n := len(srv)
if srv[:n-srvLen] == strings.ToLower(name) {
return srv
}
}
return ""
}
func getOneSysSrv(sys []string, user []string, name string) (string, error) {
service := getExactSrvMatch(user, name)
if "" != service {
return service, nil
}
var errstr string
// system service was wanted
n := len(sys)
switch {
case 0 == n:
errstr += fmt.Sprintf("Didn't find user service matching %q\n", name)
if 0 != len(user) {
errstr += fmt.Sprintf("Did you intend to run a user service instead?\n\t%s\n", strings.Join(user, "\n\t"))
}
case n > 1:
errstr += fmt.Sprintf("Found more than one matching service:\n\t%s\n", strings.Join(sys, "\n\t"))
default:
service = filepath.Join(srvSysPath, sys[0])
}
if "" != errstr {
return "", fmt.Errorf(errstr)
}
return service, nil
}
func getOneUserSrv(home string, sys []string, user []string, name string) (string, error) {
service := getExactSrvMatch(user, name)
if "" != service {
return service, nil
}
var errstr string
// user service was wanted
n := len(user)
switch {
case 0 == n:
errstr += fmt.Sprintf("Didn't find user service matching %q\n", name)
if 0 != len(sys) {
errstr += fmt.Sprintf("Did you intend to run a system service instead?\n\t%s\n", strings.Join(sys, "\n\t"))
}
case n > 1:
errstr += fmt.Sprintf("Found more than one matching service:\n\t%s\n", strings.Join(user, "\n\t"))
default:
service = filepath.Join(home, srvUserPath, user[0]+srvExt)
}
if "" != errstr {
return "", fmt.Errorf(errstr)
}
return service, nil
}
func adjustPrivs(system bool, cmds []Runnable) []Runnable {
if !system || isPrivileged() {
return cmds
}
sudos := cmds
cmds = []Runnable{}
for i := range sudos {
exe := sudos[i]
exe.Args = append([]string{exe.Exec}, exe.Args...)
exe.Exec = "sudo"
cmds = append(cmds, exe)
}
return cmds
}

File diff suppressed because one or more lines are too long

View File

@ -126,15 +126,17 @@ func add() {
conf.Normalize(force)
//fmt.Printf("\n%#v\n\n", conf)
if conf.System && !manager.IsPrivileged() {
fmt.Fprintf(os.Stderr, "Warning: You may need to use 'sudo' to add %q as a privileged system service.\n", conf.Name)
}
err = manager.Install(conf)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
fmt.Fprintf(os.Stderr, "Use 'sudo' to add service as a privileged system service.\n")
fmt.Fprintf(os.Stderr, "Use '--user' to add service as an user service.\n")
}
fmt.Printf("Once started, logs will be found at:\n\t%s\n", conf.Logdir)
printLogMessage(conf)
fmt.Println()
}
func run() {

11
serviceman_darwin.go Normal file
View File

@ -0,0 +1,11 @@
package main
import (
"fmt"
"git.rootprojects.org/root/go-serviceman/service"
)
func printLogMessage(conf *service.Service) {
fmt.Printf("If all went well the logs should have been created at:\n\t%s\n", conf.Logdir)
}

26
serviceman_linux.go Normal file
View File

@ -0,0 +1,26 @@
package main
import (
"fmt"
"git.rootprojects.org/root/go-serviceman/manager"
"git.rootprojects.org/root/go-serviceman/service"
)
func printLogMessage(conf *service.Service) {
sudo := ""
unit := "--unit"
if conf.System {
if !manager.IsPrivileged() {
sudo = "sudo"
}
} else {
unit = "--user-unit"
}
fmt.Println("If all went well you should be able to see some goodies in the logs:")
fmt.Printf("\t%sjournalctl -xe %s %s.service\n", sudo, unit, conf.Name)
if !conf.System {
fmt.Println("\nIf that's not the case, see https://unix.stackexchange.com/a/486566/45554.")
fmt.Println("(you may need to run `systemctl restart systemd-journald`)")
}
}

11
serviceman_windows.go Normal file
View File

@ -0,0 +1,11 @@
package main
import (
"fmt"
"git.rootprojects.org/root/go-serviceman/service"
)
func printLogMessage(conf *service.Service) {
fmt.Printf("If all went well the logs should have been created at:\n\t%s\n", conf.Logdir)
}