Browse Source

refactor and add service runner

tags/v0.2.0
AJ ONeal 6 months ago
parent
commit
abec5b7e59

+ 1
- 0
.ignore View File

@@ -0,0 +1 @@
1
+vendor

+ 3
- 2
go.mod View File

@@ -5,6 +5,7 @@ go 1.12
5 5
 require (
6 6
 	git.rootprojects.org/root/go-gitver v1.1.2
7 7
 	github.com/UnnoTed/fileb0x v1.1.3
8
-	golang.org/x/net v0.0.0-20180921000356-2f5d2388922f
9
-	golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8
8
+	golang.org/x/net v0.0.0-20190620200207-3b0461eec859
9
+	golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a
10
+	golang.org/x/tools v0.0.0-20190702201734-44aeb8b7c377 // indirect
10 11
 )

+ 9
- 0
go.sum View File

@@ -42,10 +42,19 @@ github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QI
42 42
 github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
43 43
 golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ=
44 44
 golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
45
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
45 46
 golang.org/x/net v0.0.0-20180921000356-2f5d2388922f h1:QM2QVxvDoW9PFSPp/zy9FgxJLfaWTZlS61KEPtBwacM=
46 47
 golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
48
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
49
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
50
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
47 51
 golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8 h1:R91KX5nmbbvEd7w370cbVzKC+EzCTGqZq63Zad5IcLM=
48 52
 golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
53
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
54
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
55
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
56
+golang.org/x/tools v0.0.0-20190702201734-44aeb8b7c377 h1:P/0pu7r+pn3Fkv7pyRpb7tBawImpURm2mTIbR6MadCc=
57
+golang.org/x/tools v0.0.0-20190702201734-44aeb8b7c377/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
49 58
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
50 59
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
51 60
 gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=

+ 5
- 66
installer/install.go View File

@@ -7,75 +7,13 @@ import (
7 7
 	"os"
8 8
 	"path/filepath"
9 9
 	"strings"
10
-)
11 10
 
12
-// Config should describe the service well-enough for it to
13
-// run on Mac, Linux, and Windows.
14
-//
15
-// 	&Config{
16
-// 		// A human-friendy name
17
-// 		Title: "Foobar App",
18
-// 		// A computer-friendly name
19
-// 		Name: "foobar-app",
20
-// 		// A name for OS X plist
21
-// 		ReverseDNS: "com.example.foobar-app",
22
-// 		// A human-friendly description
23
-// 		Desc: "Foobar App",
24
-// 		// The app /service homepage
25
-// 		URL: "https://example.com/foobar-app/",
26
-// 		// The full path of the interpreter, if any (ruby, python, node, etc)
27
-// 		Interpreter: "/opt/node/bin/node",
28
-// 		// The name of the executable (or script)
29
-// 		Exec: "foobar-app.js",
30
-// 		// An array of arguments
31
-// 		Argv: []string{"-c", "/path/to/config.json"},
32
-// 		// A map of Environment variables that should be set
33
-// 		Envs: map[string]string{
34
-// 			PORT: "8080",
35
-// 			ENV: "development",
36
-// 		},
37
-// 		// The user (Linux & Mac only).
38
-// 		// This does not apply to userspace services.
39
-// 		// There may be special considerations
40
-// 		User: "www-data",
41
-// 		// If different from User
42
-// 		Group: "",
43
-// 		// Whether to install as a system or user service
44
-// 		System: false,
45
-// 		// Whether or not the service may need privileged ports
46
-// 		PrivilegedPorts: false,
47
-// 	}
48
-//
49
-// Note that some fields are exported for templating,
50
-// but not intended to be set by you.
51
-// These are documented as omitted from JSON.
52
-// Try to stick to what's outlined above.
53
-type Config struct {
54
-	Title               string            `json:"title"`
55
-	Name                string            `json:"name"`
56
-	Desc                string            `json:"desc"`
57
-	URL                 string            `json:"url"`
58
-	ReverseDNS          string            `json:"reverse_dns"` // i.e. com.example.foo-app
59
-	Interpreter         string            `json:"interpreter"` // i.e. node, python
60
-	Exec                string            `json:"exec"`
61
-	Argv                []string          `json:"argv"`
62
-	Workdir             string            `json:"workdir"`
63
-	Envs                map[string]string `json:"envs"`
64
-	User                string            `json:"user"`
65
-	Group               string            `json:"group"`
66
-	home                string            `json:"-"`
67
-	Local               string            `json:"-"`
68
-	Logdir              string            `json:"-"`
69
-	System              bool              `json:"system"`
70
-	Restart             bool              `json:"restart"`
71
-	Production          bool              `json:"production"`
72
-	PrivilegedPorts     bool              `json:"privileged_ports"`
73
-	MultiuserProtection bool              `json:"multiuser_protection"`
74
-}
11
+	"git.rootprojects.org/root/go-serviceman/service"
12
+)
75 13
 
76 14
 // Install will do a best-effort attempt to install a start-on-startup
77 15
 // user or system service via systemd, launchd, or reg.exe
78
-func Install(c *Config) error {
16
+func Install(c *service.Service) error {
79 17
 	if "" == c.Exec {
80 18
 		c.Exec = c.Name
81 19
 	}
@@ -87,7 +25,7 @@ func Install(c *Config) error {
87 25
 			os.Exit(4)
88 26
 			return err
89 27
 		} else {
90
-			c.home = home
28
+			c.Home = home
91 29
 		}
92 30
 	}
93 31
 
@@ -112,6 +50,7 @@ func IsPrivileged() bool {
112 50
 }
113 51
 
114 52
 func WhereIs(exec string) (string, error) {
53
+	// TODO use exec.LookPath instead
115 54
 	exec = filepath.ToSlash(exec)
116 55
 	if strings.Contains(exec, "/") {
117 56
 		// it's a path (so we don't allow filenames with slashes)

+ 5
- 4
installer/install_darwin.go View File

@@ -10,9 +10,10 @@ import (
10 10
 	"text/template"
11 11
 
12 12
 	"git.rootprojects.org/root/go-serviceman/installer/static"
13
+	"git.rootprojects.org/root/go-serviceman/service"
13 14
 )
14 15
 
15
-func install(c *Config) error {
16
+func install(c *service.Service) error {
16 17
 	// Darwin-specific config options
17 18
 	if c.PrivilegedPorts {
18 19
 		if !c.System {
@@ -21,7 +22,7 @@ func install(c *Config) error {
21 22
 	}
22 23
 	plistDir := "/Library/LaunchDaemons/"
23 24
 	if !c.System {
24
-		plistDir = filepath.Join(c.home, "Library/LaunchAgents")
25
+		plistDir = filepath.Join(c.Home, "Library/LaunchAgents")
25 26
 	}
26 27
 
27 28
 	// Check paths first
@@ -57,8 +58,8 @@ func install(c *Config) error {
57 58
 	}
58 59
 	fmt.Printf("Installed. To start '%s' run the following:\n", c.Name)
59 60
 	// TODO template config file
60
-	if "" != c.home {
61
-		plistPath = strings.Replace(plistPath, c.home, "~", 1)
61
+	if "" != c.Home {
62
+		plistPath = strings.Replace(plistPath, c.Home, "~", 1)
62 63
 	}
63 64
 	sudo := ""
64 65
 	if c.System {

+ 3
- 2
installer/install_linux.go View File

@@ -9,9 +9,10 @@ import (
9 9
 	"text/template"
10 10
 
11 11
 	"git.rootprojects.org/root/go-serviceman/installer/static"
12
+	"git.rootprojects.org/root/go-serviceman/service"
12 13
 )
13 14
 
14
-func install(c *Config) error {
15
+func install(c *service.Service) error {
15 16
 	// Linux-specific config options
16 17
 	if c.System {
17 18
 		if "" == c.User {
@@ -30,7 +31,7 @@ func install(c *Config) error {
30 31
 		// * ~/.local/share/systemd/user/watchdog.service
31 32
 		// * ~/.config/systemd/user/watchdog.service
32 33
 		// https://wiki.archlinux.org/index.php/Systemd/User
33
-		serviceDir = filepath.Join(c.home, ".local/share/systemd/user")
34
+		serviceDir = filepath.Join(c.Home, ".local/share/systemd/user")
34 35
 		err := os.MkdirAll(filepath.Dir(serviceDir), 0755)
35 36
 		if nil != err {
36 37
 			return err

+ 1
- 0
installer/install_notwindows.go View File

@@ -8,6 +8,7 @@ import (
8 8
 )
9 9
 
10 10
 func whereIs(exe string) (string, error) {
11
+	// TODO use exec.LookPath instead
11 12
 	cmd := exec.Command("command", "-v", exe)
12 13
 	out, err := cmd.Output()
13 14
 	if nil != err {

+ 5
- 1
installer/install_other.go View File

@@ -2,6 +2,10 @@
2 2
 
3 3
 package installer
4 4
 
5
-func install(c *Config) error {
5
+import (
6
+	"git.rootprojects.org/root/go-serviceman/service"
7
+)
8
+
9
+func install(c *service.Service) error {
6 10
 	return nil, nil
7 11
 }

+ 84
- 24
installer/install_windows.go View File

@@ -1,19 +1,25 @@
1 1
 package installer
2 2
 
3 3
 import (
4
+	"encoding/json"
4 5
 	"fmt"
6
+	"io/ioutil"
5 7
 	"log"
8
+	"os"
6 9
 	"os/exec"
7 10
 	"path/filepath"
8 11
 	"strings"
9 12
 
13
+	"git.rootprojects.org/root/go-serviceman/service"
14
+
10 15
 	"golang.org/x/sys/windows/registry"
11 16
 )
12 17
 
18
+// TODO nab some goodness from https://github.com/takama/daemon
19
+
13 20
 // TODO system service requires elevated privileges
14 21
 // See https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
15
-func install(c *Config) error {
16
-	//token := windows.Token(0)
22
+func install(c *service.Service) error {
17 23
 	/*
18 24
 		// LEAVE THIS DOCUMENTATION HERE
19 25
 		reg.exe
@@ -51,30 +57,39 @@ func install(c *Config) error {
51 57
 	}
52 58
 	defer k.Close()
53 59
 
54
-	setArgs := ""
55
-	args := c.Argv
56
-	exec := filepath.Join(c.home, ".local", "opt", c.Name, c.Exec)
57
-	bin := c.Interpreter
58
-	if "" != bin {
59
-		// If this is something like node or python,
60
-		// the interpeter must be called as "the main thing"
61
-		// and "the app" must be an argument
62
-		args = append([]string{exec}, args...)
63
-	} else {
64
-		// Otherwise, if "the app" is a true binary,
65
-		// it can be "the main thing"
66
-		bin = exec
67
-	}
68
-	if 0 != len(args) {
69
-		// On Windows the /c acts kinda like -- does on *nix,
70
-		// at least for commands in the registry that have arguments
71
-		setArgs = ` /c `
60
+	args, err := installServiceman(c)
61
+	if nil != err {
62
+		return err
72 63
 	}
73 64
 
74
-	// The final string ends up looking something like one of these:
75
-	// "C:\Users\aj\.local\opt\appname\appname.js /c -p 8080"
76
-	// "C:\Program Files (x64)\nodejs\node.exe /c C:\Users\aj\.local\opt\appname\appname.js -p 8080"
77
-	regSZ := bin + setArgs + strings.Join(c.Argv, " ")
65
+	/*
66
+		setArgs := ""
67
+		args := c.Argv
68
+		exec := c.Exec
69
+		bin := c.Interpreter
70
+		if "" != bin {
71
+			// If this is something like node or python,
72
+			// the interpeter must be called as "the main thing"
73
+			// and "the app" must be an argument
74
+			args = append([]string{exec}, args...)
75
+		} else {
76
+			// Otherwise, if "the app" is a true binary,
77
+			// it can be "the main thing"
78
+			bin = exec
79
+		}
80
+		if 0 != len(args) {
81
+			// On Windows the /c acts kinda like -- does on *nix,
82
+			// at least for commands in the registry that have arguments
83
+			setArgs = ` /c `
84
+		}
85
+
86
+		// The final string ends up looking something like one of these:
87
+		// "C:\Users\aj\.local\opt\appname\appname.js /c -p 8080"
88
+		// "C:\Program Files (x64)\nodejs\node.exe /c C:\Users\aj\.local\opt\appname\appname.js -p 8080"
89
+		regSZ := bin + setArgs + strings.Join(c.Argv, " ")
90
+	*/
91
+
92
+	regSZ := fmt.Sprintf("%s /c %s", args[0], strings.Join(args[1:], " "))
78 93
 	if len(regSZ) > 260 {
79 94
 		return fmt.Errorf("data value is too long for registry entry")
80 95
 	}
@@ -85,7 +100,52 @@ func install(c *Config) error {
85 100
 	return nil
86 101
 }
87 102
 
103
+// copies self to install path and returns config path
104
+func installServiceman(c *service.Service) ([]string, error) {
105
+	// TODO check version and upgrade or dismiss
106
+	self := os.Args[0]
107
+	smdir := `\opt\serviceman`
108
+	// TODO support service level services (which probably wouldn't need serviceman)
109
+	smdir = filepath.Join(c.Home, ".local", smdir)
110
+	// for now we'll scope the runner to the name of the application
111
+	smbin := filepath.Join(smdir, `bin\serviceman.%s`, c.Name)
112
+
113
+	if smbin != self {
114
+		err := os.MkdirAll(filepath.Dir(smbin))
115
+		if nil != err {
116
+			return "", err
117
+		}
118
+		bin, err := ioutil.ReadFile(self)
119
+		if nil != err {
120
+			return "", err
121
+		}
122
+		err := ioutil.WriteFile(smbin, bin, 0755)
123
+		if nil != err {
124
+			return "", err
125
+		}
126
+	}
127
+
128
+	b, err := json.Marshal(c)
129
+	if nil != err {
130
+		// this should be impossible, so we'll just panic
131
+		panic(err)
132
+	}
133
+	confpath := filepath.Join(smpath, `etc`, conf.Name+`.json`)
134
+	err := ioutil.WriteFile(confpath, b, 0640)
135
+	if nil != err {
136
+		return err
137
+	}
138
+
139
+	return []string{
140
+		smbin,
141
+		"run",
142
+		"--config",
143
+		confpath,
144
+	}, nil
145
+}
146
+
88 147
 func whereIs(exe string) (string, error) {
148
+	// TODO use exec.LookPath instead
89 149
 	cmd := exec.Command("where.exe", exe)
90 150
 	out, err := cmd.Output()
91 151
 	if nil != err {

+ 85
- 0
runner/runner.go View File

@@ -0,0 +1,85 @@
1
+package runner
2
+
3
+import (
4
+	"fmt"
5
+	"os"
6
+	"os/exec"
7
+	"path/filepath"
8
+	"time"
9
+
10
+	"git.rootprojects.org/root/go-serviceman/service"
11
+)
12
+
13
+// Notes on spawning a child process
14
+// https://groups.google.com/forum/#!topic/golang-nuts/shST-SDqIp4
15
+
16
+func Run(conf *service.Service) {
17
+	originalBackoff := 1 * time.Second
18
+	maxBackoff := 1 * time.Minute
19
+	threshold := 5 * time.Second
20
+
21
+	backoff := originalBackoff
22
+	failures := 0
23
+	logfile := filepath.Join(conf.Logdir, conf.Name+".log")
24
+
25
+	binpath := conf.Exec
26
+	args := []string{}
27
+	if "" != conf.Interpreter {
28
+		binpath = conf.Interpreter
29
+		args = append(args, conf.Exec)
30
+	}
31
+	args = append(args, conf.Argv...)
32
+
33
+	for {
34
+		// setup the log
35
+		lf, err := os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
36
+		if nil != err {
37
+			fmt.Fprintf(os.Stderr, "Could not open log file %q\n", logfile)
38
+			lf = os.Stderr
39
+		} else {
40
+			defer lf.Close()
41
+		}
42
+
43
+		start := time.Now()
44
+		cmd := exec.Command(binpath, args...)
45
+		cmd.Stdin = nil
46
+		cmd.Stdout = lf
47
+		cmd.Stderr = lf
48
+		if "" != conf.Workdir {
49
+			cmd.Dir = conf.Workdir
50
+		}
51
+		err = cmd.Start()
52
+		if nil != err {
53
+			fmt.Fprintf(lf, "Could not start %q process: %s\n", conf.Name, err)
54
+		} else {
55
+			err = cmd.Wait()
56
+			if nil != err {
57
+				fmt.Fprintf(lf, "Process %q failed with error: %s\n", conf.Name, err)
58
+			} else {
59
+				fmt.Fprintf(lf, "Process %q exited cleanly\n", conf.Name)
60
+				fmt.Printf("Process %q exited cleanly\n", conf.Name)
61
+			}
62
+		}
63
+
64
+		// if this is a oneshot... so it is
65
+		if !conf.Restart {
66
+			fmt.Printf("Not restarting %q because `restart` set to `false`\n", conf.Name)
67
+			fmt.Fprintf(lf, "Not restarting %q because `restart` set to `false`\n", conf.Name)
68
+			break
69
+		}
70
+
71
+		end := time.Now()
72
+		if end.Sub(start) > threshold {
73
+			backoff = originalBackoff
74
+			failures = 0
75
+		} else {
76
+			failures += 1
77
+			fmt.Fprintf(lf, "Waiting %s to restart %q (%d consequtive immediate exits)\n", backoff, conf.Name, failures)
78
+			time.Sleep(backoff)
79
+			backoff *= 2
80
+			if backoff > maxBackoff {
81
+				backoff = maxBackoff
82
+			}
83
+		}
84
+	}
85
+}

+ 141
- 0
service/service.go View File

@@ -0,0 +1,141 @@
1
+package service
2
+
3
+import (
4
+	"fmt"
5
+	"os"
6
+	"path/filepath"
7
+	"strings"
8
+)
9
+
10
+// Service should describe the service well-enough for it to
11
+// run on Mac, Linux, and Windows.
12
+//
13
+// 	&Service{
14
+// 		// A human-friendy name
15
+// 		Title: "Foobar App",
16
+// 		// A computer-friendly name
17
+// 		Name: "foobar-app",
18
+// 		// A name for OS X plist
19
+// 		ReverseDNS: "com.example.foobar-app",
20
+// 		// A human-friendly description
21
+// 		Desc: "Foobar App",
22
+// 		// The app /service homepage
23
+// 		URL: "https://example.com/foobar-app/",
24
+// 		// The full path of the interpreter, if any (ruby, python, node, etc)
25
+// 		Interpreter: "/opt/node/bin/node",
26
+// 		// The name of the executable (or script)
27
+// 		Exec: "foobar-app.js",
28
+// 		// An array of arguments
29
+// 		Argv: []string{"-c", "/path/to/config.json"},
30
+// 		// A map of Environment variables that should be set
31
+// 		Envs: map[string]string{
32
+// 			PORT: "8080",
33
+// 			ENV: "development",
34
+// 		},
35
+// 		// The user (Linux & Mac only).
36
+// 		// This does not apply to userspace services.
37
+// 		// There may be special considerations
38
+// 		User: "www-data",
39
+// 		// If different from User
40
+// 		Group: "",
41
+// 		// Whether to install as a system or user service
42
+// 		System: false,
43
+// 		// Whether or not the service may need privileged ports
44
+// 		PrivilegedPorts: false,
45
+// 	}
46
+//
47
+// Note that some fields are exported for templating,
48
+// but not intended to be set by you.
49
+// These are documented as omitted from JSON.
50
+// Try to stick to what's outlined above.
51
+type Service struct {
52
+	Title               string            `json:"title"`
53
+	Name                string            `json:"name"`
54
+	Desc                string            `json:"desc"`
55
+	URL                 string            `json:"url"`
56
+	ReverseDNS          string            `json:"reverse_dns"` // i.e. com.example.foo-app
57
+	Interpreter         string            `json:"interpreter"` // i.e. node, python
58
+	Exec                string            `json:"exec"`
59
+	Argv                []string          `json:"argv"`
60
+	Workdir             string            `json:"workdir"`
61
+	Envs                map[string]string `json:"envs"`
62
+	User                string            `json:"user"`
63
+	Group               string            `json:"group"`
64
+	Home                string            `json:"-"`
65
+	Local               string            `json:"-"`
66
+	Logdir              string            `json:"logdir"`
67
+	System              bool              `json:"system"`
68
+	Restart             bool              `json:"restart"`
69
+	Production          bool              `json:"production"`
70
+	PrivilegedPorts     bool              `json:"privileged_ports"`
71
+	MultiuserProtection bool              `json:"multiuser_protection"`
72
+}
73
+
74
+func (s *Service) Normalize(force bool) {
75
+	if "" == s.Name {
76
+		ext := filepath.Ext(s.Exec)
77
+		base := filepath.Base(s.Exec[:len(s.Exec)-len(ext)])
78
+		s.Name = strings.ToLower(base)
79
+	}
80
+	if "" == s.Title {
81
+		s.Title = s.Name
82
+	}
83
+	if "" == s.ReverseDNS {
84
+		// technically should be something more like "com.example." + s.Name,
85
+		// but whatever
86
+		s.ReverseDNS = s.Name
87
+	}
88
+
89
+	if !s.System {
90
+		home, err := os.UserHomeDir()
91
+		if nil != err {
92
+			fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
93
+			os.Exit(4)
94
+			return
95
+		}
96
+		s.Local = filepath.Join(home, ".local")
97
+		s.Logdir = filepath.Join(home, ".local", "share", s.Name, "var", "log")
98
+	} else {
99
+		s.Logdir = "/var/log/" + s.Name
100
+	}
101
+
102
+	// Check to see if Exec exists
103
+	//   /whatever => must exist exactly
104
+	//   ./whatever => must exist in current or WorkDir(TODO)
105
+	//   whatever => may also exist in {{ .Local }}/opt/{{ .Name }}/{{ .Exec }}
106
+	_, err := os.Stat(s.Exec)
107
+	if nil != err {
108
+		bad := true
109
+		if !strings.Contains(filepath.ToSlash(s.Exec), "/") {
110
+			optpath := filepath.Join(s.Local, "/opt", s.Name, s.Exec)
111
+			_, err := os.Stat(optpath)
112
+			if nil == err {
113
+				bad = false
114
+				fmt.Fprintf(os.Stderr, "Using '%s' for '%s'\n", optpath, s.Exec)
115
+				s.Exec = optpath
116
+			}
117
+		}
118
+
119
+		if bad {
120
+			// TODO look for it in WorkDir?
121
+			fmt.Fprintf(os.Stderr, "Error: '%s' could not be found.\n", s.Exec)
122
+			if !force {
123
+				os.Exit(5)
124
+				return
125
+			}
126
+			execpath, err := filepath.Abs(s.Exec)
127
+			if nil == err {
128
+				s.Exec = execpath
129
+			}
130
+			fmt.Fprintf(os.Stderr, "Using '%s' anyway.\n", s.Exec)
131
+		}
132
+	} else {
133
+		execpath, err := filepath.Abs(s.Exec)
134
+		if nil != err {
135
+			fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
136
+			os.Exit(4)
137
+		} else {
138
+			s.Exec = execpath
139
+		}
140
+	}
141
+}

+ 97
- 63
serviceman.go View File

@@ -3,22 +3,52 @@
3 3
 package main
4 4
 
5 5
 import (
6
+	"encoding/json"
6 7
 	"flag"
7 8
 	"fmt"
9
+	"io/ioutil"
8 10
 	"os"
9
-	"path/filepath"
11
+	"os/exec"
10 12
 	"strings"
11 13
 	"time"
12 14
 
13 15
 	"git.rootprojects.org/root/go-serviceman/installer"
16
+	"git.rootprojects.org/root/go-serviceman/runner"
17
+	"git.rootprojects.org/root/go-serviceman/service"
14 18
 )
15 19
 
16 20
 var GitRev = "000000000"
17 21
 var GitVersion = "v0.0.0"
18 22
 var GitTimestamp = time.Now().Format(time.RFC3339)
19 23
 
24
+func usage() {
25
+	fmt.Println("Usage: serviceman install ./foo-app -- --foo-arg")
26
+	fmt.Println("Usage: serviceman run --config ./foo-app.json")
27
+}
28
+
20 29
 func main() {
21
-	conf := &installer.Config{
30
+	if len(os.Args) < 2 {
31
+		fmt.Fprintf(os.Stderr, "Too few arguments: %s\n", strings.Join(os.Args, " "))
32
+		usage()
33
+		os.Exit(1)
34
+	}
35
+
36
+	top := os.Args[1]
37
+	os.Args = append(os.Args[:1], os.Args[2:]...)
38
+	switch top {
39
+	case "install":
40
+		install()
41
+	case "run":
42
+		run()
43
+	default:
44
+		fmt.Fprintf(os.Stderr, "Unknown argument %s\n", top)
45
+		usage()
46
+		os.Exit(1)
47
+	}
48
+}
49
+
50
+func install() {
51
+	conf := &service.Service{
22 52
 		Restart: true,
23 53
 	}
24 54
 
@@ -91,79 +121,83 @@ func main() {
91 121
 		conf.Argv = append(args[1:], conf.Argv...)
92 122
 	}
93 123
 
94
-	if "" == conf.Name {
95
-		ext := filepath.Ext(conf.Exec)
96
-		base := filepath.Base(conf.Exec[:len(conf.Exec)-len(ext)])
97
-		conf.Name = strings.ToLower(base)
124
+	conf.Normalize(force)
125
+
126
+	fmt.Printf("\n%#v\n\n", conf)
127
+
128
+	err = installer.Install(conf)
129
+	if nil != err {
130
+		fmt.Fprintf(os.Stderr, "%s\n", err)
131
+		fmt.Fprintf(os.Stderr, "Use 'sudo' to install as a privileged system service.\n")
132
+		fmt.Fprintf(os.Stderr, "Use '--user' to install as an user service.\n")
98 133
 	}
99
-	if "" == conf.Title {
100
-		conf.Title = conf.Name
134
+}
135
+
136
+func run() {
137
+	var confpath string
138
+	var daemonize bool
139
+	flag.StringVar(&confpath, "config", "", "path to a config file to run")
140
+	flag.BoolVar(&daemonize, "daemon", false, "spawn a child process that lives in the background, and exit")
141
+	flag.Parse()
142
+
143
+	if "" == confpath {
144
+		fmt.Fprintf(os.Stderr, "%s", strings.Join(flag.Args(), " "))
145
+		fmt.Fprintf(os.Stderr, "--config /path/to/config.json is required\n")
146
+		usage()
147
+		os.Exit(1)
101 148
 	}
102
-	if "" == conf.ReverseDNS {
103
-		// technically should be something more like "com.example." + conf.Name,
104
-		// but whatever
105
-		conf.ReverseDNS = conf.Name
149
+
150
+	b, err := ioutil.ReadFile(confpath)
151
+	if nil != err {
152
+		fmt.Fprintf(os.Stderr, "Couldn't read config file: %s\n", err)
153
+		os.Exit(400)
106 154
 	}
107 155
 
108
-	if !conf.System {
109
-		home, err := os.UserHomeDir()
110
-		if nil != err {
111
-			fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
112
-			os.Exit(4)
113
-			return
114
-		}
115
-		conf.Local = filepath.Join(home, ".local")
116
-		conf.Logdir = filepath.Join(home, ".local", "share", conf.Name, "var", "log")
117
-	} else {
118
-		conf.Logdir = "/var/log/" + conf.Name
156
+	s := &service.Service{}
157
+	err = json.Unmarshal(b, s)
158
+	if nil != err {
159
+		fmt.Fprintf(os.Stderr, "Couldn't JSON parse config file: %s\n", err)
160
+		os.Exit(400)
119 161
 	}
120 162
 
121
-	// Check to see if Exec exists
122
-	//   /whatever => must exist exactly
123
-	//   ./whatever => must exist in current or WorkDir(TODO)
124
-	//   whatever => may also exist in {{ .Local }}/opt/{{ .Name }}/{{ .Exec }}
125
-	_, err = os.Stat(conf.Exec)
163
+	m := map[string]interface{}{}
164
+	err = json.Unmarshal(b, &m)
126 165
 	if nil != err {
127
-		bad := true
128
-		if !strings.Contains(filepath.ToSlash(conf.Exec), "/") {
129
-			optpath := filepath.Join(conf.Local, "/opt", conf.Name, conf.Exec)
130
-			_, err := os.Stat(optpath)
131
-			if nil == err {
132
-				bad = false
133
-				fmt.Fprintf(os.Stderr, "Using '%s' for '%s'\n", optpath, conf.Exec)
134
-				conf.Exec = optpath
135
-			}
136
-		}
166
+		fmt.Fprintf(os.Stderr, "Couldn't JSON parse config file: %s\n", err)
167
+		os.Exit(400)
168
+	}
137 169
 
138
-		if bad {
139
-			// TODO look for it in WorkDir?
140
-			fmt.Fprintf(os.Stderr, "Error: '%s' could not be found.\n", conf.Exec)
141
-			if !force {
142
-				os.Exit(5)
143
-				return
144
-			}
145
-			execpath, err := filepath.Abs(conf.Exec)
146
-			if nil == err {
147
-				conf.Exec = execpath
148
-			}
149
-			fmt.Fprintf(os.Stderr, "Using '%s' anyway.\n", conf.Exec)
150
-		}
151
-	} else {
152
-		execpath, err := filepath.Abs(conf.Exec)
153
-		if nil != err {
154
-			fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
155
-			os.Exit(4)
156
-		} else {
157
-			conf.Exec = execpath
158
-		}
170
+	// default Restart to true
171
+	if _, ok := m["restart"]; !ok {
172
+		s.Restart = true
159 173
 	}
160 174
 
161
-	fmt.Printf("\n%#v\n\n", conf)
175
+	if "" == s.Exec {
176
+		fmt.Fprintf(os.Stderr, "Missing exec\n")
177
+		os.Exit(400)
178
+	}
162 179
 
163
-	err = installer.Install(conf)
180
+	s.Normalize(false)
181
+	fmt.Fprintf(os.Stdout, "Logdir: %s\n", s.Logdir)
182
+	if !daemonize {
183
+		fmt.Fprintf(os.Stdout, "Running %s %s %s\n", s.Interpreter, s.Exec, strings.Join(s.Argv, " "))
184
+		runner.Run(s)
185
+		return
186
+	}
187
+
188
+	cmd := exec.Command(os.Args[0], "run", "--config", confpath)
189
+	// for debugging
190
+	/*
191
+		out, err := cmd.CombinedOutput()
192
+		if nil != err {
193
+			fmt.Println(err)
194
+		}
195
+		fmt.Println(string(out))
196
+	*/
197
+
198
+	err = cmd.Start()
164 199
 	if nil != err {
165 200
 		fmt.Fprintf(os.Stderr, "%s\n", err)
166
-		fmt.Fprintf(os.Stderr, "Use 'sudo' to install as a privileged system service.\n")
167
-		fmt.Fprintf(os.Stderr, "Use '--user' to install as an user service.\n")
201
+		os.Exit(500)
168 202
 	}
169 203
 }

Loading…
Cancel
Save