refactor and add service runner
This commit is contained in:
parent
8527c632f8
commit
abec5b7e59
5
go.mod
5
go.mod
|
@ -5,6 +5,7 @@ go 1.12
|
||||||
require (
|
require (
|
||||||
git.rootprojects.org/root/go-gitver v1.1.2
|
git.rootprojects.org/root/go-gitver v1.1.2
|
||||||
github.com/UnnoTed/fileb0x v1.1.3
|
github.com/UnnoTed/fileb0x v1.1.3
|
||||||
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
|
||||||
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a
|
||||||
|
golang.org/x/tools v0.0.0-20190702201734-44aeb8b7c377 // indirect
|
||||||
)
|
)
|
||||||
|
|
9
go.sum
9
go.sum
|
@ -42,10 +42,19 @@ github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QI
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
||||||
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ=
|
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ=
|
||||||
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f h1:QM2QVxvDoW9PFSPp/zy9FgxJLfaWTZlS61KEPtBwacM=
|
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f h1:QM2QVxvDoW9PFSPp/zy9FgxJLfaWTZlS61KEPtBwacM=
|
||||||
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8 h1:R91KX5nmbbvEd7w370cbVzKC+EzCTGqZq63Zad5IcLM=
|
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8 h1:R91KX5nmbbvEd7w370cbVzKC+EzCTGqZq63Zad5IcLM=
|
||||||
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190702201734-44aeb8b7c377 h1:P/0pu7r+pn3Fkv7pyRpb7tBawImpURm2mTIbR6MadCc=
|
||||||
|
golang.org/x/tools v0.0.0-20190702201734-44aeb8b7c377/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
|
|
|
@ -7,75 +7,13 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
|
||||||
|
|
||||||
// Config should describe the service well-enough for it to
|
"git.rootprojects.org/root/go-serviceman/service"
|
||||||
// run on Mac, Linux, and Windows.
|
)
|
||||||
//
|
|
||||||
// &Config{
|
|
||||||
// // A human-friendy name
|
|
||||||
// Title: "Foobar App",
|
|
||||||
// // A computer-friendly name
|
|
||||||
// Name: "foobar-app",
|
|
||||||
// // A name for OS X plist
|
|
||||||
// ReverseDNS: "com.example.foobar-app",
|
|
||||||
// // A human-friendly description
|
|
||||||
// Desc: "Foobar App",
|
|
||||||
// // The app /service homepage
|
|
||||||
// URL: "https://example.com/foobar-app/",
|
|
||||||
// // The full path of the interpreter, if any (ruby, python, node, etc)
|
|
||||||
// Interpreter: "/opt/node/bin/node",
|
|
||||||
// // The name of the executable (or script)
|
|
||||||
// Exec: "foobar-app.js",
|
|
||||||
// // An array of arguments
|
|
||||||
// Argv: []string{"-c", "/path/to/config.json"},
|
|
||||||
// // A map of Environment variables that should be set
|
|
||||||
// Envs: map[string]string{
|
|
||||||
// PORT: "8080",
|
|
||||||
// ENV: "development",
|
|
||||||
// },
|
|
||||||
// // The user (Linux & Mac only).
|
|
||||||
// // This does not apply to userspace services.
|
|
||||||
// // There may be special considerations
|
|
||||||
// User: "www-data",
|
|
||||||
// // If different from User
|
|
||||||
// Group: "",
|
|
||||||
// // Whether to install as a system or user service
|
|
||||||
// System: false,
|
|
||||||
// // Whether or not the service may need privileged ports
|
|
||||||
// PrivilegedPorts: false,
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Note that some fields are exported for templating,
|
|
||||||
// but not intended to be set by you.
|
|
||||||
// These are documented as omitted from JSON.
|
|
||||||
// Try to stick to what's outlined above.
|
|
||||||
type Config struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Desc string `json:"desc"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
ReverseDNS string `json:"reverse_dns"` // i.e. com.example.foo-app
|
|
||||||
Interpreter string `json:"interpreter"` // i.e. node, python
|
|
||||||
Exec string `json:"exec"`
|
|
||||||
Argv []string `json:"argv"`
|
|
||||||
Workdir string `json:"workdir"`
|
|
||||||
Envs map[string]string `json:"envs"`
|
|
||||||
User string `json:"user"`
|
|
||||||
Group string `json:"group"`
|
|
||||||
home string `json:"-"`
|
|
||||||
Local string `json:"-"`
|
|
||||||
Logdir string `json:"-"`
|
|
||||||
System bool `json:"system"`
|
|
||||||
Restart bool `json:"restart"`
|
|
||||||
Production bool `json:"production"`
|
|
||||||
PrivilegedPorts bool `json:"privileged_ports"`
|
|
||||||
MultiuserProtection bool `json:"multiuser_protection"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install will do a best-effort attempt to install a start-on-startup
|
// Install will do a best-effort attempt to install a start-on-startup
|
||||||
// user or system service via systemd, launchd, or reg.exe
|
// user or system service via systemd, launchd, or reg.exe
|
||||||
func Install(c *Config) error {
|
func Install(c *service.Service) error {
|
||||||
if "" == c.Exec {
|
if "" == c.Exec {
|
||||||
c.Exec = c.Name
|
c.Exec = c.Name
|
||||||
}
|
}
|
||||||
|
@ -87,7 +25,7 @@ func Install(c *Config) error {
|
||||||
os.Exit(4)
|
os.Exit(4)
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
c.home = home
|
c.Home = home
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +50,7 @@ func IsPrivileged() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func WhereIs(exec string) (string, error) {
|
func WhereIs(exec string) (string, error) {
|
||||||
|
// TODO use exec.LookPath instead
|
||||||
exec = filepath.ToSlash(exec)
|
exec = filepath.ToSlash(exec)
|
||||||
if strings.Contains(exec, "/") {
|
if strings.Contains(exec, "/") {
|
||||||
// it's a path (so we don't allow filenames with slashes)
|
// it's a path (so we don't allow filenames with slashes)
|
||||||
|
|
|
@ -10,9 +10,10 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"git.rootprojects.org/root/go-serviceman/installer/static"
|
"git.rootprojects.org/root/go-serviceman/installer/static"
|
||||||
|
"git.rootprojects.org/root/go-serviceman/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func install(c *Config) error {
|
func install(c *service.Service) error {
|
||||||
// Darwin-specific config options
|
// Darwin-specific config options
|
||||||
if c.PrivilegedPorts {
|
if c.PrivilegedPorts {
|
||||||
if !c.System {
|
if !c.System {
|
||||||
|
@ -21,7 +22,7 @@ func install(c *Config) error {
|
||||||
}
|
}
|
||||||
plistDir := "/Library/LaunchDaemons/"
|
plistDir := "/Library/LaunchDaemons/"
|
||||||
if !c.System {
|
if !c.System {
|
||||||
plistDir = filepath.Join(c.home, "Library/LaunchAgents")
|
plistDir = filepath.Join(c.Home, "Library/LaunchAgents")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check paths first
|
// Check paths first
|
||||||
|
@ -57,8 +58,8 @@ func install(c *Config) error {
|
||||||
}
|
}
|
||||||
fmt.Printf("Installed. To start '%s' run the following:\n", c.Name)
|
fmt.Printf("Installed. To start '%s' run the following:\n", c.Name)
|
||||||
// TODO template config file
|
// TODO template config file
|
||||||
if "" != c.home {
|
if "" != c.Home {
|
||||||
plistPath = strings.Replace(plistPath, c.home, "~", 1)
|
plistPath = strings.Replace(plistPath, c.Home, "~", 1)
|
||||||
}
|
}
|
||||||
sudo := ""
|
sudo := ""
|
||||||
if c.System {
|
if c.System {
|
||||||
|
|
|
@ -9,9 +9,10 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"git.rootprojects.org/root/go-serviceman/installer/static"
|
"git.rootprojects.org/root/go-serviceman/installer/static"
|
||||||
|
"git.rootprojects.org/root/go-serviceman/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func install(c *Config) error {
|
func install(c *service.Service) error {
|
||||||
// Linux-specific config options
|
// Linux-specific config options
|
||||||
if c.System {
|
if c.System {
|
||||||
if "" == c.User {
|
if "" == c.User {
|
||||||
|
@ -30,7 +31,7 @@ func install(c *Config) error {
|
||||||
// * ~/.local/share/systemd/user/watchdog.service
|
// * ~/.local/share/systemd/user/watchdog.service
|
||||||
// * ~/.config/systemd/user/watchdog.service
|
// * ~/.config/systemd/user/watchdog.service
|
||||||
// https://wiki.archlinux.org/index.php/Systemd/User
|
// https://wiki.archlinux.org/index.php/Systemd/User
|
||||||
serviceDir = filepath.Join(c.home, ".local/share/systemd/user")
|
serviceDir = filepath.Join(c.Home, ".local/share/systemd/user")
|
||||||
err := os.MkdirAll(filepath.Dir(serviceDir), 0755)
|
err := os.MkdirAll(filepath.Dir(serviceDir), 0755)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func whereIs(exe string) (string, error) {
|
func whereIs(exe string) (string, error) {
|
||||||
|
// TODO use exec.LookPath instead
|
||||||
cmd := exec.Command("command", "-v", exe)
|
cmd := exec.Command("command", "-v", exe)
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
if nil != err {
|
if nil != err {
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
package installer
|
package installer
|
||||||
|
|
||||||
func install(c *Config) error {
|
import (
|
||||||
|
"git.rootprojects.org/root/go-serviceman/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(c *service.Service) error {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
package installer
|
package installer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/go-serviceman/service"
|
||||||
|
|
||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO nab some goodness from https://github.com/takama/daemon
|
||||||
|
|
||||||
// TODO system service requires elevated privileges
|
// TODO system service requires elevated privileges
|
||||||
// See https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
|
// See https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
|
||||||
func install(c *Config) error {
|
func install(c *service.Service) error {
|
||||||
//token := windows.Token(0)
|
|
||||||
/*
|
/*
|
||||||
// LEAVE THIS DOCUMENTATION HERE
|
// LEAVE THIS DOCUMENTATION HERE
|
||||||
reg.exe
|
reg.exe
|
||||||
|
@ -51,30 +57,39 @@ func install(c *Config) error {
|
||||||
}
|
}
|
||||||
defer k.Close()
|
defer k.Close()
|
||||||
|
|
||||||
setArgs := ""
|
args, err := installServiceman(c)
|
||||||
args := c.Argv
|
if nil != err {
|
||||||
exec := filepath.Join(c.home, ".local", "opt", c.Name, c.Exec)
|
return err
|
||||||
bin := c.Interpreter
|
|
||||||
if "" != bin {
|
|
||||||
// If this is something like node or python,
|
|
||||||
// the interpeter must be called as "the main thing"
|
|
||||||
// and "the app" must be an argument
|
|
||||||
args = append([]string{exec}, args...)
|
|
||||||
} else {
|
|
||||||
// Otherwise, if "the app" is a true binary,
|
|
||||||
// it can be "the main thing"
|
|
||||||
bin = exec
|
|
||||||
}
|
|
||||||
if 0 != len(args) {
|
|
||||||
// On Windows the /c acts kinda like -- does on *nix,
|
|
||||||
// at least for commands in the registry that have arguments
|
|
||||||
setArgs = ` /c `
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The final string ends up looking something like one of these:
|
/*
|
||||||
// "C:\Users\aj\.local\opt\appname\appname.js /c -p 8080"
|
setArgs := ""
|
||||||
// "C:\Program Files (x64)\nodejs\node.exe /c C:\Users\aj\.local\opt\appname\appname.js -p 8080"
|
args := c.Argv
|
||||||
regSZ := bin + setArgs + strings.Join(c.Argv, " ")
|
exec := c.Exec
|
||||||
|
bin := c.Interpreter
|
||||||
|
if "" != bin {
|
||||||
|
// If this is something like node or python,
|
||||||
|
// the interpeter must be called as "the main thing"
|
||||||
|
// and "the app" must be an argument
|
||||||
|
args = append([]string{exec}, args...)
|
||||||
|
} else {
|
||||||
|
// Otherwise, if "the app" is a true binary,
|
||||||
|
// it can be "the main thing"
|
||||||
|
bin = exec
|
||||||
|
}
|
||||||
|
if 0 != len(args) {
|
||||||
|
// On Windows the /c acts kinda like -- does on *nix,
|
||||||
|
// at least for commands in the registry that have arguments
|
||||||
|
setArgs = ` /c `
|
||||||
|
}
|
||||||
|
|
||||||
|
// The final string ends up looking something like one of these:
|
||||||
|
// "C:\Users\aj\.local\opt\appname\appname.js /c -p 8080"
|
||||||
|
// "C:\Program Files (x64)\nodejs\node.exe /c C:\Users\aj\.local\opt\appname\appname.js -p 8080"
|
||||||
|
regSZ := bin + setArgs + strings.Join(c.Argv, " ")
|
||||||
|
*/
|
||||||
|
|
||||||
|
regSZ := fmt.Sprintf("%s /c %s", args[0], strings.Join(args[1:], " "))
|
||||||
if len(regSZ) > 260 {
|
if len(regSZ) > 260 {
|
||||||
return fmt.Errorf("data value is too long for registry entry")
|
return fmt.Errorf("data value is too long for registry entry")
|
||||||
}
|
}
|
||||||
|
@ -85,7 +100,52 @@ func install(c *Config) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copies self to install path and returns config path
|
||||||
|
func installServiceman(c *service.Service) ([]string, error) {
|
||||||
|
// TODO check version and upgrade or dismiss
|
||||||
|
self := os.Args[0]
|
||||||
|
smdir := `\opt\serviceman`
|
||||||
|
// TODO support service level services (which probably wouldn't need serviceman)
|
||||||
|
smdir = filepath.Join(c.Home, ".local", smdir)
|
||||||
|
// for now we'll scope the runner to the name of the application
|
||||||
|
smbin := filepath.Join(smdir, `bin\serviceman.%s`, c.Name)
|
||||||
|
|
||||||
|
if smbin != self {
|
||||||
|
err := os.MkdirAll(filepath.Dir(smbin))
|
||||||
|
if nil != err {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
bin, err := ioutil.ReadFile(self)
|
||||||
|
if nil != err {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err := ioutil.WriteFile(smbin, bin, 0755)
|
||||||
|
if nil != err {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(c)
|
||||||
|
if nil != err {
|
||||||
|
// this should be impossible, so we'll just panic
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
confpath := filepath.Join(smpath, `etc`, conf.Name+`.json`)
|
||||||
|
err := ioutil.WriteFile(confpath, b, 0640)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{
|
||||||
|
smbin,
|
||||||
|
"run",
|
||||||
|
"--config",
|
||||||
|
confpath,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func whereIs(exe string) (string, error) {
|
func whereIs(exe string) (string, error) {
|
||||||
|
// TODO use exec.LookPath instead
|
||||||
cmd := exec.Command("where.exe", exe)
|
cmd := exec.Command("where.exe", exe)
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
if nil != err {
|
if nil != err {
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/go-serviceman/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Notes on spawning a child process
|
||||||
|
// https://groups.google.com/forum/#!topic/golang-nuts/shST-SDqIp4
|
||||||
|
|
||||||
|
func Run(conf *service.Service) {
|
||||||
|
originalBackoff := 1 * time.Second
|
||||||
|
maxBackoff := 1 * time.Minute
|
||||||
|
threshold := 5 * time.Second
|
||||||
|
|
||||||
|
backoff := originalBackoff
|
||||||
|
failures := 0
|
||||||
|
logfile := filepath.Join(conf.Logdir, conf.Name+".log")
|
||||||
|
|
||||||
|
binpath := conf.Exec
|
||||||
|
args := []string{}
|
||||||
|
if "" != conf.Interpreter {
|
||||||
|
binpath = conf.Interpreter
|
||||||
|
args = append(args, conf.Exec)
|
||||||
|
}
|
||||||
|
args = append(args, conf.Argv...)
|
||||||
|
|
||||||
|
for {
|
||||||
|
// setup the log
|
||||||
|
lf, err := os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "Could not open log file %q\n", logfile)
|
||||||
|
lf = os.Stderr
|
||||||
|
} else {
|
||||||
|
defer lf.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
cmd := exec.Command(binpath, args...)
|
||||||
|
cmd.Stdin = nil
|
||||||
|
cmd.Stdout = lf
|
||||||
|
cmd.Stderr = lf
|
||||||
|
if "" != conf.Workdir {
|
||||||
|
cmd.Dir = conf.Workdir
|
||||||
|
}
|
||||||
|
err = cmd.Start()
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(lf, "Could not start %q process: %s\n", conf.Name, err)
|
||||||
|
} else {
|
||||||
|
err = cmd.Wait()
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(lf, "Process %q failed with error: %s\n", conf.Name, err)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(lf, "Process %q exited cleanly\n", conf.Name)
|
||||||
|
fmt.Printf("Process %q exited cleanly\n", conf.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this is a oneshot... so it is
|
||||||
|
if !conf.Restart {
|
||||||
|
fmt.Printf("Not restarting %q because `restart` set to `false`\n", conf.Name)
|
||||||
|
fmt.Fprintf(lf, "Not restarting %q because `restart` set to `false`\n", conf.Name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
end := time.Now()
|
||||||
|
if end.Sub(start) > threshold {
|
||||||
|
backoff = originalBackoff
|
||||||
|
failures = 0
|
||||||
|
} else {
|
||||||
|
failures += 1
|
||||||
|
fmt.Fprintf(lf, "Waiting %s to restart %q (%d consequtive immediate exits)\n", backoff, conf.Name, failures)
|
||||||
|
time.Sleep(backoff)
|
||||||
|
backoff *= 2
|
||||||
|
if backoff > maxBackoff {
|
||||||
|
backoff = maxBackoff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service should describe the service well-enough for it to
|
||||||
|
// run on Mac, Linux, and Windows.
|
||||||
|
//
|
||||||
|
// &Service{
|
||||||
|
// // A human-friendy name
|
||||||
|
// Title: "Foobar App",
|
||||||
|
// // A computer-friendly name
|
||||||
|
// Name: "foobar-app",
|
||||||
|
// // A name for OS X plist
|
||||||
|
// ReverseDNS: "com.example.foobar-app",
|
||||||
|
// // A human-friendly description
|
||||||
|
// Desc: "Foobar App",
|
||||||
|
// // The app /service homepage
|
||||||
|
// URL: "https://example.com/foobar-app/",
|
||||||
|
// // The full path of the interpreter, if any (ruby, python, node, etc)
|
||||||
|
// Interpreter: "/opt/node/bin/node",
|
||||||
|
// // The name of the executable (or script)
|
||||||
|
// Exec: "foobar-app.js",
|
||||||
|
// // An array of arguments
|
||||||
|
// Argv: []string{"-c", "/path/to/config.json"},
|
||||||
|
// // A map of Environment variables that should be set
|
||||||
|
// Envs: map[string]string{
|
||||||
|
// PORT: "8080",
|
||||||
|
// ENV: "development",
|
||||||
|
// },
|
||||||
|
// // The user (Linux & Mac only).
|
||||||
|
// // This does not apply to userspace services.
|
||||||
|
// // There may be special considerations
|
||||||
|
// User: "www-data",
|
||||||
|
// // If different from User
|
||||||
|
// Group: "",
|
||||||
|
// // Whether to install as a system or user service
|
||||||
|
// System: false,
|
||||||
|
// // Whether or not the service may need privileged ports
|
||||||
|
// PrivilegedPorts: false,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Note that some fields are exported for templating,
|
||||||
|
// but not intended to be set by you.
|
||||||
|
// These are documented as omitted from JSON.
|
||||||
|
// Try to stick to what's outlined above.
|
||||||
|
type Service struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
ReverseDNS string `json:"reverse_dns"` // i.e. com.example.foo-app
|
||||||
|
Interpreter string `json:"interpreter"` // i.e. node, python
|
||||||
|
Exec string `json:"exec"`
|
||||||
|
Argv []string `json:"argv"`
|
||||||
|
Workdir string `json:"workdir"`
|
||||||
|
Envs map[string]string `json:"envs"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Group string `json:"group"`
|
||||||
|
Home string `json:"-"`
|
||||||
|
Local string `json:"-"`
|
||||||
|
Logdir string `json:"logdir"`
|
||||||
|
System bool `json:"system"`
|
||||||
|
Restart bool `json:"restart"`
|
||||||
|
Production bool `json:"production"`
|
||||||
|
PrivilegedPorts bool `json:"privileged_ports"`
|
||||||
|
MultiuserProtection bool `json:"multiuser_protection"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Normalize(force bool) {
|
||||||
|
if "" == s.Name {
|
||||||
|
ext := filepath.Ext(s.Exec)
|
||||||
|
base := filepath.Base(s.Exec[:len(s.Exec)-len(ext)])
|
||||||
|
s.Name = strings.ToLower(base)
|
||||||
|
}
|
||||||
|
if "" == s.Title {
|
||||||
|
s.Title = s.Name
|
||||||
|
}
|
||||||
|
if "" == s.ReverseDNS {
|
||||||
|
// technically should be something more like "com.example." + s.Name,
|
||||||
|
// but whatever
|
||||||
|
s.ReverseDNS = s.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.System {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
|
||||||
|
os.Exit(4)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Local = filepath.Join(home, ".local")
|
||||||
|
s.Logdir = filepath.Join(home, ".local", "share", s.Name, "var", "log")
|
||||||
|
} else {
|
||||||
|
s.Logdir = "/var/log/" + s.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if Exec exists
|
||||||
|
// /whatever => must exist exactly
|
||||||
|
// ./whatever => must exist in current or WorkDir(TODO)
|
||||||
|
// whatever => may also exist in {{ .Local }}/opt/{{ .Name }}/{{ .Exec }}
|
||||||
|
_, err := os.Stat(s.Exec)
|
||||||
|
if nil != err {
|
||||||
|
bad := true
|
||||||
|
if !strings.Contains(filepath.ToSlash(s.Exec), "/") {
|
||||||
|
optpath := filepath.Join(s.Local, "/opt", s.Name, s.Exec)
|
||||||
|
_, err := os.Stat(optpath)
|
||||||
|
if nil == err {
|
||||||
|
bad = false
|
||||||
|
fmt.Fprintf(os.Stderr, "Using '%s' for '%s'\n", optpath, s.Exec)
|
||||||
|
s.Exec = optpath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bad {
|
||||||
|
// TODO look for it in WorkDir?
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: '%s' could not be found.\n", s.Exec)
|
||||||
|
if !force {
|
||||||
|
os.Exit(5)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
execpath, err := filepath.Abs(s.Exec)
|
||||||
|
if nil == err {
|
||||||
|
s.Exec = execpath
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Using '%s' anyway.\n", s.Exec)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
execpath, err := filepath.Abs(s.Exec)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
|
||||||
|
os.Exit(4)
|
||||||
|
} else {
|
||||||
|
s.Exec = execpath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
170
serviceman.go
170
serviceman.go
|
@ -3,22 +3,52 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.rootprojects.org/root/go-serviceman/installer"
|
"git.rootprojects.org/root/go-serviceman/installer"
|
||||||
|
"git.rootprojects.org/root/go-serviceman/runner"
|
||||||
|
"git.rootprojects.org/root/go-serviceman/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var GitRev = "000000000"
|
var GitRev = "000000000"
|
||||||
var GitVersion = "v0.0.0"
|
var GitVersion = "v0.0.0"
|
||||||
var GitTimestamp = time.Now().Format(time.RFC3339)
|
var GitTimestamp = time.Now().Format(time.RFC3339)
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Println("Usage: serviceman install ./foo-app -- --foo-arg")
|
||||||
|
fmt.Println("Usage: serviceman run --config ./foo-app.json")
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
conf := &installer.Config{
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Too few arguments: %s\n", strings.Join(os.Args, " "))
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
top := os.Args[1]
|
||||||
|
os.Args = append(os.Args[:1], os.Args[2:]...)
|
||||||
|
switch top {
|
||||||
|
case "install":
|
||||||
|
install()
|
||||||
|
case "run":
|
||||||
|
run()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown argument %s\n", top)
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func install() {
|
||||||
|
conf := &service.Service{
|
||||||
Restart: true,
|
Restart: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,72 +121,7 @@ func main() {
|
||||||
conf.Argv = append(args[1:], conf.Argv...)
|
conf.Argv = append(args[1:], conf.Argv...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if "" == conf.Name {
|
conf.Normalize(force)
|
||||||
ext := filepath.Ext(conf.Exec)
|
|
||||||
base := filepath.Base(conf.Exec[:len(conf.Exec)-len(ext)])
|
|
||||||
conf.Name = strings.ToLower(base)
|
|
||||||
}
|
|
||||||
if "" == conf.Title {
|
|
||||||
conf.Title = conf.Name
|
|
||||||
}
|
|
||||||
if "" == conf.ReverseDNS {
|
|
||||||
// technically should be something more like "com.example." + conf.Name,
|
|
||||||
// but whatever
|
|
||||||
conf.ReverseDNS = conf.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
if !conf.System {
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if nil != err {
|
|
||||||
fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
|
|
||||||
os.Exit(4)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conf.Local = filepath.Join(home, ".local")
|
|
||||||
conf.Logdir = filepath.Join(home, ".local", "share", conf.Name, "var", "log")
|
|
||||||
} else {
|
|
||||||
conf.Logdir = "/var/log/" + conf.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check to see if Exec exists
|
|
||||||
// /whatever => must exist exactly
|
|
||||||
// ./whatever => must exist in current or WorkDir(TODO)
|
|
||||||
// whatever => may also exist in {{ .Local }}/opt/{{ .Name }}/{{ .Exec }}
|
|
||||||
_, err = os.Stat(conf.Exec)
|
|
||||||
if nil != err {
|
|
||||||
bad := true
|
|
||||||
if !strings.Contains(filepath.ToSlash(conf.Exec), "/") {
|
|
||||||
optpath := filepath.Join(conf.Local, "/opt", conf.Name, conf.Exec)
|
|
||||||
_, err := os.Stat(optpath)
|
|
||||||
if nil == err {
|
|
||||||
bad = false
|
|
||||||
fmt.Fprintf(os.Stderr, "Using '%s' for '%s'\n", optpath, conf.Exec)
|
|
||||||
conf.Exec = optpath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bad {
|
|
||||||
// TODO look for it in WorkDir?
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: '%s' could not be found.\n", conf.Exec)
|
|
||||||
if !force {
|
|
||||||
os.Exit(5)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
execpath, err := filepath.Abs(conf.Exec)
|
|
||||||
if nil == err {
|
|
||||||
conf.Exec = execpath
|
|
||||||
}
|
|
||||||
fmt.Fprintf(os.Stderr, "Using '%s' anyway.\n", conf.Exec)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
execpath, err := filepath.Abs(conf.Exec)
|
|
||||||
if nil != err {
|
|
||||||
fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
|
|
||||||
os.Exit(4)
|
|
||||||
} else {
|
|
||||||
conf.Exec = execpath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\n%#v\n\n", conf)
|
fmt.Printf("\n%#v\n\n", conf)
|
||||||
|
|
||||||
|
@ -167,3 +132,72 @@ func main() {
|
||||||
fmt.Fprintf(os.Stderr, "Use '--user' to install as an user service.\n")
|
fmt.Fprintf(os.Stderr, "Use '--user' to install as an user service.\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
var confpath string
|
||||||
|
var daemonize bool
|
||||||
|
flag.StringVar(&confpath, "config", "", "path to a config file to run")
|
||||||
|
flag.BoolVar(&daemonize, "daemon", false, "spawn a child process that lives in the background, and exit")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if "" == confpath {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s", strings.Join(flag.Args(), " "))
|
||||||
|
fmt.Fprintf(os.Stderr, "--config /path/to/config.json is required\n")
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(confpath)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "Couldn't read config file: %s\n", err)
|
||||||
|
os.Exit(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &service.Service{}
|
||||||
|
err = json.Unmarshal(b, s)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "Couldn't JSON parse config file: %s\n", err)
|
||||||
|
os.Exit(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := map[string]interface{}{}
|
||||||
|
err = json.Unmarshal(b, &m)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "Couldn't JSON parse config file: %s\n", err)
|
||||||
|
os.Exit(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default Restart to true
|
||||||
|
if _, ok := m["restart"]; !ok {
|
||||||
|
s.Restart = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" == s.Exec {
|
||||||
|
fmt.Fprintf(os.Stderr, "Missing exec\n")
|
||||||
|
os.Exit(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Normalize(false)
|
||||||
|
fmt.Fprintf(os.Stdout, "Logdir: %s\n", s.Logdir)
|
||||||
|
if !daemonize {
|
||||||
|
fmt.Fprintf(os.Stdout, "Running %s %s %s\n", s.Interpreter, s.Exec, strings.Join(s.Argv, " "))
|
||||||
|
runner.Run(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(os.Args[0], "run", "--config", confpath)
|
||||||
|
// for debugging
|
||||||
|
/*
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if nil != err {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(out))
|
||||||
|
*/
|
||||||
|
|
||||||
|
err = cmd.Start()
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue