Browse Source

basic functionality works

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

+ 15
- 1
README.md View File

@@ -1,3 +1,17 @@
1 1
 # go-serviceman
2 2
 
3
-A cross-platform service manager.
3
+A cross-platform service manager.
4
+
5
+Goal:
6
+
7
+```bash
8
+serviceman install [options] [interpreter] <service> [-- [options]]
9
+```
10
+
11
+```bash
12
+serviceman install --user ./foo-app -- -c ./
13
+```
14
+
15
+```bash
16
+serviceman install --user /usr/local/bin/node ./whatever.js -- -c ./
17
+```

+ 10
- 0
go.mod View File

@@ -0,0 +1,10 @@
1
+module git.rootprojects.org/root/go-serviceman
2
+
3
+go 1.12
4
+
5
+require (
6
+	git.rootprojects.org/root/go-gitver v1.1.2
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
10
+)

+ 52
- 0
go.sum View File

@@ -0,0 +1,52 @@
1
+git.rootprojects.org/root/go-gitver v1.1.2 h1:AQhr8ktJyP+X+jFbtLavCi/FQLSmB6xvdG2Nfp+J2JA=
2
+git.rootprojects.org/root/go-gitver v1.1.2/go.mod h1:Rj1v3TBhvdaSphFEqMynUYwAz/4f+wY/+syBTvRrmlI=
3
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
4
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
5
+github.com/UnnoTed/fileb0x v1.1.3 h1:TUfJRey+psXuivBqasgp7Du3iXB4hzjI5UXDl+BCrzE=
6
+github.com/UnnoTed/fileb0x v1.1.3/go.mod h1:AyTnLP7elx6MM4eHxahl5sBEWBw0QLf6TM/s64LtM4s=
7
+github.com/airking05/termui v2.2.0+incompatible h1:S3j2WJzr70u8KjUktaQ0Cmja+R0edOXChltFoQSGG8I=
8
+github.com/airking05/termui v2.2.0+incompatible/go.mod h1:B/M5sgOwSZlvGm3TsR98s1BSzlSH4wPQzUUNwZG+uUM=
9
+github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ=
10
+github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
11
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
12
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
14
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
15
+github.com/karrick/godirwalk v1.7.8 h1:VfG72pyIxgtC7+3X9CMHI0AOl4LwyRAg98WAgsvffi8=
16
+github.com/karrick/godirwalk v1.7.8/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
17
+github.com/labstack/echo v3.2.1+incompatible h1:J2M7YArHx4gi8p/3fDw8tX19SXhBCoRpviyAZSN3I88=
18
+github.com/labstack/echo v3.2.1+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
19
+github.com/labstack/gommon v0.2.7 h1:2qOPq/twXDrQ6ooBGrn3mrmVOC+biLlatwgIu8lbzRM=
20
+github.com/labstack/gommon v0.2.7/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
21
+github.com/maruel/panicparse v1.1.1 h1:k62YPcEoLncEEpjMt92GtG5ugb8WL/510Ys3/h5IkRc=
22
+github.com/maruel/panicparse v1.1.1/go.mod h1:nty42YY5QByNC5MM7q/nj938VbgPU7avs45z6NClpxI=
23
+github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
24
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
25
+github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
26
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
27
+github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
28
+github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
29
+github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
30
+github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
31
+github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e h1:fvw0uluMptljaRKSU8459cJ4bmi3qUYyMs5kzpic2fY=
32
+github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
33
+github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
34
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
35
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
36
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
37
+github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
38
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
39
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
40
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
41
+github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
42
+github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
43
+golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ=
44
+golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
45
+golang.org/x/net v0.0.0-20180921000356-2f5d2388922f h1:QM2QVxvDoW9PFSPp/zy9FgxJLfaWTZlS61KEPtBwacM=
46
+golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
47
+golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8 h1:R91KX5nmbbvEd7w370cbVzKC+EzCTGqZq63Zad5IcLM=
48
+golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
49
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
50
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
51
+gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
52
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

+ 24
- 0
installer/b0x.toml View File

@@ -0,0 +1,24 @@
1
+# all folders and files are relative to the path where fileb0x was run!
2
+
3
+pkg = "static"
4
+dest = "./static/"
5
+fmt = true
6
+
7
+# build tags for the main b0x.go file
8
+tags = ""
9
+
10
+# default: ab0x.go (so that its init() sorts first)
11
+output = "ab0x.go"
12
+
13
+[[custom]]
14
+  # everything inside the folder
15
+  # type: array of strings
16
+  files = ["./dist/"]
17
+
18
+  # base is the path that will be removed from all files' path
19
+  # type: string
20
+  base = ""
21
+
22
+  # prefix is the path that will be added to all files' path
23
+  # type: string
24
+  prefix = ""

+ 71
- 0
installer/dist/Library/LaunchDaemons/_rdns_.plist.tmpl View File

@@ -0,0 +1,71 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+<plist version="1.0">
4
+<dict>
5
+	<key>Label</key>
6
+	<string>{{ .ReverseDNS }}</string>
7
+	<key>ProgramArguments</key>
8
+	<array>
9
+		{{- if .Interpreter }}
10
+		<string>{{ .Interpreter }}</string>
11
+		{{- end }}
12
+		<string>{{ .Local }}/opt/{{ .Name }}/{{ .Exec }}</string>
13
+	  {{- if .Argv }}
14
+		{{- range $arg := .Argv }}
15
+		<string>{{ $arg }}</string>
16
+		{{- end }}
17
+	  {{- end }}
18
+	</array>
19
+	{{- if .Envs }}
20
+	<key>EnvironmentVariables</key>
21
+	<dict>
22
+		{{- range $key, $value := .Envs }}
23
+		<key>{{ $key }}</key>
24
+		<string>{{ $value }}</string>
25
+		{{- end }}
26
+	</dict>
27
+	{{- end }}
28
+
29
+	{{if .User -}}
30
+	<key>UserName</key>
31
+	<string>{{ .User }}</string>
32
+	<key>GroupName</key>
33
+	<string>{{ .Group }}</string>
34
+	<key>InitGroups</key>
35
+	<true/>
36
+
37
+	{{end -}}
38
+	<key>RunAtLoad</key>
39
+	<true/>
40
+	{{ if .Restart -}}
41
+	<key>KeepAlive</key>
42
+	<true/>
43
+	<!--dict>
44
+		<key>Crashed</key>
45
+		<true/>
46
+		<key>NetworkState</key>
47
+		<true/>
48
+		<key>SuccessfulExit</key>
49
+		<false/>
50
+	</dict-->
51
+
52
+	{{ end -}}
53
+	{{ if .Production -}}
54
+	<key>SoftResourceLimits</key>
55
+	<dict>
56
+		<key>NumberOfFiles</key>
57
+		<integer>8192</integer>
58
+	</dict>
59
+	<key>HardResourceLimits</key>
60
+	<dict/>
61
+
62
+	{{ end -}}
63
+	<key>WorkingDirectory</key>
64
+	<string>{{ .Local }}/opt/{{ .Name }}</string>
65
+
66
+	<key>StandardErrorPath</key>
67
+	<string>{{ .LogDir }}/{{ .Name }}.log</string>
68
+	<key>StandardOutPath</key>
69
+	<string>{{ .LogDir }}/{{ .Name }}.log</string>
70
+</dict>
71
+</plist>

+ 87
- 0
installer/dist/etc/systemd/system/_name_.service.tmpl View File

@@ -0,0 +1,87 @@
1
+# Pre-req
2
+# sudo mkdir -p {{ .Local }}/opt/{{ .Name }}/ {{ .Local }}/var/log/{{ .Name }}
3
+{{ if not .Local -}}
4
+{{- if and .User ( ne "root" .User ) -}}
5
+# sudo adduser {{ .User }} --home /opt/{{ .Name }}
6
+# sudo chown -R {{ .User }}:{{ .Group }} /opt/{{ .Name }}/ /var/log/{{ .Name }}
7
+{{- end }}
8
+{{ end -}}
9
+# Post-install
10
+# sudo systemctl {{ if .Local -}} --user {{ end -}} daemon-reload
11
+# sudo systemctl {{ if .Local -}} --user {{ end -}} restart {{ .Name }}.service
12
+# sudo journalctl {{ if .Local -}} --user {{ end -}} -xefu {{ .Name }}
13
+
14
+[Unit]
15
+Description={{ .Title }} - {{ .Desc }}
16
+Documentation={{ .URL }}
17
+{{ if not .Local -}}
18
+After=network-online.target
19
+Wants=network-online.target systemd-networkd-wait-online.service
20
+{{- end }}
21
+
22
+[Service]
23
+# Restart on crash (bad signal), but not on 'clean' failure (error exit code)
24
+# Allow up to 3 restarts within 10 seconds
25
+# (it's unlikely that a user or properly-running script will do this)
26
+Restart=on-abnormal
27
+StartLimitInterval=10
28
+StartLimitBurst=3
29
+
30
+{{ if .User -}}
31
+# User and group the process will run as
32
+User={{ .User }}
33
+Group={{ .Group }}
34
+
35
+{{ end -}}
36
+WorkingDirectory={{ .Local }}/opt/{{ .Name }}
37
+ExecStart={{if .Interpreter }}{{ .Interpreter }} {{ end }}{{ .Local }}/opt/{{ .Name }}/{{ .Name }} {{ .Args }}
38
+ExecReload=/bin/kill -USR1 $MAINPID
39
+
40
+{{if .Production -}}
41
+# Limit the number of file descriptors and processes; see `man systemd.exec` for more limit settings.
42
+# These are reasonable defaults for a production system.
43
+# Note: systemd "user units" do not support this
44
+LimitNOFILE=1048576
45
+LimitNPROC=64
46
+
47
+{{ end -}}
48
+{{if .MultiuserProtection -}}
49
+# Use private /tmp and /var/tmp, which are discarded after the service stops.
50
+PrivateTmp=true
51
+# Use a minimal /dev
52
+PrivateDevices=true
53
+# Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
54
+ProtectHome=true
55
+# Make /usr, /boot, /etc and possibly some more folders read-only.
56
+ProtectSystem=full
57
+# ... except /opt/{{ .Name }} because we want a place for the database
58
+# and /var/log/{{ .Name }} because we want a place where logs can go.
59
+# This merely retains r/w access rights, it does not add any new.
60
+# Must still be writable on the host!
61
+ReadWriteDirectories=/opt/{{ .Name }} /var/log/{{ .Name }}
62
+
63
+# Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories
64
+; ReadWritePaths=/opt/{{ .Name }} /var/log/{{ .Name }}
65
+
66
+{{ end -}}
67
+{{if .PrivilegedPorts -}}
68
+# The following additional security directives only work with systemd v229 or later.
69
+# They further retrict privileges that can be gained by the service.
70
+# Note that you may have to add capabilities required by any plugins in use.
71
+CapabilityBoundingSet=CAP_NET_BIND_SERVICE
72
+AmbientCapabilities=CAP_NET_BIND_SERVICE
73
+NoNewPrivileges=true
74
+
75
+# Caveat: Some features may need additional capabilities.
76
+# For example an "upload" may need CAP_LEASE
77
+; CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_LEASE
78
+; AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_LEASE
79
+; NoNewPrivileges=true
80
+
81
+{{ end -}}
82
+[Install]
83
+{{ if not .Local -}}
84
+WantedBy=multi-user.target
85
+{{- else -}}
86
+WantedBy=default.target
87
+{{- end }}

+ 7
- 0
installer/doc.go View File

@@ -0,0 +1,7 @@
1
+// Package installer can be used cross-platform to install apps
2
+// as either userspace or system services for fairly simple applications.
3
+// This is not intended for complex installers.
4
+//
5
+// I'm prototyping this out to be useful for more than just watchdog
6
+// hence there are a few unnecessary things for the sake of the trying it out
7
+package installer

+ 23
- 0
installer/filesystem.go View File

@@ -0,0 +1,23 @@
1
+package installer
2
+
3
+import (
4
+	"io"
5
+	"os"
6
+)
7
+
8
+// "A little copying is better than a little dependency"
9
+// These are here so that we don't need a dependency on http.FileSystem and http.File
10
+
11
+// FileSystem is the same as http.FileSystem
12
+type FileSystem interface {
13
+	Open(name string) (File, error)
14
+}
15
+
16
+// File is the same as http.File
17
+type File interface {
18
+	io.Closer
19
+	io.Reader
20
+	io.Seeker
21
+	Readdir(count int) ([]os.FileInfo, error)
22
+	Stat() (os.FileInfo, error)
23
+}

+ 128
- 0
installer/install.go View File

@@ -0,0 +1,128 @@
1
+//go:generate go run -mod=vendor github.com/UnnoTed/fileb0x b0x.toml
2
+
3
+package installer
4
+
5
+import (
6
+	"os"
7
+	"path/filepath"
8
+	"strings"
9
+)
10
+
11
+// Config should describe the service well-enough for it to
12
+// run on Mac, Linux, and Windows.
13
+//
14
+// 	&Config{
15
+// 		// A human-friendy name
16
+// 		Title: "Foobar App",
17
+// 		// A computer-friendly name
18
+// 		Name: "foobar-app",
19
+// 		// A name for OS X plist
20
+// 		ReverseDNS: "com.example.foobar-app",
21
+// 		// A human-friendly description
22
+// 		Desc: "Foobar App",
23
+// 		// The app /service homepage
24
+// 		URL: "https://example.com/foobar-app/",
25
+// 		// The full path of the interpreter, if any (ruby, python, node, etc)
26
+// 		Interpreter: "/opt/node/bin/node",
27
+// 		// The name of the executable (or script)
28
+// 		Exec: "foobar-app.js",
29
+// 		// An array of arguments
30
+// 		Argv: []string{"-c", "/path/to/config.json"},
31
+// 		// A map of Environment variables that should be set
32
+// 		Envs: map[string]string{
33
+// 			PORT: "8080",
34
+// 			ENV: "development",
35
+// 		},
36
+// 		// The user (Linux & Mac only).
37
+// 		// This does not apply to userspace services.
38
+// 		// There may be special considerations
39
+// 		User: "www-data",
40
+// 		// If different from User
41
+// 		Group: "",
42
+// 		// Whether to install as a system or user service
43
+// 		System: false,
44
+// 		// Whether or not the service may need privileged ports
45
+// 		PrivilegedPorts: false,
46
+// 	}
47
+//
48
+// Note that some fields are exported for templating,
49
+// but not intended to be set by you.
50
+// These are documented as omitted from JSON.
51
+// Try to stick to what's outlined above.
52
+type Config struct {
53
+	Title               string            `json:"title"`
54
+	Name                string            `json:"name"`
55
+	Desc                string            `json:"desc"`
56
+	URL                 string            `json:"url"`
57
+	ReverseDNS          string            `json:"reverse_dns"` // i.e. com.example.foo-app
58
+	Interpreter         string            `json:"interpreter"` // i.e. node, python
59
+	Exec                string            `json:"exec"`
60
+	Argv                []string          `json:"argv"`
61
+	Args                string            `json:"-"`
62
+	Envs                map[string]string `json:"envs"`
63
+	User                string            `json:"user"`
64
+	Group               string            `json:"group"`
65
+	home                string            `json:"-"`
66
+	Local               string            `json:"-"`
67
+	LogDir              string            `json:"-"`
68
+	System              bool              `json:"system"`
69
+	Restart             bool              `json:"restart"`
70
+	Production          bool              `json:"production"`
71
+	PrivilegedPorts     bool              `json:"privileged_ports"`
72
+	MultiuserProtection bool              `json:"multiuser_protection"`
73
+}
74
+
75
+// Install will do a best-effort attempt to install a start-on-startup
76
+// user or system service via systemd, launchd, or reg.exe
77
+func Install(c *Config) error {
78
+	if "" == c.Exec {
79
+		c.Exec = c.Name
80
+	}
81
+	c.Args = strings.Join(c.Argv, " ")
82
+
83
+	// TODO handle non-system installs
84
+	// * ~/.local/opt/watchdog/watchdog
85
+	// * ~/.local/share/watchdog/var/log/
86
+	// * ~/.config/watchdog/watchdog.json
87
+	if !c.System {
88
+		home, err := os.UserHomeDir()
89
+		if nil != err {
90
+			return err
91
+		}
92
+		c.home = home
93
+		c.Local = filepath.Join(c.home, ".local")
94
+		c.LogDir = filepath.Join(c.home, ".local", "share", c.Name, "var", "log")
95
+	} else {
96
+		c.LogDir = "/var/log/" + c.Name
97
+	}
98
+
99
+	err := install(c)
100
+	if nil != err {
101
+		return err
102
+	}
103
+
104
+	err = os.MkdirAll(c.LogDir, 0750)
105
+	if nil != err {
106
+		return err
107
+	}
108
+
109
+	return nil
110
+}
111
+
112
+// Returns true if we suspect that the current user (or process) will be able
113
+// to write to system folders, bind to privileged ports, and otherwise
114
+// successfully run a system service.
115
+func IsPrivileged() bool {
116
+	return isPrivileged()
117
+}
118
+
119
+func WhereIs(exec string) (string, error) {
120
+	exec = filepath.ToSlash(exec)
121
+	if strings.Contains(exec, "/") {
122
+		// filepath.Clean(exec)
123
+		// it's a path (don't allow filenames with slashes)
124
+		// TODO stat
125
+		return exec, nil
126
+	}
127
+	return whereIs(exec)
128
+}

+ 64
- 0
installer/install_darwin.go View File

@@ -0,0 +1,64 @@
1
+package installer
2
+
3
+import (
4
+	"bytes"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"os"
8
+	"path/filepath"
9
+	"strings"
10
+	"text/template"
11
+
12
+	"git.rootprojects.org/root/go-serviceman/installer/static"
13
+)
14
+
15
+func install(c *Config) error {
16
+	// Darwin-specific config options
17
+	if c.PrivilegedPorts {
18
+		if !c.System {
19
+			return fmt.Errorf("You must use root-owned LaunchDaemons (not user-owned LaunchAgents) to use priveleged ports on OS X")
20
+		}
21
+	}
22
+	plistDir := "/Library/LaunchDaemons/"
23
+	if !c.System {
24
+		plistDir = filepath.Join(c.home, "Library/LaunchAgents")
25
+	}
26
+
27
+	// Check paths first
28
+	err := os.MkdirAll(filepath.Dir(plistDir), 0750)
29
+	if nil != err {
30
+		return err
31
+	}
32
+
33
+	// Create service file from template
34
+	b, err := static.ReadFile("dist/Library/LaunchDaemons/_rdns_.plist.tmpl")
35
+	if err != nil {
36
+		return err
37
+	}
38
+	s := string(b)
39
+	rw := &bytes.Buffer{}
40
+	// not sure what the template name does, but whatever
41
+	tmpl, err := template.New("service").Parse(s)
42
+	if err != nil {
43
+		return err
44
+	}
45
+	err = tmpl.Execute(rw, c)
46
+	if nil != err {
47
+		return err
48
+	}
49
+
50
+	// Write the file out
51
+	// TODO rdns
52
+	plistName := c.Name + ".plist"
53
+	plistPath := filepath.Join(plistDir, plistName)
54
+	if err := ioutil.WriteFile(plistPath, rw.Bytes(), 0644); err != nil {
55
+		fmt.Println("Use 'sudo' to install as a privileged system service.")
56
+		fmt.Println("Use '--userspace' to install as an user service.")
57
+		return fmt.Errorf("ioutil.WriteFile error: %v", err)
58
+	}
59
+	fmt.Printf("Installed. To start '%s' run the following:\n", c.Name)
60
+	// TODO template config file
61
+	fmt.Printf("\tlaunchctl load -w %s\n", strings.Replace(plistPath, c.home, "~", 1))
62
+
63
+	return nil
64
+}

+ 76
- 0
installer/install_linux.go View File

@@ -0,0 +1,76 @@
1
+package installer
2
+
3
+import (
4
+	"bytes"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"os"
8
+	"path/filepath"
9
+	"text/template"
10
+
11
+	"git.rootprojects.org/root/go-serviceman/installer/static"
12
+)
13
+
14
+func install(c *Config) error {
15
+	// Linux-specific config options
16
+	if c.System {
17
+		if "" == c.User {
18
+			c.User = "root"
19
+		}
20
+	}
21
+	if "" == c.Group {
22
+		c.Group = c.User
23
+	}
24
+	serviceDir := "/etc/systemd/system/"
25
+
26
+	// Check paths first
27
+	serviceName := c.Name + ".service"
28
+	if !c.System {
29
+		// Not sure which of these it's supposed to be...
30
+		// * ~/.local/share/systemd/user/watchdog.service
31
+		// * ~/.config/systemd/user/watchdog.service
32
+		// https://wiki.archlinux.org/index.php/Systemd/User
33
+		serviceDir = filepath.Join(c.home, ".local/share/systemd/user")
34
+	}
35
+	err := os.MkdirAll(filepath.Dir(serviceDir), 0750)
36
+	if nil != err {
37
+		return err
38
+	}
39
+
40
+	// Create service file from template
41
+	b, err := static.ReadFile("dist/etc/systemd/system/_name_.service.tmpl")
42
+	if err != nil {
43
+		return err
44
+	}
45
+	s := string(b)
46
+	rw := &bytes.Buffer{}
47
+	// not sure what the template name does, but whatever
48
+	tmpl, err := template.New("service").Parse(s)
49
+	if err != nil {
50
+		return err
51
+	}
52
+	err = tmpl.Execute(rw, c)
53
+	if nil != err {
54
+		return err
55
+	}
56
+
57
+	// Write the file out
58
+	servicePath := filepath.Join(serviceDir, serviceName)
59
+	if err := ioutil.WriteFile(servicePath, rw.Bytes(), 0644); err != nil {
60
+		return fmt.Errorf("ioutil.WriteFile error: %v", err)
61
+	}
62
+
63
+	// TODO template this as well?
64
+	userspace := ""
65
+	sudo := "sudo "
66
+	if !c.System {
67
+		userspace = "--user "
68
+		sudo = ""
69
+	}
70
+	fmt.Printf("System service installed as '%s'.\n", servicePath)
71
+	fmt.Printf("Run the following to start '%s':\n", c.Name)
72
+	fmt.Printf("\t" + sudo + "systemctl " + userspace + "daemon-reload\n")
73
+	fmt.Printf("\t"+sudo+"systemctl "+userspace+"restart %s.service\n", c.Name)
74
+	fmt.Printf("\t"+sudo+"journalctl "+userspace+"-xefu %s\n", c.Name)
75
+	return nil
76
+}

+ 17
- 0
installer/install_notwindows.go View File

@@ -0,0 +1,17 @@
1
+// +build !windows
2
+
3
+package installer
4
+
5
+import (
6
+	"os/exec"
7
+	"strings"
8
+)
9
+
10
+func whereIs(exe string) (string, error) {
11
+	cmd := exec.Command("command", "-v", exe)
12
+	out, err := cmd.Output()
13
+	if nil != err {
14
+		return "", err
15
+	}
16
+	return strings.TrimSpace(string(out)), nil
17
+}

+ 7
- 0
installer/install_other.go View File

@@ -0,0 +1,7 @@
1
+// +build !windows,!linux,!darwin
2
+
3
+package installer
4
+
5
+func install(c *Config) error {
6
+	return nil, nil
7
+}

+ 95
- 0
installer/install_windows.go View File

@@ -0,0 +1,95 @@
1
+package installer
2
+
3
+import (
4
+	"fmt"
5
+	"log"
6
+	"os/exec"
7
+	"path/filepath"
8
+	"strings"
9
+
10
+	"golang.org/x/sys/windows/registry"
11
+)
12
+
13
+// TODO system service requires elevated privileges
14
+// See https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
15
+func install(c *Config) error {
16
+	//token := windows.Token(0)
17
+	/*
18
+		// LEAVE THIS DOCUMENTATION HERE
19
+		reg.exe
20
+		/V <value name> - "Telebit"
21
+		/T <data type> - "REG_SZ" - String
22
+		/D <value data>
23
+		/C - case sensitive
24
+		/F <search data??> - not sure...
25
+
26
+		// Special Note:
27
+		"/c" is similar to -- (*nix), and required within the data string
28
+		So instead of setting "do.exe --do-arg1 --do-arg2"
29
+		you must set "do.exe /c --do-arg1 --do-arg2"
30
+
31
+		vars.telebitNode += '.exe';
32
+		var cmd = 'reg.exe add "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"'
33
+		+ ' /V "Telebit" /t REG_SZ /D '
34
+		+ '"' + things.argv[0] + ' /c '  // something like C:\Program Files (x64)\nodejs\node.exe
35
+		+ [ path.join(__dirname, 'bin/telebitd.js')
36
+			, 'daemon'
37
+			, '--config'
38
+			, path.join(os.homedir(), '.config/telebit/telebitd.yml')
39
+			].join(' ')
40
+		+ '" /F'
41
+		;
42
+	*/
43
+	autorunKey := `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
44
+	k, _, err := registry.CreateKey(
45
+		registry.CURRENT_USER,
46
+		autorunKey,
47
+		registry.SET_VALUE,
48
+	)
49
+	if err != nil {
50
+		log.Fatal(err)
51
+	}
52
+	defer k.Close()
53
+
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 `
72
+	}
73
+
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, " ")
78
+	if len(regSZ) > 260 {
79
+		return fmt.Errorf("data value is too long for registry entry")
80
+	}
81
+	fmt.Println("Set Registry Key:")
82
+	fmt.Println(autorunKey, c.Title, regSZ)
83
+	k.SetStringValue(c.Title, regSZ)
84
+
85
+	return nil
86
+}
87
+
88
+func whereIs(exe string) (string, error) {
89
+	cmd := exec.Command("where.exe", exe)
90
+	out, err := cmd.Output()
91
+	if nil != err {
92
+		return "", err
93
+	}
94
+	return strings.TrimSpace(string(out)), nil
95
+}

+ 207
- 0
installer/static/ab0x.go
File diff suppressed because it is too large
View File


+ 15
- 0
installer/whoami.go View File

@@ -0,0 +1,15 @@
1
+// +build !windows
2
+
3
+package installer
4
+
5
+import "os/user"
6
+
7
+func isPrivileged() bool {
8
+	u, err := user.Current()
9
+	if nil != err {
10
+		return false
11
+	}
12
+
13
+	// not quite, but close enough for now
14
+	return "0" == u.Uid
15
+}

+ 43
- 0
installer/whoami_windows.go View File

@@ -0,0 +1,43 @@
1
+package installer
2
+
3
+import (
4
+	"fmt"
5
+	"os"
6
+
7
+	"golang.org/x/sys/windows"
8
+)
9
+
10
+func isPrivileged() bool {
11
+	var sid *windows.SID
12
+
13
+	// Although this looks scary, it is directly copied from the
14
+	// official windows documentation. The Go API for this is a
15
+	// direct wrap around the official C++ API.
16
+	// See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
17
+	err := windows.AllocateAndInitializeSid(
18
+		&windows.SECURITY_NT_AUTHORITY,
19
+		2,
20
+		windows.SECURITY_BUILTIN_DOMAIN_RID,
21
+		windows.DOMAIN_ALIAS_RID_ADMINS,
22
+		0, 0, 0, 0, 0, 0,
23
+		&sid)
24
+	if err != nil {
25
+		// we don't believe this _can_ return an error with the given inputs
26
+		// and if it does, the important info is still the false
27
+		fmt.Fprintf(os.Stderr, "warning: Unexpected Windows UserID Error: %s\n", err)
28
+		return false
29
+	}
30
+
31
+	// This appears to cast a null pointer so I'm not sure why this
32
+	// works, but this guy says it does and it Works for Me™:
33
+	// https://github.com/golang/go/issues/28804#issuecomment-438838144
34
+	token := windows.Token(0)
35
+
36
+	isAdmin, err := token.IsMember(sid)
37
+	if err != nil {
38
+		fmt.Fprintf(os.Stderr, "warning: Unexpected Windows Permission ID Error: %s\n", err)
39
+		return false
40
+	}
41
+
42
+	return isAdmin || token.IsElevated()
43
+}

+ 105
- 0
serviceman.go View File

@@ -0,0 +1,105 @@
1
+//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
2
+
3
+package main
4
+
5
+import (
6
+	"flag"
7
+	"fmt"
8
+	"log"
9
+	"os"
10
+	"path/filepath"
11
+	"strings"
12
+	"time"
13
+
14
+	"git.rootprojects.org/root/go-serviceman/installer"
15
+)
16
+
17
+var GitRev = "000000000"
18
+var GitVersion = "v0.0.0"
19
+var GitTimestamp = time.Now().Format(time.RFC3339)
20
+
21
+func main() {
22
+	conf := &installer.Config{
23
+		Restart: true,
24
+	}
25
+
26
+	args := []string{}
27
+	for i := range os.Args {
28
+		if "--" == os.Args[i] {
29
+			if len(os.Args) > i+1 {
30
+				args = os.Args[i+1:]
31
+			}
32
+			os.Args = os.Args[:i]
33
+			break
34
+		}
35
+	}
36
+	conf.Argv = args
37
+	conf.Args = strings.Join(conf.Argv, " ")
38
+
39
+	forUser := false
40
+	forSystem := false
41
+	flag.StringVar(&conf.Title, "title", "", "a human-friendly name for the service")
42
+	flag.StringVar(&conf.Desc, "desc", "", "a human-friendly description of the service (ex: Foo App)")
43
+	flag.StringVar(&conf.Name, "name", "", "a computer-friendly name for the service (ex: foo-app)")
44
+	flag.StringVar(&conf.URL, "url", "", "the documentation on home page of the service")
45
+	flag.StringVar(&conf.ReverseDNS, "rdns", "", "a plist-friendly Reverse DNS name for launchctl (ex: com.example.foo-app)")
46
+	flag.BoolVar(&forSystem, "system", false, "attempt to install system service as an unprivileged/unelevated user")
47
+	flag.BoolVar(&forUser, "user", false, "install user space / user mode service even when admin/root/sudo/elevated")
48
+	flag.StringVar(&conf.User, "username", "", "run the service as this user")
49
+	flag.StringVar(&conf.Group, "groupname", "", "run the service as this group")
50
+	flag.BoolVar(&conf.PrivilegedPorts, "cap-net-bind", false, "this service should have access to privileged ports")
51
+	flag.Parse()
52
+	args = flag.Args()
53
+
54
+	if forUser && forSystem {
55
+		fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
56
+		os.Exit(1)
57
+	}
58
+	if forUser {
59
+		conf.System = false
60
+	} else if forSystem {
61
+		conf.System = true
62
+	} else {
63
+		conf.System = installer.IsPrivileged()
64
+	}
65
+
66
+	n := len(args)
67
+	if 0 == n {
68
+		fmt.Println("Usage: serviceman install ./foo-app -- --foo-arg")
69
+		os.Exit(1)
70
+	}
71
+
72
+	execpath, err := installer.WhereIs(args[0])
73
+	if nil != err {
74
+		fmt.Fprintf(os.Stderr, "Error: '%s' could not be found.", args[0])
75
+		os.Exit(1)
76
+	}
77
+	args[0] = execpath
78
+	conf.Exec = args[0]
79
+	args = args[1:]
80
+
81
+	if n >= 2 {
82
+		conf.Interpreter = conf.Exec
83
+		conf.Exec = args[0]
84
+		conf.Argv = append(args[1:], conf.Argv...)
85
+	}
86
+
87
+	if "" == conf.Name {
88
+		ext := filepath.Ext(conf.Exec)
89
+		base := filepath.Base(conf.Exec[:len(conf.Exec)-len(ext)])
90
+		conf.Name = strings.ToLower(base)
91
+	}
92
+	if "" == conf.Title {
93
+		conf.Title = conf.Name
94
+	}
95
+	if "" == conf.ReverseDNS {
96
+		conf.ReverseDNS = "com.example." + conf.Name
97
+	}
98
+
99
+	fmt.Printf("\n%#v\n\n", conf)
100
+
101
+	err = installer.Install(conf)
102
+	if nil != err {
103
+		log.Fatal(err)
104
+	}
105
+}

+ 8
- 0
tools/tools.go View File

@@ -0,0 +1,8 @@
1
+// +build tools
2
+
3
+package tools
4
+
5
+import (
6
+	_ "git.rootprojects.org/root/go-gitver"
7
+	_ "github.com/UnnoTed/fileb0x"
8
+)

Loading…
Cancel
Save