refactor and add service runner

This commit is contained in:
AJ ONeal 2019-07-02 23:51:30 -06:00
parent 8527c632f8
commit abec5b7e59
12 changed files with 444 additions and 167 deletions

1
.ignore Normal file
View File

@ -0,0 +1 @@
vendor

5
go.mod
View File

@ -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
View File

@ -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=

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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
} }

View File

@ -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 {

85
runner/runner.go Normal file
View File

@ -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
}
}
}
}

141
service/service.go Normal file
View File

@ -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
}
}
}

View File

@ -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)
}
}