better arg handling, more descriptive output
This commit is contained in:
parent
389b88331d
commit
40a82f26c4
193
README.md
193
README.md
|
@ -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** <program> **--** <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>
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
```bash
|
||||
./snarfblat.sh --port 8421
|
||||
```
|
||||
|
||||
You'd create a system service for it like this:
|
||||
Although your text script may be executable, you'll need to specify the interpreter
|
||||
in order for `serviceman` to configure the service correctly.
|
||||
|
||||
> serviceman add **bash** ./snarfblat.sh **--** --port 8421
|
||||
This can be done in two ways:
|
||||
|
||||
`serviceman` will resolve `./snarfblat.sh` correctly because it comes
|
||||
before the **--**.
|
||||
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
|
||||
```
|
||||
|
||||
Normally you would run the script like this:
|
||||
|
||||
```bash
|
||||
./imok.sh
|
||||
```
|
||||
|
||||
So you'd either need to modify the script to include a hashbang:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
while true; do
|
||||
sleep 1; echo "I'm Ok!"
|
||||
done
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
> **serviceman add** node ./demo.js **--** --foo bar --baz
|
||||
```bash
|
||||
sudo serviceman add npm start
|
||||
```
|
||||
|
||||
It is important that you specify `node ./demo.js` and not just `./demo.js`
|
||||
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:
|
||||
|
||||
```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.
|
||||
|
||||
```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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
383
serviceman.go
383
serviceman.go
|
@ -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)
|
||||
return
|
||||
}
|
||||
|
||||
execpath, err := manager.WhereIs(args[0])
|
||||
exepath, err := findExec(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 {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(3)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
args[0] = execpath
|
||||
}
|
||||
conf.Exec = args[0]
|
||||
args = args[1:]
|
||||
flagargs[0] = exepath
|
||||
|
||||
if n >= 2 {
|
||||
conf.Interpreter = conf.Exec
|
||||
conf.Exec = args[0]
|
||||
conf.Argv = append(args[1:], conf.Argv...)
|
||||
exeargs, err := testScript(flagargs[0], force)
|
||||
if nil != err {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(3)
|
||||
return
|
||||
}
|
||||
|
||||
conf.Normalize(force)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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:
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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…
Reference in New Issue