Browse Source

better arg handling, more descriptive output

tags/v0.5.0
AJ ONeal 6 months ago
parent
commit
40a82f26c4

+ 136
- 49
README.md View File

@@ -9,11 +9,11 @@ Because debugging launchctl, systemd, etc absolutely sucks!
9 9
 
10 10
 ## Features
11 11
 
12
--   Unprivileged (User Mode) Services
12
+-   Unprivileged (User Mode) Services with `--user` (_Default_)
13 13
     -   [x] Linux (`sytemctl --user`)
14 14
     -   [x] MacOS (`launchctl`)
15 15
     -   [x] Windows (`HKEY_CURRENT_USER/.../Run`)
16
--   Privileged (System) Services
16
+-   Privileged (System) Services with `--system` (_Default_ for `root`)
17 17
     -   [x] Linux (`sudo sytemctl`)
18 18
     -   [x] MacOS (`sudo launchctl`)
19 19
     -   [ ] Windows (_not yet implemented_)
@@ -40,26 +40,17 @@ Because debugging launchctl, systemd, etc absolutely sucks!
40 40
 
41 41
 The basic pattern of usage:
42 42
 
43
-```
44
-serviceman add [options] [interpreter] <service> -- [service options]
45
-serviceman start <service>
46
-serviceman stop <service>
43
+```bash
44
+sudo serviceman add --name "foobar" [options] [interpreter] <service> [--] [service options]
45
+sudo serviceman start <service>
46
+sudo serviceman stop <service>
47 47
 serviceman version
48 48
 ```
49 49
 
50 50
 And what that might look like:
51 51
 
52
-```
53
-# Here the service is named "foo" implicitly
54
-# '--bar /baz' will be used for arguments to foo.exe in the service file
55
-serviceman add foo.exe -- --bar /baz
56
-```
57
-
58
-```
59
-# Here the service is named "foo-app" explicitly
60
-# 'node' will be found in the path
61
-# './index.js' will be resolved to a full path
62
-serviceman add --name "foo-app" node ./index.js
52
+```bash
53
+sudo serviceman add --name "foo" foo.exe -c ./config.json
63 54
 ```
64 55
 
65 56
 You can also view the help:
@@ -68,6 +59,14 @@ You can also view the help:
68 59
 serviceman add --help
69 60
 ```
70 61
 
62
+# System Services VS User Mode Services
63
+
64
+User services start **on login**.
65
+
66
+System services start **on boot**.
67
+
68
+The **default** is to register a _user_ services. To register a _system_ service, use `sudo` or run as `root`.
69
+
71 70
 # Install
72 71
 
73 72
 There are a number of pre-built binaries.
@@ -171,8 +170,8 @@ curl https://rootprojects.org/serviceman/dist/linux/armv5/serviceman -o servicem
171 170
 
172 171
 ```
173 172
 mkdir %userprofile%\bin
174
-reg add HKEY_CURRENT_USER\Environment /v PATH /d "%PATH%;%userprofile%\bin"
175 173
 move serviceman.exe %userprofile%\bin\serviceman.exe
174
+reg add HKEY_CURRENT_USER\Environment /v PATH /d "%PATH%;%userprofile%\bin"
176 175
 ```
177 176
 
178 177
 **All Others**
@@ -184,43 +183,100 @@ sudo mv ./serviceman /usr/local/bin/
184 183
 
185 184
 # Examples
186 185
 
187
-> **serviceman add** &lt;program> **--** &lt;program options>
186
+```bash
187
+sudo serviceman add --name <name> <program> [options] [--] [raw options]
188
+
189
+# Example
190
+sudo serviceman add --name "gizmo" gizmo --foo bar/baz
191
+```
192
+
193
+Anything that looks like file or directory will be **resolved to its absolute path**:
194
+
195
+```bash
196
+# Example of path resolution
197
+gizmo --foo /User/me/gizmo/bar/baz
198
+```
199
+
200
+Use `--` to prevent this behavior:
201
+
202
+```bash
203
+# Complex Example
204
+sudo serviceman add --name "gizmo" gizmo -c ./config.ini -- --separator .
205
+```
206
+
207
+For native **Windows** programs that use `/` for flags, you'll need to resolve some paths yourself:
208
+
209
+```bash
210
+# Windows Example
211
+serviceman add --name "gizmo" gizmo.exe .\input.txt -- /c \User\me\gizmo\config.ini /q /s .
212
+```
213
+
214
+In this case `./config.ini` would still be resolved (before `--`), but `.` would not (after `--`)
188 215
 
189 216
 <details>
190 217
 <summary>Compiled Programs</summary>
191 218
 
192 219
 Normally you might your program somewhat like this:
193 220
 
194
-```
195
-dinglehopper --port 8421
221
+```bash
222
+gizmo run --port 8421 --config envs/prod.ini
196 223
 ```
197 224
 
198 225
 Adding a service for that program with `serviceman` would look like this:
199 226
 
200
-> **serviceman add** dinglehopper **--** --port 8421
227
+```bash
228
+sudo serviceman add --name "gizmo" gizmo run --port 8421 --config envs/prod.ini
229
+```
201 230
 
202
-serviceman will find dinglehopper in your PATH.
231
+serviceman will find `gizmo` in your PATH and resolve `envs/prod.ini` to its absolute path.
203 232
 
204 233
 </details>
205 234
 
206 235
 <details>
207 236
 <summary>Using with scripts</summary>
208 237
 
238
+```bash
239
+./snarfblat.sh --port 8421
240
+```
241
+
209 242
 Although your text script may be executable, you'll need to specify the interpreter
210 243
 in order for `serviceman` to configure the service correctly.
211 244
 
212
-For example, if you had a bash script that you normally ran like this:
245
+This can be done in two ways:
246
+
247
+1. Put a **hashbang** in your script, such as `#!/bin/bash`.
248
+2. Prepend the **interpreter** explicitly to your command, such as `bash ./dinglehopper.sh`.
213 249
 
250
+For example, suppose you had a script like this:
251
+
252
+`iamok.sh`:
253
+
254
+```bash
255
+while true; do
256
+  sleep 1; echo "Still Alive, Still Alive!"
257
+done
214 258
 ```
215
-./snarfblat.sh --port 8421
259
+
260
+Normally you would run the script like this:
261
+
262
+```bash
263
+./imok.sh
216 264
 ```
217 265
 
218
-You'd create a system service for it like this:
266
+So you'd either need to modify the script to include a hashbang:
219 267
 
220
-> serviceman add **bash** ./snarfblat.sh **--** --port 8421
268
+```bash
269
+#!/usr/bin/env bash
270
+while true; do
271
+  sleep 1; echo "I'm Ok!"
272
+done
273
+```
221 274
 
222
-`serviceman` will resolve `./snarfblat.sh` correctly because it comes
223
-before the **--**.
275
+Or you'd need to prepend it with `bash` when creating a service for it:
276
+
277
+```bash
278
+sudo serviceman add --name "imok" bash ./imok.sh
279
+```
224 280
 
225 281
 **Background Information**
226 282
 
@@ -244,6 +300,8 @@ like this:
244 300
 #!/usr/local/bin/node --harmony --inspect
245 301
 ```
246 302
 
303
+Serviceman understands all 3 of those approaches.
304
+
247 305
 </details>
248 306
 
249 307
 <details>
@@ -252,14 +310,37 @@ like this:
252 310
 If normally you run your node script something like this:
253 311
 
254 312
 ```bash
255
-node ./demo.js --foo bar --baz
313
+pushd ~/my-node-project/
314
+npm start
315
+```
316
+
317
+Then you would add it as a system service like this:
318
+
319
+```bash
320
+sudo serviceman add npm start
321
+```
322
+
323
+If normally you run your node script something like this:
324
+
325
+```bash
326
+pushd ~/my-node-project/
327
+node ./serve.js --foo bar --baz
256 328
 ```
257 329
 
258 330
 Then you would add it as a system service like this:
259 331
 
260
-> **serviceman add** node ./demo.js **--** --foo bar --baz
332
+```bash
333
+sudo serviceman add node ./serve.js --foo bar --baz
334
+```
335
+
336
+It's important that any paths start with `./` and have the `.js`
337
+so that serviceman knows to resolve the full path.
261 338
 
262
-It is important that you specify `node ./demo.js` and not just `./demo.js`
339
+```bash
340
+# Bad Examples
341
+sudo serviceman add node ./demo # Wouldn't work for 'demo.js' - not a real filename
342
+sudo serviceman add node demo   # Wouldn't work for './demo/' - doesn't look like a directory
343
+```
263 344
 
264 345
 See **Using with scripts** for more detailed information.
265 346
 
@@ -271,14 +352,15 @@ See **Using with scripts** for more detailed information.
271 352
 If normally you run your python script something like this:
272 353
 
273 354
 ```bash
274
-python ./demo.py --foo bar --baz
355
+pushd ~/my-python-project/
356
+python ./serve.py --config ./config.ini
275 357
 ```
276 358
 
277 359
 Then you would add it as a system service like this:
278 360
 
279
-> **serviceman add** python ./demo.py **--** --foo bar --baz
280
-
281
-It is important that you specify `python ./demo.py` and not just `./demo.py`
361
+```bash
362
+sudo serviceman add python ./serve.py --config ./config.ini
363
+```
282 364
 
283 365
 See **Using with scripts** for more detailed information.
284 366
 
@@ -290,31 +372,32 @@ See **Using with scripts** for more detailed information.
290 372
 If normally you run your ruby script something like this:
291 373
 
292 374
 ```bash
293
-ruby ./demo.rb --foo bar --baz
375
+pushd ~/my-ruby-project/
376
+ruby ./serve.rb --config ./config.yaml
294 377
 ```
295 378
 
296 379
 Then you would add it as a system service like this:
297 380
 
298
-> **serviceman add** ruby ./demo.rb **--** --foo bar --baz
299
-
300
-It is important that you specify `ruby ./demo.rb` and not just `./demo.rb`
381
+```bash
382
+sudo serviceman add ruby ./serve.rb --config ./config.yaml
383
+```
301 384
 
302 385
 See **Using with scripts** for more detailed information.
303 386
 
304 387
 </details>
305 388
 
306
-## Relative vs Absolute Paths
389
+## Hints
307 390
 
308
-Although serviceman can expand the executable's path,
309
-if you have any arguments with relative paths
310
-you should switch to using absolute paths.
391
+-   If something goes wrong, read the output **completely** - it'll probably be helpful
392
+-   Run `serviceman` from your **project directory**, just as you would run it normally
393
+    -   Otherwise specify `--name <service-name>` and `--workdir <project directory>`
394
+-   Use `--` in front of arguments that should not be resolved as paths
395
+    -   This also holds true if you need `--` as an argument, such as `-- --foo -- --bar`
311 396
 
312 397
 ```
313
-dinglehopper --config ./conf.json
314
-```
315
-
316
-```
317
-serviceman add dinglehopper -- --config /Users/me/dinglehopper/conf.json
398
+# Example of a / that isn't a path
399
+# (it needs to be escaped with --)
400
+sudo serviceman add dinglehopper config/prod -- --category color/blue
318 401
 ```
319 402
 
320 403
 # Logging
@@ -323,6 +406,7 @@ serviceman add dinglehopper -- --config /Users/me/dinglehopper/conf.json
323 406
 
324 407
 ```bash
325 408
 sudo journalctl -xef --unit <NAME>
409
+sudo journalctl -xef --user-unit <NAME>
326 410
 ```
327 411
 
328 412
 ### Mac, Windows
@@ -354,6 +438,9 @@ why your app failed to start.
354 438
 
355 439
 # Debugging
356 440
 
441
+-   `serviceman add --dryrun <normal options>`
442
+-   `serviceman run --config <special config>`
443
+
357 444
 One of the most irritating problems with all of these launchers is that they're
358 445
 terrible to debug - it's often difficult to find the logs, and nearly impossible
359 446
 to interpret them, if they exist at all.

+ 6
- 6
manager/install.go View File

@@ -14,7 +14,7 @@ import (
14 14
 
15 15
 // Install will do a best-effort attempt to install a start-on-startup
16 16
 // user or system service via systemd, launchd, or reg.exe
17
-func Install(c *service.Service) error {
17
+func Install(c *service.Service) (string, error) {
18 18
 	if "" == c.Exec {
19 19
 		c.Exec = c.Name
20 20
 	}
@@ -24,23 +24,23 @@ func Install(c *service.Service) error {
24 24
 		if nil != err {
25 25
 			fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
26 26
 			os.Exit(4)
27
-			return err
27
+			return "", err
28 28
 		} else {
29 29
 			c.Home = home
30 30
 		}
31 31
 	}
32 32
 
33
-	err := install(c)
33
+	name, err := install(c)
34 34
 	if nil != err {
35
-		return err
35
+		return "", err
36 36
 	}
37 37
 
38 38
 	err = os.MkdirAll(c.Logdir, 0755)
39 39
 	if nil != err {
40
-		return err
40
+		return "", err
41 41
 	}
42 42
 
43
-	return nil
43
+	return name, nil
44 44
 }
45 45
 
46 46
 func Start(conf *service.Service) error {

+ 31
- 24
manager/install_darwin.go View File

@@ -50,12 +50,11 @@ func start(conf *service.Service) error {
50 50
 
51 51
 	cmds = adjustPrivs(system, cmds)
52 52
 
53
-	fmt.Println()
54 53
 	typ := "USER"
55 54
 	if system {
56 55
 		typ = "SYSTEM"
57 56
 	}
58
-	fmt.Printf("Starting launchd %s service...\n", typ)
57
+	fmt.Printf("Starting launchd %s service...\n\n", typ)
59 58
 	for i := range cmds {
60 59
 		exe := cmds[i]
61 60
 		fmt.Println("\t" + exe.String())
@@ -109,11 +108,32 @@ func stop(conf *service.Service) error {
109 108
 	return nil
110 109
 }
111 110
 
112
-func install(c *service.Service) error {
111
+func Render(c *service.Service) ([]byte, error) {
112
+	// Create service file from template
113
+	b, err := static.ReadFile("dist/Library/LaunchDaemons/_rdns_.plist.tmpl")
114
+	if err != nil {
115
+		return nil, err
116
+	}
117
+	s := string(b)
118
+	rw := &bytes.Buffer{}
119
+	// not sure what the template name does, but whatever
120
+	tmpl, err := template.New("service").Parse(s)
121
+	if err != nil {
122
+		return nil, err
123
+	}
124
+	err = tmpl.Execute(rw, c)
125
+	if nil != err {
126
+		return nil, err
127
+	}
128
+
129
+	return rw.Bytes(), nil
130
+}
131
+
132
+func install(c *service.Service) (string, error) {
113 133
 	// Darwin-specific config options
114 134
 	if c.PrivilegedPorts {
115 135
 		if !c.System {
116
-			return fmt.Errorf("You must use root-owned LaunchDaemons (not user-owned LaunchAgents) to use priveleged ports on OS X")
136
+			return "", fmt.Errorf("You must use root-owned LaunchDaemons (not user-owned LaunchAgents) to use priveleged ports on OS X")
117 137
 		}
118 138
 	}
119 139
 	plistDir := srvSysPath
@@ -124,32 +144,20 @@ func install(c *service.Service) error {
124 144
 	// Check paths first
125 145
 	err := os.MkdirAll(filepath.Dir(plistDir), 0755)
126 146
 	if nil != err {
127
-		return err
147
+		return "", err
128 148
 	}
129 149
 
130
-	// Create service file from template
131
-	b, err := static.ReadFile("dist/Library/LaunchDaemons/_rdns_.plist.tmpl")
132
-	if err != nil {
133
-		return err
134
-	}
135
-	s := string(b)
136
-	rw := &bytes.Buffer{}
137
-	// not sure what the template name does, but whatever
138
-	tmpl, err := template.New("service").Parse(s)
139
-	if err != nil {
140
-		return err
141
-	}
142
-	err = tmpl.Execute(rw, c)
150
+	b, err := Render(c)
143 151
 	if nil != err {
144
-		return err
152
+		return "", err
145 153
 	}
146 154
 
147 155
 	// Write the file out
148 156
 	// TODO rdns
149 157
 	plistName := c.ReverseDNS + ".plist"
150 158
 	plistPath := filepath.Join(plistDir, plistName)
151
-	if err := ioutil.WriteFile(plistPath, rw.Bytes(), 0644); err != nil {
152
-		return fmt.Errorf("Error writing %s: %v", plistPath, err)
159
+	if err := ioutil.WriteFile(plistPath, b, 0644); err != nil {
160
+		return "", fmt.Errorf("Error writing %s: %v", plistPath, err)
153 161
 	}
154 162
 
155 163
 	// TODO --no-start
@@ -158,9 +166,8 @@ func install(c *service.Service) error {
158 166
 		fmt.Printf("If things don't go well you should be able to get additional logging from launchctl:\n")
159 167
 		fmt.Printf("\tsudo launchctl log level debug\n")
160 168
 		fmt.Printf("\ttail -f /var/log/system.log\n")
161
-		return err
169
+		return "", err
162 170
 	}
163 171
 
164
-	fmt.Printf("Added and started '%s' as a launchctl service.\n", c.Name)
165
-	return nil
172
+	return "launchd", nil
166 173
 }

+ 30
- 23
manager/install_linux.go View File

@@ -88,12 +88,11 @@ func start(conf *service.Service) error {
88 88
 
89 89
 	cmds = adjustPrivs(system, cmds)
90 90
 
91
-	fmt.Println()
92 91
 	typ := "USER MODE"
93 92
 	if system {
94 93
 		typ = "SYSTEM"
95 94
 	}
96
-	fmt.Printf("Starting systemd %s service unit...\n", typ)
95
+	fmt.Printf("Starting systemd %s service unit...\n\n", typ)
97 96
 	for i := range cmds {
98 97
 		exe := cmds[i]
99 98
 		fmt.Println("\t" + exe.String())
@@ -160,7 +159,28 @@ func stop(conf *service.Service) error {
160 159
 	return nil
161 160
 }
162 161
 
163
-func install(c *service.Service) error {
162
+func Render(c *service.Service) ([]byte, error) {
163
+	// Create service file from template
164
+	b, err := static.ReadFile("dist/etc/systemd/system/_name_.service.tmpl")
165
+	if err != nil {
166
+		return nil, err
167
+	}
168
+	s := string(b)
169
+	rw := &bytes.Buffer{}
170
+	// not sure what the template name does, but whatever
171
+	tmpl, err := template.New("service").Parse(s)
172
+	if err != nil {
173
+		return nil, err
174
+	}
175
+	err = tmpl.Execute(rw, c)
176
+	if nil != err {
177
+		return nil, err
178
+	}
179
+
180
+	return rw.Bytes(), nil
181
+}
182
+
183
+func install(c *service.Service) (string, error) {
164 184
 	// Linux-specific config options
165 185
 	if c.System {
166 186
 		if "" == c.User {
@@ -177,32 +197,20 @@ func install(c *service.Service) error {
177 197
 		serviceDir = filepath.Join(c.Home, srvUserPath)
178 198
 		err := os.MkdirAll(serviceDir, 0755)
179 199
 		if nil != err {
180
-			return err
200
+			return "", err
181 201
 		}
182 202
 	}
183 203
 
184
-	// Create service file from template
185
-	b, err := static.ReadFile("dist/etc/systemd/system/_name_.service.tmpl")
186
-	if err != nil {
187
-		return err
188
-	}
189
-	s := string(b)
190
-	rw := &bytes.Buffer{}
191
-	// not sure what the template name does, but whatever
192
-	tmpl, err := template.New("service").Parse(s)
193
-	if err != nil {
194
-		return err
195
-	}
196
-	err = tmpl.Execute(rw, c)
204
+	b, err := Render(c)
197 205
 	if nil != err {
198
-		return err
206
+		return "", err
199 207
 	}
200 208
 
201 209
 	// Write the file out
202 210
 	serviceName := c.Name + ".service"
203 211
 	servicePath := filepath.Join(serviceDir, serviceName)
204
-	if err := ioutil.WriteFile(servicePath, rw.Bytes(), 0644); err != nil {
205
-		return fmt.Errorf("Error writing %s: %v", servicePath, err)
212
+	if err := ioutil.WriteFile(servicePath, b, 0644); err != nil {
213
+		return "", fmt.Errorf("Error writing %s: %v", servicePath, err)
206 214
 	}
207 215
 
208 216
 	// TODO --no-start
@@ -217,9 +225,8 @@ func install(c *service.Service) error {
217 225
 		}
218 226
 		fmt.Printf("If things don't go well you should be able to get additional logging from journalctl:\n")
219 227
 		fmt.Printf("\t%sjournalctl -xe %s %s.service\n", sudo, unit, c.Name)
220
-		return err
228
+		return "", err
221 229
 	}
222 230
 
223
-	fmt.Printf("Added and started '%s' as a systemd service.\n", c.Name)
224
-	return nil
231
+	return "systemd", nil
225 232
 }

+ 4
- 0
manager/install_other.go View File

@@ -6,6 +6,10 @@ import (
6 6
 	"git.rootprojects.org/root/go-serviceman/service"
7 7
 )
8 8
 
9
+func Render(c *service.Service) ([]byte, error) {
10
+	return nil, nil
11
+}
12
+
9 13
 func install(c *service.Service) error {
10 14
 	return nil, nil
11 15
 }

+ 16
- 11
manager/install_windows.go View File

@@ -30,7 +30,7 @@ func init() {
30 30
 
31 31
 // TODO system service requires elevated privileges
32 32
 // See https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
33
-func install(c *service.Service) error {
33
+func install(c *service.Service) (string, error) {
34 34
 	/*
35 35
 		// LEAVE THIS DOCUMENTATION HERE
36 36
 		reg.exe
@@ -73,7 +73,7 @@ func install(c *service.Service) error {
73 73
 
74 74
 	args, err := installServiceman(c)
75 75
 	if nil != err {
76
-		return err
76
+		return "", err
77 77
 	}
78 78
 
79 79
 	/*
@@ -100,7 +100,7 @@ func install(c *service.Service) error {
100 100
 
101 101
 	regSZ := fmt.Sprintf(`"%s" %s`, args[0], strings.Join(args[1:], " "))
102 102
 	if len(regSZ) > 260 {
103
-		return fmt.Errorf("data value is too long for registry entry")
103
+		return "", fmt.Errorf("data value is too long for registry entry")
104 104
 	}
105 105
 	// In order for a windows gui program to not show a console,
106 106
 	// it has to not output any messages?
@@ -108,17 +108,22 @@ func install(c *service.Service) error {
108 108
 	//fmt.Println(autorunKey, c.Title, regSZ)
109 109
 	k.SetStringValue(c.Title, regSZ)
110 110
 
111
-	// to return ErrDaemonize
112
-	return start(c)
111
+	err = start(c)
112
+	return "serviceman", err
113
+}
114
+
115
+func Render(c *service.Service) ([]byte, error) {
116
+	b, err := json.Marshal(c)
117
+	if nil != err {
118
+		return nil, err
119
+	}
120
+	return b, nil
113 121
 }
114 122
 
115 123
 func start(conf *service.Service) error {
116 124
 	args := getRunnerArgs(conf)
117
-	return &ErrDaemonize{
118
-		DaemonArgs: append(args, "--daemon"),
119
-		error:      "Not as much an error as a bad value...",
120
-	}
121
-	//return runner.Start(conf)
125
+	args = append(args, "--daemon")
126
+	return Run(args[0], args[1:]...)
122 127
 }
123 128
 
124 129
 func stop(conf *service.Service) error {
@@ -173,7 +178,7 @@ func installServiceman(c *service.Service) ([]string, error) {
173 178
 		}
174 179
 	}
175 180
 
176
-	b, err := json.Marshal(c)
181
+	b, err := Render(c)
177 182
 	if nil != err {
178 183
 		// this should be impossible, so we'll just panic
179 184
 		panic(err)

+ 18
- 0
manager/start.go View File

@@ -227,3 +227,21 @@ func adjustPrivs(system bool, cmds []Runnable) []Runnable {
227 227
 
228 228
 	return cmds
229 229
 }
230
+
231
+func Run(bin string, args ...string) error {
232
+	cmd := exec.Command(bin, args...)
233
+	// for debugging
234
+	/*
235
+		out, err := cmd.CombinedOutput()
236
+		if nil != err {
237
+			fmt.Println(err)
238
+		}
239
+		fmt.Println(string(out))
240
+	*/
241
+
242
+	err := cmd.Start()
243
+	if nil != err {
244
+		return err
245
+	}
246
+	return nil
247
+}

+ 1
- 1
service/service.go View File

@@ -116,7 +116,7 @@ func (s *Service) Normalize(force bool) {
116 116
 			_, err := os.Stat(optpath)
117 117
 			if nil == err {
118 118
 				bad = false
119
-				fmt.Fprintf(os.Stderr, "Using '%s' for '%s'\n", optpath, s.Exec)
119
+				//fmt.Fprintf(os.Stderr, "Using '%s' for '%s'\n", optpath, s.Exec)
120 120
 				s.Exec = optpath
121 121
 			}
122 122
 		}

+ 311
- 68
serviceman.go View File

@@ -1,5 +1,6 @@
1 1
 //go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
2 2
 
3
+// main runs the things and does the stuff
3 4
 package main
4 5
 
5 6
 import (
@@ -9,8 +10,11 @@ import (
9 10
 	"io/ioutil"
10 11
 	"os"
11 12
 	"os/exec"
13
+	"os/user"
14
+	"path/filepath"
12 15
 	"strings"
13 16
 	"time"
17
+	"unicode/utf8"
14 18
 
15 19
 	"git.rootprojects.org/root/go-serviceman/manager"
16 20
 	"git.rootprojects.org/root/go-serviceman/runner"
@@ -62,26 +66,15 @@ func add() {
62 66
 		Restart: true,
63 67
 	}
64 68
 
65
-	args := []string{}
66
-	for i := range os.Args {
67
-		if "--" == os.Args[i] {
68
-			if len(os.Args) > i+1 {
69
-				args = os.Args[i+1:]
70
-			}
71
-			os.Args = os.Args[:i]
72
-			break
73
-		}
74
-	}
75
-	conf.Argv = args
76
-
77 69
 	force := false
78 70
 	forUser := false
79 71
 	forSystem := false
72
+	dryrun := false
80 73
 	flag.StringVar(&conf.Title, "title", "", "a human-friendly name for the service")
81 74
 	flag.StringVar(&conf.Desc, "desc", "", "a human-friendly description of the service (ex: Foo App)")
82 75
 	flag.StringVar(&conf.Name, "name", "", "a computer-friendly name for the service (ex: foo-app)")
83 76
 	flag.StringVar(&conf.URL, "url", "", "the documentation on home page of the service")
84
-	//flag.StringVar(&conf.Workdir, "workdir", "", "the directory in which the service should be started")
77
+	flag.StringVar(&conf.Workdir, "workdir", "", "the directory in which the service should be started (if supported)")
85 78
 	flag.StringVar(&conf.ReverseDNS, "rdns", "", "a plist-friendly Reverse DNS name for launchctl (ex: com.example.foo-app)")
86 79
 	flag.BoolVar(&forSystem, "system", false, "attempt to add system service as an unprivileged/unelevated user")
87 80
 	flag.BoolVar(&forUser, "user", false, "add user space / user mode service even when admin/root/sudo/elevated")
@@ -89,67 +82,339 @@ func add() {
89 82
 	flag.StringVar(&conf.User, "username", "", "run the service as this user")
90 83
 	flag.StringVar(&conf.Group, "groupname", "", "run the service as this group")
91 84
 	flag.BoolVar(&conf.PrivilegedPorts, "cap-net-bind", false, "this service should have access to privileged ports")
85
+	flag.BoolVar(&dryrun, "dryrun", false, "output the service file without modifying anything on disk")
86
+	flag.Usage = func() {
87
+		fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0])
88
+
89
+		flag.PrintDefaults()
90
+
91
+		fmt.Fprintf(os.Stderr, "Flags and arguments after \"--\" will be completely ignored by serviceman\n", os.Args[0])
92
+	}
92 93
 	flag.Parse()
93
-	args = flag.Args()
94
+	flagargs := flag.Args()
95
+
96
+	// You must have something to run, duh
97
+	n := len(flagargs)
98
+	if 0 == n {
99
+		fmt.Println("Usage: serviceman add ./foo-app --foo-arg")
100
+		os.Exit(2)
101
+		return
102
+	}
94 103
 
95 104
 	if forUser && forSystem {
96 105
 		fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
97 106
 		os.Exit(1)
98 107
 		return
99 108
 	}
109
+
110
+	// There are three groups of flags
111
+	// serviceman --flag1 arg1 non-flag-arg --child1 -- --raw1 -- --raw2
112
+	//  serviceman --flag1 arg1   // these belong to serviceman
113
+	//  non-flag-arg --child1     // these will be interpretted
114
+	//  --                        // separator
115
+	//  --raw1 -- --raw2          // after the separater (including additional separators) will be ignored
116
+	rawargs := []string{}
117
+	for i := range flagargs {
118
+		if "--" == flagargs[i] {
119
+			if len(flagargs) > i+1 {
120
+				rawargs = flagargs[i+1:]
121
+			}
122
+			flagargs = flagargs[:i]
123
+			break
124
+		}
125
+	}
126
+
127
+	// Assumptions
128
+	ass := []string{}
100 129
 	if forUser {
101 130
 		conf.System = false
102 131
 	} else if forSystem {
103 132
 		conf.System = true
104 133
 	} else {
105 134
 		conf.System = manager.IsPrivileged()
135
+		if conf.System {
136
+			ass = append(ass, "# Because you're a privileged user")
137
+			ass = append(ass, "  --system")
138
+			ass = append(ass, "")
139
+		} else {
140
+			ass = append(ass, "# Because you're a unprivileged user")
141
+			ass = append(ass, "  --user")
142
+			ass = append(ass, "")
143
+		}
144
+	}
145
+	if "" == conf.Workdir {
146
+		dir, _ := os.Getwd()
147
+		conf.Workdir = dir
148
+		ass = append(ass, "# Because this is your current working directory")
149
+		ass = append(ass, fmt.Sprintf("  --workdir %s", conf.Workdir))
150
+		ass = append(ass, "")
151
+	}
152
+	if "" == conf.Name {
153
+		name, _ := os.Getwd()
154
+		base := filepath.Base(name)
155
+		ext := filepath.Ext(base)
156
+		n := (len(base) - len(ext))
157
+		name = base[:n]
158
+		if "" == name {
159
+			name = base
160
+		}
161
+		conf.Name = name
162
+		ass = append(ass, "# Because this is the name of your current working directory")
163
+		ass = append(ass, fmt.Sprintf("  --name %s", conf.Name))
164
+		ass = append(ass, "")
106 165
 	}
107 166
 
108
-	n := len(args)
109
-	if 0 == n {
110
-		fmt.Println("Usage: serviceman add ./foo-app -- --foo-arg")
111
-		os.Exit(2)
167
+	exepath, err := findExec(flagargs[0], force)
168
+	if nil != err {
169
+		fmt.Fprintf(os.Stderr, "%s\n", err)
170
+		os.Exit(3)
112 171
 		return
113 172
 	}
173
+	flagargs[0] = exepath
114 174
 
115
-	execpath, err := manager.WhereIs(args[0])
175
+	exeargs, err := testScript(flagargs[0], force)
116 176
 	if nil != err {
117
-		fmt.Fprintf(os.Stderr, "Error: '%s' could not be found in PATH or working directory.\n", args[0])
118
-		if !force {
119
-			os.Exit(3)
120
-			return
121
-		}
122
-	} else {
123
-		args[0] = execpath
177
+		fmt.Fprintf(os.Stderr, "%s\n", err)
178
+		os.Exit(3)
179
+		return
124 180
 	}
125
-	conf.Exec = args[0]
126
-	args = args[1:]
127 181
 
128
-	if n >= 2 {
129
-		conf.Interpreter = conf.Exec
130
-		conf.Exec = args[0]
131
-		conf.Argv = append(args[1:], conf.Argv...)
182
+	flagargs = append(exeargs, flagargs...)
183
+	// TODO
184
+	for i := range flagargs {
185
+		arg := flagargs[i]
186
+		arg = filepath.ToSlash(arg)
187
+		// Paths considered to be anything starting with ./, .\, /, \, C:
188
+		if "." == arg || strings.Contains(arg, "/") {
189
+			//if "." == arg || (len(arg) >= 2 && "./" == arg[:2] || '/' == arg[0] || "C:" == strings.ToUpper(arg[:1])) {
190
+			var err error
191
+			arg, err = filepath.Abs(arg)
192
+			if nil == err {
193
+				_, err = os.Stat(arg)
194
+			}
195
+			if nil != err {
196
+				fmt.Printf("%q appears to be a file path, but %q could not be read\n", flagargs[i], arg)
197
+				if !force {
198
+					os.Exit(7)
199
+					return
200
+				}
201
+				continue
202
+			}
203
+
204
+			if '\\' != os.PathSeparator {
205
+				// Convert paths back to .\ for Windows
206
+				arg = filepath.FromSlash(arg)
207
+			}
208
+
209
+			// Lookin' good
210
+			flagargs[i] = arg
211
+		}
132 212
 	}
133 213
 
134
-	conf.Normalize(force)
214
+	// We won't bother with Interpreter here
215
+	// (it's really just for documentation),
216
+	// but we will add any and all unchecked args to the full slice
217
+	conf.Exec = flagargs[0]
218
+	conf.Argv = append(flagargs[1:], rawargs...)
219
+
220
+	// TODO update docs: go to the work directory
221
+	// TODO test with "npm start"
222
+
223
+	conf.NormalizeWithoutPath()
135 224
 
136 225
 	//fmt.Printf("\n%#v\n\n", conf)
137 226
 	if conf.System && !manager.IsPrivileged() {
138 227
 		fmt.Fprintf(os.Stderr, "Warning: You may need to use 'sudo' to add %q as a privileged system service.\n", conf.Name)
139 228
 	}
140 229
 
141
-	err = manager.Install(conf)
142
-	switch e := err.(type) {
143
-	case nil:
144
-		// do nothing
145
-	case *manager.ErrDaemonize:
146
-		runAsDaemon(e.DaemonArgs[0], e.DaemonArgs[1:]...)
147
-	default:
230
+	if len(ass) > 0 {
231
+		fmt.Println("OPTIONS: Making some assumptions...\n")
232
+		for i := range ass {
233
+			fmt.Println("\t" + ass[i])
234
+		}
235
+	}
236
+
237
+	// Find who this is running as
238
+	// And pretty print the command to run
239
+	runAs := conf.User
240
+	var wasflag bool
241
+	fmt.Printf("COMMAND: Service %q will be run like this (more or less):\n\n", conf.Title)
242
+	if conf.System {
243
+		if "" == runAs {
244
+			runAs = "root"
245
+		}
246
+		fmt.Printf("\t# Starts on system boot, as %q\n", runAs)
247
+	} else {
248
+		u, _ := user.Current()
249
+		runAs = u.Name
250
+		if "" == runAs {
251
+			runAs = u.Username
252
+		}
253
+		fmt.Printf("\t# Starts as %q, when %q logs in\n", runAs, u.Username)
254
+	}
255
+	//fmt.Printf("\tpushd %s\n", conf.Workdir)
256
+	fmt.Printf("\t%s\n", conf.Exec)
257
+	for i := range conf.Argv {
258
+		arg := conf.Argv[i]
259
+		if '-' == arg[0] {
260
+			if wasflag {
261
+				fmt.Println()
262
+			}
263
+			wasflag = true
264
+			fmt.Printf("\t\t%s", arg)
265
+		} else {
266
+			if wasflag {
267
+				fmt.Printf(" %s\n", arg)
268
+			} else {
269
+				fmt.Printf("\t\t%s\n", arg)
270
+			}
271
+			wasflag = false
272
+		}
273
+	}
274
+	if wasflag {
275
+		fmt.Println()
276
+	}
277
+	fmt.Println()
278
+
279
+	// TODO output config without installing
280
+	if dryrun {
281
+		b, err := manager.Render(conf)
282
+		if nil != err {
283
+			fmt.Fprintf(os.Stderr, "Error rendering: %s\n", err)
284
+			os.Exit(10)
285
+		}
286
+		fmt.Println(string(b))
287
+		return
288
+	}
289
+
290
+	fmt.Printf("LAUNCHER: ")
291
+	servicetype, err := manager.Install(conf)
292
+	if nil != err {
148 293
 		fmt.Fprintf(os.Stderr, "%s\n", err)
294
+		os.Exit(500)
295
+		return
149 296
 	}
150 297
 
298
+	fmt.Printf("LOGS: ")
151 299
 	printLogMessage(conf)
152 300
 	fmt.Println()
301
+
302
+	servicemode := "USER MODE"
303
+	if conf.System {
304
+		servicemode = "SYSTEM"
305
+	}
306
+	fmt.Printf(
307
+		"SUCCESS:\n\n\t%q started as a %q %s service, running as %q\n",
308
+		conf.Name,
309
+		servicetype,
310
+		servicemode,
311
+		runAs,
312
+	)
313
+	fmt.Println()
314
+}
315
+
316
+func findExec(exe string, force bool) (string, error) {
317
+	// ex: node => /usr/local/bin/node
318
+	// ex: ./demo.js => /Users/aj/project/demo.js
319
+	exepath, err := exec.LookPath(exe)
320
+	if nil != err {
321
+		var msg string
322
+		if strings.Contains(filepath.ToSlash(exe), "/") {
323
+			if _, err := os.Stat(exe); err != nil {
324
+				msg = fmt.Sprintf("Error: '%s' could not be found in PATH or working directory.\n", exe)
325
+			} else {
326
+				msg = fmt.Sprintf("Error: '%s' is not an executable.\nYou may be able to fix that. Try running this:\n\tchmod a+x %s\n", exe, exe)
327
+			}
328
+		} else {
329
+			if _, err := os.Stat(exe); err != nil {
330
+				msg = fmt.Sprintf("Error: '%s' could not be found in PATH", exe)
331
+			} else {
332
+				msg = fmt.Sprintf("Error: '%s' could not be found in PATH, did you mean './%s'?\n", exe, exe)
333
+			}
334
+		}
335
+		if !force {
336
+			return "", fmt.Errorf(msg)
337
+		}
338
+		fmt.Fprintf(os.Stderr, "%s\n", msg)
339
+		return exe, nil
340
+	}
341
+
342
+	// ex: \Users\aj\project\demo.js => /Users/aj/project/demo.js
343
+	// Can't have an error here when lookpath succeeded
344
+	exepath, _ = filepath.Abs(filepath.ToSlash(exepath))
345
+	return exepath, nil
346
+}
347
+
348
+func testScript(exepath string, force bool) ([]string, error) {
349
+	f, err := os.Open(exepath)
350
+	b := make([]byte, 256)
351
+	if nil == err {
352
+		_, err = f.Read(b)
353
+	}
354
+	if nil != err || len(b) < len("#!/x") {
355
+		msg := fmt.Sprintf("Error when testing if '%s' is a binary or script: could not read file: %s\n", exepath, err)
356
+		if !force {
357
+			return nil, fmt.Errorf(msg)
358
+		}
359
+		fmt.Fprintf(os.Stderr, "%s\n", msg)
360
+		return nil, nil
361
+	}
362
+
363
+	// Nott sure if this is more readable and idiomatic as if else or switch
364
+	// However, the order matters
365
+	switch {
366
+	case utf8.Valid(b):
367
+		// Looks like an executable script
368
+		if "#!/" == string(b[:3]) {
369
+			break
370
+		}
371
+
372
+		msg := fmt.Sprintf("Error: %q looks like a script, but we don't know the interpreter.\nYou can probably fix this by...\n"+
373
+			"\tExplicitly naming the interpreter (ex: 'python my-script.py' instead of just 'my-script.py')\n"+
374
+			"\tPlacing a hashbang at the top of the script (ex: '#!/usr/bin/env python')", exepath)
375
+
376
+		if !force {
377
+			return nil, fmt.Errorf(msg)
378
+		}
379
+		return nil, nil
380
+	case "#!/" != string(b[:3]):
381
+		// Looks like a normal binary
382
+		return nil, nil
383
+	default:
384
+		// Looks like a corrupt script file
385
+		msg := "Error: It looks like you've specified a corrupt script file."
386
+		if !force {
387
+			return nil, fmt.Errorf(msg)
388
+		}
389
+		return nil, nil
390
+	}
391
+
392
+	// Deal with #!/whatever
393
+
394
+	// Get that first line
395
+	// "#!/usr/bin/env node" => ["/usr/bin/env", "node"]
396
+	// "#!/usr/bin/node --harmony => ["/usr/bin/node", "--harmony"]
397
+	s := string(b[2:]) // strip leading #!
398
+	s = strings.Split(strings.Replace(s, "\r\n", "\n", -1), "\n")[0]
399
+	allargs := strings.Split(strings.TrimSpace(s), " ")
400
+	args := []string{}
401
+	for i := range allargs {
402
+		arg := strings.TrimSpace(allargs[i])
403
+		if "" != arg {
404
+			args = append(args, arg)
405
+		}
406
+	}
407
+	if strings.HasSuffix(args[0], "/env") && len(args) > 1 {
408
+		// TODO warn that "env" is probably not an executable if 1 = len(args)?
409
+		args = args[1:]
410
+	}
411
+	exepath, err = findExec(args[0], force)
412
+	if nil != err {
413
+		return nil, err
414
+	}
415
+	args[0] = exepath
416
+
417
+	return args, nil
153 418
 }
154 419
 
155 420
 func start() {
@@ -185,14 +450,10 @@ func start() {
185 450
 	conf.NormalizeWithoutPath()
186 451
 
187 452
 	err := manager.Start(conf)
188
-	switch e := err.(type) {
189
-	case nil:
190
-		// do nothing
191
-	case *manager.ErrDaemonize:
192
-		runAsDaemon(e.DaemonArgs[0], e.DaemonArgs[1:]...)
193
-	default:
194
-		fmt.Println(err)
195
-		os.Exit(127)
453
+	if nil != err {
454
+		fmt.Fprintf(os.Stderr, "%s\n", err)
455
+		os.Exit(500)
456
+		return
196 457
 	}
197 458
 }
198 459
 
@@ -242,7 +503,7 @@ func run() {
242 503
 	flag.Parse()
243 504
 
244 505
 	if "" == confpath {
245
-		fmt.Fprintf(os.Stderr, "%s", strings.Join(flag.Args(), " "))
506
+		fmt.Fprintf(os.Stderr, "%s\n", strings.Join(flag.Args(), " "))
246 507
 		fmt.Fprintf(os.Stderr, "--config /path/to/config.json is required\n")
247 508
 		usage()
248 509
 		os.Exit(1)
@@ -295,23 +556,5 @@ func run() {
295 556
 		return
296 557
 	}
297 558
 
298
-	runAsDaemon(os.Args[0], "run", "--config", confpath)
299
-}
300
-
301
-func runAsDaemon(bin string, args ...string) {
302
-	cmd := exec.Command(bin, args...)
303
-	// for debugging
304
-	/*
305
-		out, err := cmd.CombinedOutput()
306
-		if nil != err {
307
-			fmt.Println(err)
308
-		}
309
-		fmt.Println(string(out))
310
-	*/
311
-
312
-	err := cmd.Start()
313
-	if nil != err {
314
-		fmt.Fprintf(os.Stderr, "%s\n", err)
315
-		os.Exit(500)
316
-	}
559
+	manager.Run(os.Args[0], "run", "--config", confpath)
317 560
 }

+ 1
- 1
serviceman_darwin.go View File

@@ -7,5 +7,5 @@ import (
7 7
 )
8 8
 
9 9
 func printLogMessage(conf *service.Service) {
10
-	fmt.Printf("If all went well the logs should have been created at:\n\t%s\n", conf.Logdir)
10
+	fmt.Printf("If all went well the logs should have been created at:\n\n\t%s\n", conf.Logdir)
11 11
 }

+ 1
- 1
serviceman_linux.go View File

@@ -17,7 +17,7 @@ func printLogMessage(conf *service.Service) {
17 17
 	} else {
18 18
 		unit = "--user-unit"
19 19
 	}
20
-	fmt.Println("If all went well you should be able to see some goodies in the logs:")
20
+	fmt.Println("If all went well you should be able to see some goodies in the logs:\n")
21 21
 	fmt.Printf("\t%sjournalctl -xe %s %s.service\n", sudo, unit, conf.Name)
22 22
 	if !conf.System {
23 23
 		fmt.Println("\nIf that's not the case, see https://unix.stackexchange.com/a/486566/45554.")

+ 1
- 1
serviceman_windows.go View File

@@ -7,5 +7,5 @@ import (
7 7
 )
8 8
 
9 9
 func printLogMessage(conf *service.Service) {
10
-	fmt.Printf("If all went well the logs should have been created at:\n\t%s\n", conf.Logdir)
10
+	fmt.Printf("If all went well the logs should have been created at:\n\n\t%s\n", conf.Logdir)
11 11
 }

Loading…
Cancel
Save