From abec5b7e594d4757abbd46b97cb37bcb87ebdefe Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 2 Jul 2019 23:51:30 -0600 Subject: [PATCH] refactor and add service runner --- .ignore | 1 + go.mod | 5 +- go.sum | 9 ++ installer/install.go | 71 +------------- installer/install_darwin.go | 9 +- installer/install_linux.go | 5 +- installer/install_notwindows.go | 1 + installer/install_other.go | 6 +- installer/install_windows.go | 108 ++++++++++++++++----- runner/runner.go | 85 +++++++++++++++++ service/service.go | 141 ++++++++++++++++++++++++++++ serviceman.go | 160 +++++++++++++++++++------------- 12 files changed, 439 insertions(+), 162 deletions(-) create mode 100644 .ignore create mode 100644 runner/runner.go create mode 100644 service/service.go diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/.ignore @@ -0,0 +1 @@ +vendor diff --git a/go.mod b/go.mod index a182622..d618fec 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index b9a1259..cd1eee9 100644 --- a/go.sum +++ b/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= diff --git a/installer/install.go b/installer/install.go index 1e47eff..02d21a2 100644 --- a/installer/install.go +++ b/installer/install.go @@ -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) diff --git a/installer/install_darwin.go b/installer/install_darwin.go index d595d4b..5ff21bd 100644 --- a/installer/install_darwin.go +++ b/installer/install_darwin.go @@ -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 { diff --git a/installer/install_linux.go b/installer/install_linux.go index 3208796..3aa2f39 100644 --- a/installer/install_linux.go +++ b/installer/install_linux.go @@ -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 diff --git a/installer/install_notwindows.go b/installer/install_notwindows.go index 236e7cf..bf3a07e 100644 --- a/installer/install_notwindows.go +++ b/installer/install_notwindows.go @@ -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 { diff --git a/installer/install_other.go b/installer/install_other.go index f78efaf..3a88a5b 100644 --- a/installer/install_other.go +++ b/installer/install_other.go @@ -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 } diff --git a/installer/install_windows.go b/installer/install_windows.go index ee6b6fc..fcd9e3c 100644 --- a/installer/install_windows.go +++ b/installer/install_windows.go @@ -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 { diff --git a/runner/runner.go b/runner/runner.go new file mode 100644 index 0000000..fb86f7b --- /dev/null +++ b/runner/runner.go @@ -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 + } + } + } +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..dbecb11 --- /dev/null +++ b/service/service.go @@ -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 + } + } +} diff --git a/serviceman.go b/serviceman.go index 62d0a5b..56b589c 100644 --- a/serviceman.go +++ b/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,79 +121,83 @@ 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) + conf.Normalize(force) + + fmt.Printf("\n%#v\n\n", conf) + + err = installer.Install(conf) + if nil != err { + fmt.Fprintf(os.Stderr, "%s\n", err) + fmt.Fprintf(os.Stderr, "Use 'sudo' to install as a privileged system service.\n") + fmt.Fprintf(os.Stderr, "Use '--user' to install as an user service.\n") } - if "" == conf.Title { - conf.Title = conf.Name +} + +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) } - if "" == conf.ReverseDNS { - // technically should be something more like "com.example." + conf.Name, - // but whatever - conf.ReverseDNS = conf.Name + + b, err := ioutil.ReadFile(confpath) + if nil != err { + fmt.Fprintf(os.Stderr, "Couldn't read config file: %s\n", err) + os.Exit(400) } - 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 + 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) } - // 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) + m := map[string]interface{}{} + err = json.Unmarshal(b, &m) 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 - } - } + fmt.Fprintf(os.Stderr, "Couldn't JSON parse config file: %s\n", err) + os.Exit(400) + } - 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 - } + // default Restart to true + if _, ok := m["restart"]; !ok { + s.Restart = true } - fmt.Printf("\n%#v\n\n", conf) + if "" == s.Exec { + fmt.Fprintf(os.Stderr, "Missing exec\n") + os.Exit(400) + } - err = installer.Install(conf) + 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) - fmt.Fprintf(os.Stderr, "Use 'sudo' to install as a privileged system service.\n") - fmt.Fprintf(os.Stderr, "Use '--user' to install as an user service.\n") + os.Exit(500) } }