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 (
|
||||
git.rootprojects.org/root/go-gitver v1.1.2
|
||||
github.com/UnnoTed/fileb0x v1.1.3
|
||||
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f
|
||||
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
|
||||
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=
|
||||
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-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/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/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
|
|
|
@ -7,75 +7,13 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config should describe the service well-enough for it to
|
||||
// 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"`
|
||||
}
|
||||
"git.rootprojects.org/root/go-serviceman/service"
|
||||
)
|
||||
|
||||
// Install will do a best-effort attempt to install a start-on-startup
|
||||
// user or system service via systemd, launchd, or reg.exe
|
||||
func Install(c *Config) error {
|
||||
func Install(c *service.Service) error {
|
||||
if "" == c.Exec {
|
||||
c.Exec = c.Name
|
||||
}
|
||||
|
@ -87,7 +25,7 @@ func Install(c *Config) error {
|
|||
os.Exit(4)
|
||||
return err
|
||||
} else {
|
||||
c.home = home
|
||||
c.Home = home
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,6 +50,7 @@ func IsPrivileged() bool {
|
|||
}
|
||||
|
||||
func WhereIs(exec string) (string, error) {
|
||||
// TODO use exec.LookPath instead
|
||||
exec = filepath.ToSlash(exec)
|
||||
if strings.Contains(exec, "/") {
|
||||
// it's a path (so we don't allow filenames with slashes)
|
||||
|
|
|
@ -10,9 +10,10 @@ import (
|
|||
"text/template"
|
||||
|
||||
"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
|
||||
if c.PrivilegedPorts {
|
||||
if !c.System {
|
||||
|
@ -21,7 +22,7 @@ func install(c *Config) error {
|
|||
}
|
||||
plistDir := "/Library/LaunchDaemons/"
|
||||
if !c.System {
|
||||
plistDir = filepath.Join(c.home, "Library/LaunchAgents")
|
||||
plistDir = filepath.Join(c.Home, "Library/LaunchAgents")
|
||||
}
|
||||
|
||||
// Check paths first
|
||||
|
@ -57,8 +58,8 @@ func install(c *Config) error {
|
|||
}
|
||||
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)
|
||||
if "" != c.Home {
|
||||
plistPath = strings.Replace(plistPath, c.Home, "~", 1)
|
||||
}
|
||||
sudo := ""
|
||||
if c.System {
|
||||
|
|
|
@ -9,9 +9,10 @@ import (
|
|||
"text/template"
|
||||
|
||||
"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
|
||||
if c.System {
|
||||
if "" == c.User {
|
||||
|
@ -30,7 +31,7 @@ func install(c *Config) error {
|
|||
// * ~/.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")
|
||||
serviceDir = filepath.Join(c.Home, ".local/share/systemd/user")
|
||||
err := os.MkdirAll(filepath.Dir(serviceDir), 0755)
|
||||
if nil != err {
|
||||
return err
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func whereIs(exe string) (string, error) {
|
||||
// TODO use exec.LookPath instead
|
||||
cmd := exec.Command("command", "-v", exe)
|
||||
out, err := cmd.Output()
|
||||
if nil != err {
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
package installer
|
||||
|
||||
func install(c *Config) error {
|
||||
import (
|
||||
"git.rootprojects.org/root/go-serviceman/service"
|
||||
)
|
||||
|
||||
func install(c *service.Service) error {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -1,19 +1,25 @@
|
|||
package installer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.rootprojects.org/root/go-serviceman/service"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
// TODO nab some goodness from https://github.com/takama/daemon
|
||||
|
||||
// TODO system service requires elevated privileges
|
||||
// See https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
|
||||
func install(c *Config) error {
|
||||
//token := windows.Token(0)
|
||||
func install(c *service.Service) error {
|
||||
/*
|
||||
// LEAVE THIS DOCUMENTATION HERE
|
||||
reg.exe
|
||||
|
@ -51,30 +57,39 @@ func install(c *Config) error {
|
|||
}
|
||||
defer k.Close()
|
||||
|
||||
setArgs := ""
|
||||
args := c.Argv
|
||||
exec := filepath.Join(c.home, ".local", "opt", c.Name, 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 `
|
||||
args, err := installServiceman(c)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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, " ")
|
||||
/*
|
||||
setArgs := ""
|
||||
args := 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 {
|
||||
return fmt.Errorf("data value is too long for registry entry")
|
||||
}
|
||||
|
@ -85,7 +100,52 @@ func install(c *Config) error {
|
|||
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) {
|
||||
// TODO use exec.LookPath instead
|
||||
cmd := exec.Command("where.exe", exe)
|
||||
out, err := cmd.Output()
|
||||
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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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 GitVersion = "v0.0.0"
|
||||
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() {
|
||||
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,
|
||||
}
|
||||
|
||||
|
@ -91,72 +121,7 @@ func main() {
|
|||
conf.Argv = append(args[1:], conf.Argv...)
|
||||
}
|
||||
|
||||
if "" == conf.Name {
|
||||
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
|
||||
}
|
||||
}
|
||||
conf.Normalize(force)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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