Browse Source

better arg handling, more descriptive output

tags/v0.5.0
AJ ONeal 8 months ago
parent
commit
40a82f26c4
12 changed files with 556 additions and 185 deletions
  1. +136
    -49
      README.md
  2. +6
    -6
      manager/install.go
  3. +31
    -24
      manager/install_darwin.go
  4. +30
    -23
      manager/install_linux.go
  5. +4
    -0
      manager/install_other.go
  6. +16
    -11
      manager/install_windows.go
  7. +18
    -0
      manager/start.go
  8. +1
    -1
      service/service.go
  9. +311
    -68
      serviceman.go
  10. +1
    -1
      serviceman_darwin.go
  11. +1
    -1
      serviceman_linux.go
  12. +1
    -1
      serviceman_windows.go

+ 136
- 49
README.md View File

@@ -9,11 +9,11 @@ Because debugging launchctl, systemd, etc absolutely sucks!

## Features

- Unprivileged (User Mode) Services
- Unprivileged (User Mode) Services with `--user` (_Default_)
- [x] Linux (`sytemctl --user`)
- [x] MacOS (`launchctl`)
- [x] Windows (`HKEY_CURRENT_USER/.../Run`)
- Privileged (System) Services
- Privileged (System) Services with `--system` (_Default_ for `root`)
- [x] Linux (`sudo sytemctl`)
- [x] MacOS (`sudo launchctl`)
- [ ] Windows (_not yet implemented_)
@@ -40,26 +40,17 @@ Because debugging launchctl, systemd, etc absolutely sucks!

The basic pattern of usage:

```
serviceman add [options] [interpreter] <service> -- [service options]
serviceman start <service>
serviceman stop <service>
```bash
sudo serviceman add --name "foobar" [options] [interpreter] <service> [--] [service options]
sudo serviceman start <service>
sudo serviceman stop <service>
serviceman version
```

And what that might look like:

```
# Here the service is named "foo" implicitly
# '--bar /baz' will be used for arguments to foo.exe in the service file
serviceman add foo.exe -- --bar /baz
```

```
# Here the service is named "foo-app" explicitly
# 'node' will be found in the path
# './index.js' will be resolved to a full path
serviceman add --name "foo-app" node ./index.js
```bash
sudo serviceman add --name "foo" foo.exe -c ./config.json
```

You can also view the help:
@@ -68,6 +59,14 @@ You can also view the help:
serviceman add --help
```

# System Services VS User Mode Services

User services start **on login**.

System services start **on boot**.

The **default** is to register a _user_ services. To register a _system_ service, use `sudo` or run as `root`.

# Install

There are a number of pre-built binaries.
@@ -171,8 +170,8 @@ curl https://rootprojects.org/serviceman/dist/linux/armv5/serviceman -o servicem

```
mkdir %userprofile%\bin
reg add HKEY_CURRENT_USER\Environment /v PATH /d "%PATH%;%userprofile%\bin"
move serviceman.exe %userprofile%\bin\serviceman.exe
reg add HKEY_CURRENT_USER\Environment /v PATH /d "%PATH%;%userprofile%\bin"
```

**All Others**
@@ -184,43 +183,100 @@ sudo mv ./serviceman /usr/local/bin/

# Examples

> **serviceman add** &lt;program> **--** &lt;program options>
```bash
sudo serviceman add --name <name> <program> [options] [--] [raw options]

# Example
sudo serviceman add --name "gizmo" gizmo --foo bar/baz
```

Anything that looks like file or directory will be **resolved to its absolute path**:

```bash
# Example of path resolution
gizmo --foo /User/me/gizmo/bar/baz
```

Use `--` to prevent this behavior:

```bash
# Complex Example
sudo serviceman add --name "gizmo" gizmo -c ./config.ini -- --separator .
```

For native **Windows** programs that use `/` for flags, you'll need to resolve some paths yourself:

```bash
# Windows Example
serviceman add --name "gizmo" gizmo.exe .\input.txt -- /c \User\me\gizmo\config.ini /q /s .
```

In this case `./config.ini` would still be resolved (before `--`), but `.` would not (after `--`)

<details>
<summary>Compiled Programs</summary>

Normally you might your program somewhat like this:

```
dinglehopper --port 8421
```bash
gizmo run --port 8421 --config envs/prod.ini
```

Adding a service for that program with `serviceman` would look like this:

> **serviceman add** dinglehopper **--** --port 8421
```bash
sudo serviceman add --name "gizmo" gizmo run --port 8421 --config envs/prod.ini
```

serviceman will find dinglehopper in your PATH.
serviceman will find `gizmo` in your PATH and resolve `envs/prod.ini` to its absolute path.

</details>

<details>
<summary>Using with scripts</summary>

```bash
./snarfblat.sh --port 8421
```

Although your text script may be executable, you'll need to specify the interpreter
in order for `serviceman` to configure the service correctly.

For example, if you had a bash script that you normally ran like this:
This can be done in two ways:

1. Put a **hashbang** in your script, such as `#!/bin/bash`.
2. Prepend the **interpreter** explicitly to your command, such as `bash ./dinglehopper.sh`.

For example, suppose you had a script like this:

`iamok.sh`:

```bash
while true; do
sleep 1; echo "Still Alive, Still Alive!"
done
```
./snarfblat.sh --port 8421

Normally you would run the script like this:

```bash
./imok.sh
```

You'd create a system service for it like this:
So you'd either need to modify the script to include a hashbang:

> serviceman add **bash** ./snarfblat.sh **--** --port 8421
```bash
#!/usr/bin/env bash
while true; do
sleep 1; echo "I'm Ok!"
done
```

`serviceman` will resolve `./snarfblat.sh` correctly because it comes
before the **--**.
Or you'd need to prepend it with `bash` when creating a service for it:

```bash
sudo serviceman add --name "imok" bash ./imok.sh
```

**Background Information**

@@ -244,6 +300,8 @@ like this:
#!/usr/local/bin/node --harmony --inspect
```

Serviceman understands all 3 of those approaches.

</details>

<details>
@@ -252,14 +310,37 @@ like this:
If normally you run your node script something like this:

```bash
node ./demo.js --foo bar --baz
pushd ~/my-node-project/
npm start
```

Then you would add it as a system service like this:

```bash
sudo serviceman add npm start
```

If normally you run your node script something like this:

```bash
pushd ~/my-node-project/
node ./serve.js --foo bar --baz
```

Then you would add it as a system service like this:

> **serviceman add** node ./demo.js **--** --foo bar --baz
```bash
sudo serviceman add node ./serve.js --foo bar --baz
```

It's important that any paths start with `./` and have the `.js`
so that serviceman knows to resolve the full path.

It is important that you specify `node ./demo.js` and not just `./demo.js`
```bash
# Bad Examples
sudo serviceman add node ./demo # Wouldn't work for 'demo.js' - not a real filename
sudo serviceman add node demo # Wouldn't work for './demo/' - doesn't look like a directory
```

See **Using with scripts** for more detailed information.

@@ -271,14 +352,15 @@ See **Using with scripts** for more detailed information.
If normally you run your python script something like this:

```bash
python ./demo.py --foo bar --baz
pushd ~/my-python-project/
python ./serve.py --config ./config.ini
```

Then you would add it as a system service like this:

> **serviceman add** python ./demo.py **--** --foo bar --baz
It is important that you specify `python ./demo.py` and not just `./demo.py`
```bash
sudo serviceman add python ./serve.py --config ./config.ini
```

See **Using with scripts** for more detailed information.

@@ -290,31 +372,32 @@ See **Using with scripts** for more detailed information.
If normally you run your ruby script something like this:

```bash
ruby ./demo.rb --foo bar --baz
pushd ~/my-ruby-project/
ruby ./serve.rb --config ./config.yaml
```

Then you would add it as a system service like this:

> **serviceman add** ruby ./demo.rb **--** --foo bar --baz
It is important that you specify `ruby ./demo.rb` and not just `./demo.rb`
```bash
sudo serviceman add ruby ./serve.rb --config ./config.yaml
```

See **Using with scripts** for more detailed information.

</details>

## Relative vs Absolute Paths
## Hints

Although serviceman can expand the executable's path,
if you have any arguments with relative paths
you should switch to using absolute paths.
- If something goes wrong, read the output **completely** - it'll probably be helpful
- Run `serviceman` from your **project directory**, just as you would run it normally
- Otherwise specify `--name <service-name>` and `--workdir <project directory>`
- Use `--` in front of arguments that should not be resolved as paths
- This also holds true if you need `--` as an argument, such as `-- --foo -- --bar`

```
dinglehopper --config ./conf.json
```

```
serviceman add dinglehopper -- --config /Users/me/dinglehopper/conf.json
# Example of a / that isn't a path
# (it needs to be escaped with --)
sudo serviceman add dinglehopper config/prod -- --category color/blue
```

# Logging
@@ -323,6 +406,7 @@ serviceman add dinglehopper -- --config /Users/me/dinglehopper/conf.json

```bash
sudo journalctl -xef --unit <NAME>
sudo journalctl -xef --user-unit <NAME>
```

### Mac, Windows
@@ -354,6 +438,9 @@ why your app failed to start.

# Debugging

- `serviceman add --dryrun <normal options>`
- `serviceman run --config <special config>`

One of the most irritating problems with all of these launchers is that they're
terrible to debug - it's often difficult to find the logs, and nearly impossible
to interpret them, if they exist at all.


+ 6
- 6
manager/install.go View File

@@ -14,7 +14,7 @@ import (

// Install will do a best-effort attempt to install a start-on-startup
// user or system service via systemd, launchd, or reg.exe
func Install(c *service.Service) error {
func Install(c *service.Service) (string, error) {
if "" == c.Exec {
c.Exec = c.Name
}
@@ -24,23 +24,23 @@ func Install(c *service.Service) error {
if nil != err {
fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
os.Exit(4)
return err
return "", err
} else {
c.Home = home
}
}

err := install(c)
name, err := install(c)
if nil != err {
return err
return "", err
}

err = os.MkdirAll(c.Logdir, 0755)
if nil != err {
return err
return "", err
}

return nil
return name, nil
}

func Start(conf *service.Service) error {


+ 31
- 24
manager/install_darwin.go View File

@@ -50,12 +50,11 @@ func start(conf *service.Service) error {

cmds = adjustPrivs(system, cmds)

fmt.Println()
typ := "USER"
if system {
typ = "SYSTEM"
}
fmt.Printf("Starting launchd %s service...\n", typ)
fmt.Printf("Starting launchd %s service...\n\n", typ)
for i := range cmds {
exe := cmds[i]
fmt.Println("\t" + exe.String())
@@ -109,11 +108,32 @@ func stop(conf *service.Service) error {
return nil
}

func install(c *service.Service) error {
func Render(c *service.Service) ([]byte, error) {
// Create service file from template
b, err := static.ReadFile("dist/Library/LaunchDaemons/_rdns_.plist.tmpl")
if err != nil {
return nil, err
}
s := string(b)
rw := &bytes.Buffer{}
// not sure what the template name does, but whatever
tmpl, err := template.New("service").Parse(s)
if err != nil {
return nil, err
}
err = tmpl.Execute(rw, c)
if nil != err {
return nil, err
}

return rw.Bytes(), nil
}

func install(c *service.Service) (string, error) {
// Darwin-specific config options
if c.PrivilegedPorts {
if !c.System {
return fmt.Errorf("You must use root-owned LaunchDaemons (not user-owned LaunchAgents) to use priveleged ports on OS X")
return "", fmt.Errorf("You must use root-owned LaunchDaemons (not user-owned LaunchAgents) to use priveleged ports on OS X")
}
}
plistDir := srvSysPath
@@ -124,32 +144,20 @@ func install(c *service.Service) error {
// Check paths first
err := os.MkdirAll(filepath.Dir(plistDir), 0755)
if nil != err {
return err
return "", err
}

// Create service file from template
b, err := static.ReadFile("dist/Library/LaunchDaemons/_rdns_.plist.tmpl")
if err != nil {
return err
}
s := string(b)
rw := &bytes.Buffer{}
// not sure what the template name does, but whatever
tmpl, err := template.New("service").Parse(s)
if err != nil {
return err
}
err = tmpl.Execute(rw, c)
b, err := Render(c)
if nil != err {
return err
return "", err
}

// Write the file out
// TODO rdns
plistName := c.ReverseDNS + ".plist"
plistPath := filepath.Join(plistDir, plistName)
if err := ioutil.WriteFile(plistPath, rw.Bytes(), 0644); err != nil {
return fmt.Errorf("Error writing %s: %v", plistPath, err)
if err := ioutil.WriteFile(plistPath, b, 0644); err != nil {
return "", fmt.Errorf("Error writing %s: %v", plistPath, err)
}

// TODO --no-start
@@ -158,9 +166,8 @@ func install(c *service.Service) error {
fmt.Printf("If things don't go well you should be able to get additional logging from launchctl:\n")
fmt.Printf("\tsudo launchctl log level debug\n")
fmt.Printf("\ttail -f /var/log/system.log\n")
return err
return "", err
}

fmt.Printf("Added and started '%s' as a launchctl service.\n", c.Name)
return nil
return "launchd", nil
}

+ 30
- 23
manager/install_linux.go View File

@@ -88,12 +88,11 @@ func start(conf *service.Service) error {

cmds = adjustPrivs(system, cmds)

fmt.Println()
typ := "USER MODE"
if system {
typ = "SYSTEM"
}
fmt.Printf("Starting systemd %s service unit...\n", typ)
fmt.Printf("Starting systemd %s service unit...\n\n", typ)
for i := range cmds {
exe := cmds[i]
fmt.Println("\t" + exe.String())
@@ -160,7 +159,28 @@ func stop(conf *service.Service) error {
return nil
}

func install(c *service.Service) error {
func Render(c *service.Service) ([]byte, error) {
// Create service file from template
b, err := static.ReadFile("dist/etc/systemd/system/_name_.service.tmpl")
if err != nil {
return nil, err
}
s := string(b)
rw := &bytes.Buffer{}
// not sure what the template name does, but whatever
tmpl, err := template.New("service").Parse(s)
if err != nil {
return nil, err
}
err = tmpl.Execute(rw, c)
if nil != err {
return nil, err
}

return rw.Bytes(), nil
}

func install(c *service.Service) (string, error) {
// Linux-specific config options
if c.System {
if "" == c.User {
@@ -177,32 +197,20 @@ func install(c *service.Service) error {
serviceDir = filepath.Join(c.Home, srvUserPath)
err := os.MkdirAll(serviceDir, 0755)
if nil != err {
return err
return "", err
}
}

// Create service file from template
b, err := static.ReadFile("dist/etc/systemd/system/_name_.service.tmpl")
if err != nil {
return err
}
s := string(b)
rw := &bytes.Buffer{}
// not sure what the template name does, but whatever
tmpl, err := template.New("service").Parse(s)
if err != nil {
return err
}
err = tmpl.Execute(rw, c)
b, err := Render(c)
if nil != err {
return err
return "", err
}

// Write the file out
serviceName := c.Name + ".service"
servicePath := filepath.Join(serviceDir, serviceName)
if err := ioutil.WriteFile(servicePath, rw.Bytes(), 0644); err != nil {
return fmt.Errorf("Error writing %s: %v", servicePath, err)
if err := ioutil.WriteFile(servicePath, b, 0644); err != nil {
return "", fmt.Errorf("Error writing %s: %v", servicePath, err)
}

// TODO --no-start
@@ -217,9 +225,8 @@ func install(c *service.Service) error {
}
fmt.Printf("If things don't go well you should be able to get additional logging from journalctl:\n")
fmt.Printf("\t%sjournalctl -xe %s %s.service\n", sudo, unit, c.Name)
return err
return "", err
}

fmt.Printf("Added and started '%s' as a systemd service.\n", c.Name)
return nil
return "systemd", nil
}

+ 4
- 0
manager/install_other.go View File

@@ -6,6 +6,10 @@ import (
"git.rootprojects.org/root/go-serviceman/service"
)

func Render(c *service.Service) ([]byte, error) {
return nil, nil
}

func install(c *service.Service) error {
return nil, nil
}

+ 16
- 11
manager/install_windows.go View File

@@ -30,7 +30,7 @@ func init() {

// TODO system service requires elevated privileges
// See https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
func install(c *service.Service) error {
func install(c *service.Service) (string, error) {
/*
// LEAVE THIS DOCUMENTATION HERE
reg.exe
@@ -73,7 +73,7 @@ func install(c *service.Service) error {

args, err := installServiceman(c)
if nil != err {
return err
return "", err
}

/*
@@ -100,7 +100,7 @@ func install(c *service.Service) error {

regSZ := fmt.Sprintf(`"%s" %s`, args[0], strings.Join(args[1:], " "))
if len(regSZ) > 260 {
return fmt.Errorf("data value is too long for registry entry")
return "", fmt.Errorf("data value is too long for registry entry")
}
// In order for a windows gui program to not show a console,
// it has to not output any messages?
@@ -108,17 +108,22 @@ func install(c *service.Service) error {
//fmt.Println(autorunKey, c.Title, regSZ)
k.SetStringValue(c.Title, regSZ)

// to return ErrDaemonize
return start(c)
err = start(c)
return "serviceman", err
}

func Render(c *service.Service) ([]byte, error) {
b, err := json.Marshal(c)
if nil != err {
return nil, err
}
return b, nil
}

func start(conf *service.Service) error {
args := getRunnerArgs(conf)
return &ErrDaemonize{
DaemonArgs: append(args, "--daemon"),
error: "Not as much an error as a bad value...",
}
//return runner.Start(conf)
args = append(args, "--daemon")
return Run(args[0], args[1:]...)
}

func stop(conf *service.Service) error {
@@ -173,7 +178,7 @@ func installServiceman(c *service.Service) ([]string, error) {
}
}

b, err := json.Marshal(c)
b, err := Render(c)
if nil != err {
// this should be impossible, so we'll just panic
panic(err)


+ 18
- 0
manager/start.go View File

@@ -227,3 +227,21 @@ func adjustPrivs(system bool, cmds []Runnable) []Runnable {

return cmds
}

func Run(bin string, args ...string) error {
cmd := exec.Command(bin, args...)
// for debugging
/*
out, err := cmd.CombinedOutput()
if nil != err {
fmt.Println(err)
}
fmt.Println(string(out))
*/

err := cmd.Start()
if nil != err {
return err
}
return nil
}

+ 1
- 1
service/service.go View File

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


+ 311
- 68
serviceman.go View File

@@ -1,5 +1,6 @@
//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver

// main runs the things and does the stuff
package main

import (
@@ -9,8 +10,11 @@ import (
"io/ioutil"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"time"
"unicode/utf8"

"git.rootprojects.org/root/go-serviceman/manager"
"git.rootprojects.org/root/go-serviceman/runner"
@@ -62,26 +66,15 @@ func add() {
Restart: true,
}

args := []string{}
for i := range os.Args {
if "--" == os.Args[i] {
if len(os.Args) > i+1 {
args = os.Args[i+1:]
}
os.Args = os.Args[:i]
break
}
}
conf.Argv = args

force := false
forUser := false
forSystem := false
dryrun := false
flag.StringVar(&conf.Title, "title", "", "a human-friendly name for the service")
flag.StringVar(&conf.Desc, "desc", "", "a human-friendly description of the service (ex: Foo App)")
flag.StringVar(&conf.Name, "name", "", "a computer-friendly name for the service (ex: foo-app)")
flag.StringVar(&conf.URL, "url", "", "the documentation on home page of the service")
//flag.StringVar(&conf.Workdir, "workdir", "", "the directory in which the service should be started")
flag.StringVar(&conf.Workdir, "workdir", "", "the directory in which the service should be started (if supported)")
flag.StringVar(&conf.ReverseDNS, "rdns", "", "a plist-friendly Reverse DNS name for launchctl (ex: com.example.foo-app)")
flag.BoolVar(&forSystem, "system", false, "attempt to add system service as an unprivileged/unelevated user")
flag.BoolVar(&forUser, "user", false, "add user space / user mode service even when admin/root/sudo/elevated")
@@ -89,67 +82,339 @@ func add() {
flag.StringVar(&conf.User, "username", "", "run the service as this user")
flag.StringVar(&conf.Group, "groupname", "", "run the service as this group")
flag.BoolVar(&conf.PrivilegedPorts, "cap-net-bind", false, "this service should have access to privileged ports")
flag.BoolVar(&dryrun, "dryrun", false, "output the service file without modifying anything on disk")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0])

flag.PrintDefaults()

fmt.Fprintf(os.Stderr, "Flags and arguments after \"--\" will be completely ignored by serviceman\n", os.Args[0])
}
flag.Parse()
args = flag.Args()
flagargs := flag.Args()

// You must have something to run, duh
n := len(flagargs)
if 0 == n {
fmt.Println("Usage: serviceman add ./foo-app --foo-arg")
os.Exit(2)
return
}

if forUser && forSystem {
fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
os.Exit(1)
return
}

// There are three groups of flags
// serviceman --flag1 arg1 non-flag-arg --child1 -- --raw1 -- --raw2
// serviceman --flag1 arg1 // these belong to serviceman
// non-flag-arg --child1 // these will be interpretted
// -- // separator
// --raw1 -- --raw2 // after the separater (including additional separators) will be ignored
rawargs := []string{}
for i := range flagargs {
if "--" == flagargs[i] {
if len(flagargs) > i+1 {
rawargs = flagargs[i+1:]
}
flagargs = flagargs[:i]
break
}
}

// Assumptions
ass := []string{}
if forUser {
conf.System = false
} else if forSystem {
conf.System = true
} else {
conf.System = manager.IsPrivileged()
if conf.System {
ass = append(ass, "# Because you're a privileged user")
ass = append(ass, " --system")
ass = append(ass, "")
} else {
ass = append(ass, "# Because you're a unprivileged user")
ass = append(ass, " --user")
ass = append(ass, "")
}
}
if "" == conf.Workdir {
dir, _ := os.Getwd()
conf.Workdir = dir
ass = append(ass, "# Because this is your current working directory")
ass = append(ass, fmt.Sprintf(" --workdir %s", conf.Workdir))
ass = append(ass, "")
}
if "" == conf.Name {
name, _ := os.Getwd()
base := filepath.Base(name)
ext := filepath.Ext(base)
n := (len(base) - len(ext))
name = base[:n]
if "" == name {
name = base
}
conf.Name = name
ass = append(ass, "# Because this is the name of your current working directory")
ass = append(ass, fmt.Sprintf(" --name %s", conf.Name))
ass = append(ass, "")
}

n := len(args)
if 0 == n {
fmt.Println("Usage: serviceman add ./foo-app -- --foo-arg")
os.Exit(2)
exepath, err := findExec(flagargs[0], force)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(3)
return
}
flagargs[0] = exepath

execpath, err := manager.WhereIs(args[0])
exeargs, err := testScript(flagargs[0], force)
if nil != err {
fmt.Fprintf(os.Stderr, "Error: '%s' could not be found in PATH or working directory.\n", args[0])
if !force {
os.Exit(3)
return
}
} else {
args[0] = execpath
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(3)
return
}
conf.Exec = args[0]
args = args[1:]

if n >= 2 {
conf.Interpreter = conf.Exec
conf.Exec = args[0]
conf.Argv = append(args[1:], conf.Argv...)
flagargs = append(exeargs, flagargs...)
// TODO
for i := range flagargs {
arg := flagargs[i]
arg = filepath.ToSlash(arg)
// Paths considered to be anything starting with ./, .\, /, \, C:
if "." == arg || strings.Contains(arg, "/") {
//if "." == arg || (len(arg) >= 2 && "./" == arg[:2] || '/' == arg[0] || "C:" == strings.ToUpper(arg[:1])) {
var err error
arg, err = filepath.Abs(arg)
if nil == err {
_, err = os.Stat(arg)
}
if nil != err {
fmt.Printf("%q appears to be a file path, but %q could not be read\n", flagargs[i], arg)
if !force {
os.Exit(7)
return
}
continue
}

if '\\' != os.PathSeparator {
// Convert paths back to .\ for Windows
arg = filepath.FromSlash(arg)
}

// Lookin' good
flagargs[i] = arg
}
}

conf.Normalize(force)
// We won't bother with Interpreter here
// (it's really just for documentation),
// but we will add any and all unchecked args to the full slice
conf.Exec = flagargs[0]
conf.Argv = append(flagargs[1:], rawargs...)

// TODO update docs: go to the work directory
// TODO test with "npm start"

conf.NormalizeWithoutPath()

//fmt.Printf("\n%#v\n\n", conf)
if conf.System && !manager.IsPrivileged() {
fmt.Fprintf(os.Stderr, "Warning: You may need to use 'sudo' to add %q as a privileged system service.\n", conf.Name)
}

err = manager.Install(conf)
switch e := err.(type) {
case nil:
// do nothing
case *manager.ErrDaemonize:
runAsDaemon(e.DaemonArgs[0], e.DaemonArgs[1:]...)
default:
if len(ass) > 0 {
fmt.Println("OPTIONS: Making some assumptions...\n")
for i := range ass {
fmt.Println("\t" + ass[i])
}
}

// Find who this is running as
// And pretty print the command to run
runAs := conf.User
var wasflag bool
fmt.Printf("COMMAND: Service %q will be run like this (more or less):\n\n", conf.Title)
if conf.System {
if "" == runAs {
runAs = "root"
}
fmt.Printf("\t# Starts on system boot, as %q\n", runAs)
} else {
u, _ := user.Current()
runAs = u.Name
if "" == runAs {
runAs = u.Username
}
fmt.Printf("\t# Starts as %q, when %q logs in\n", runAs, u.Username)
}
//fmt.Printf("\tpushd %s\n", conf.Workdir)
fmt.Printf("\t%s\n", conf.Exec)
for i := range conf.Argv {
arg := conf.Argv[i]
if '-' == arg[0] {
if wasflag {
fmt.Println()
}
wasflag = true
fmt.Printf("\t\t%s", arg)
} else {
if wasflag {
fmt.Printf(" %s\n", arg)
} else {
fmt.Printf("\t\t%s\n", arg)
}
wasflag = false
}
}
if wasflag {
fmt.Println()
}
fmt.Println()

// TODO output config without installing
if dryrun {
b, err := manager.Render(conf)
if nil != err {
fmt.Fprintf(os.Stderr, "Error rendering: %s\n", err)
os.Exit(10)
}
fmt.Println(string(b))
return
}

fmt.Printf("LAUNCHER: ")
servicetype, err := manager.Install(conf)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(500)
return
}

fmt.Printf("LOGS: ")
printLogMessage(conf)
fmt.Println()

servicemode := "USER MODE"
if conf.System {
servicemode = "SYSTEM"
}
fmt.Printf(
"SUCCESS:\n\n\t%q started as a %q %s service, running as %q\n",
conf.Name,
servicetype,
servicemode,
runAs,
)
fmt.Println()
}

func findExec(exe string, force bool) (string, error) {
// ex: node => /usr/local/bin/node
// ex: ./demo.js => /Users/aj/project/demo.js
exepath, err := exec.LookPath(exe)
if nil != err {
var msg string
if strings.Contains(filepath.ToSlash(exe), "/") {
if _, err := os.Stat(exe); err != nil {
msg = fmt.Sprintf("Error: '%s' could not be found in PATH or working directory.\n", exe)
} else {
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)
}
} else {
if _, err := os.Stat(exe); err != nil {
msg = fmt.Sprintf("Error: '%s' could not be found in PATH", exe)
} else {
msg = fmt.Sprintf("Error: '%s' could not be found in PATH, did you mean './%s'?\n", exe, exe)
}
}
if !force {
return "", fmt.Errorf(msg)
}
fmt.Fprintf(os.Stderr, "%s\n", msg)
return exe, nil
}

// ex: \Users\aj\project\demo.js => /Users/aj/project/demo.js
// Can't have an error here when lookpath succeeded
exepath, _ = filepath.Abs(filepath.ToSlash(exepath))
return exepath, nil
}

func testScript(exepath string, force bool) ([]string, error) {
f, err := os.Open(exepath)
b := make([]byte, 256)
if nil == err {
_, err = f.Read(b)
}
if nil != err || len(b) < len("#!/x") {
msg := fmt.Sprintf("Error when testing if '%s' is a binary or script: could not read file: %s\n", exepath, err)
if !force {
return nil, fmt.Errorf(msg)
}
fmt.Fprintf(os.Stderr, "%s\n", msg)
return nil, nil
}

// Nott sure if this is more readable and idiomatic as if else or switch
// However, the order matters
switch {
case utf8.Valid(b):
// Looks like an executable script
if "#!/" == string(b[:3]) {
break
}

msg := fmt.Sprintf("Error: %q looks like a script, but we don't know the interpreter.\nYou can probably fix this by...\n"+
"\tExplicitly naming the interpreter (ex: 'python my-script.py' instead of just 'my-script.py')\n"+
"\tPlacing a hashbang at the top of the script (ex: '#!/usr/bin/env python')", exepath)

if !force {
return nil, fmt.Errorf(msg)
}
return nil, nil
case "#!/" != string(b[:3]):
// Looks like a normal binary
return nil, nil
default:
// Looks like a corrupt script file
msg := "Error: It looks like you've specified a corrupt script file."
if !force {
return nil, fmt.Errorf(msg)
}
return nil, nil
}

// Deal with #!/whatever

// Get that first line
// "#!/usr/bin/env node" => ["/usr/bin/env", "node"]
// "#!/usr/bin/node --harmony => ["/usr/bin/node", "--harmony"]
s := string(b[2:]) // strip leading #!
s = strings.Split(strings.Replace(s, "\r\n", "\n", -1), "\n")[0]
allargs := strings.Split(strings.TrimSpace(s), " ")
args := []string{}
for i := range allargs {
arg := strings.TrimSpace(allargs[i])
if "" != arg {
args = append(args, arg)
}
}
if strings.HasSuffix(args[0], "/env") && len(args) > 1 {
// TODO warn that "env" is probably not an executable if 1 = len(args)?
args = args[1:]
}
exepath, err = findExec(args[0], force)
if nil != err {
return nil, err
}
args[0] = exepath

return args, nil
}

func start() {
@@ -185,14 +450,10 @@ func start() {
conf.NormalizeWithoutPath()

err := manager.Start(conf)
switch e := err.(type) {
case nil:
// do nothing
case *manager.ErrDaemonize:
runAsDaemon(e.DaemonArgs[0], e.DaemonArgs[1:]...)
default:
fmt.Println(err)
os.Exit(127)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(500)
return
}
}

@@ -242,7 +503,7 @@ func run() {
flag.Parse()

if "" == confpath {
fmt.Fprintf(os.Stderr, "%s", strings.Join(flag.Args(), " "))
fmt.Fprintf(os.Stderr, "%s\n", strings.Join(flag.Args(), " "))
fmt.Fprintf(os.Stderr, "--config /path/to/config.json is required\n")
usage()
os.Exit(1)
@@ -295,23 +556,5 @@ func run() {
return
}

runAsDaemon(os.Args[0], "run", "--config", confpath)
}

func runAsDaemon(bin string, args ...string) {
cmd := exec.Command(bin, args...)
// for debugging
/*
out, err := cmd.CombinedOutput()
if nil != err {
fmt.Println(err)
}
fmt.Println(string(out))
*/

err := cmd.Start()
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(500)
}
manager.Run(os.Args[0], "run", "--config", confpath)
}

+ 1
- 1
serviceman_darwin.go View File

@@ -7,5 +7,5 @@ import (
)

func printLogMessage(conf *service.Service) {
fmt.Printf("If all went well the logs should have been created at:\n\t%s\n", conf.Logdir)
fmt.Printf("If all went well the logs should have been created at:\n\n\t%s\n", conf.Logdir)
}

+ 1
- 1
serviceman_linux.go View File

@@ -17,7 +17,7 @@ func printLogMessage(conf *service.Service) {
} else {
unit = "--user-unit"
}
fmt.Println("If all went well you should be able to see some goodies in the logs:")
fmt.Println("If all went well you should be able to see some goodies in the logs:\n")
fmt.Printf("\t%sjournalctl -xe %s %s.service\n", sudo, unit, conf.Name)
if !conf.System {
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 (
)

func printLogMessage(conf *service.Service) {
fmt.Printf("If all went well the logs should have been created at:\n\t%s\n", conf.Logdir)
fmt.Printf("If all went well the logs should have been created at:\n\n\t%s\n", conf.Logdir)
}

Loading…
Cancel
Save