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
|
## Features
|
||||||
|
|
||||||
- Unprivileged (User Mode) Services
|
- Unprivileged (User Mode) Services with `--user` (_Default_)
|
||||||
- [x] Linux (`sytemctl --user`)
|
- [x] Linux (`sytemctl --user`)
|
||||||
- [x] MacOS (`launchctl`)
|
- [x] MacOS (`launchctl`)
|
||||||
- [x] Windows (`HKEY_CURRENT_USER/.../Run`)
|
- [x] Windows (`HKEY_CURRENT_USER/.../Run`)
|
||||||
- Privileged (System) Services
|
- Privileged (System) Services with `--system` (_Default_ for `root`)
|
||||||
- [x] Linux (`sudo sytemctl`)
|
- [x] Linux (`sudo sytemctl`)
|
||||||
- [x] MacOS (`sudo launchctl`)
|
- [x] MacOS (`sudo launchctl`)
|
||||||
- [ ] Windows (_not yet implemented_)
|
- [ ] Windows (_not yet implemented_)
|
||||||
|
@ -40,26 +40,17 @@ Because debugging launchctl, systemd, etc absolutely sucks!
|
||||||
|
|
||||||
The basic pattern of usage:
|
The basic pattern of usage:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
serviceman add [options] [interpreter] <service> -- [service options]
|
sudo serviceman add --name "foobar" [options] [interpreter] <service> [--] [service options]
|
||||||
serviceman start <service>
|
sudo serviceman start <service>
|
||||||
serviceman stop <service>
|
sudo serviceman stop <service>
|
||||||
serviceman version
|
serviceman version
|
||||||
```
|
```
|
||||||
|
|
||||||
And what that might look like:
|
And what that might look like:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
# Here the service is named "foo" implicitly
|
sudo serviceman add --name "foo" foo.exe -c ./config.json
|
||||||
# '--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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also view the help:
|
You can also view the help:
|
||||||
|
@ -68,6 +59,14 @@ You can also view the help:
|
||||||
serviceman add --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
|
# Install
|
||||||
|
|
||||||
There are a number of pre-built binaries.
|
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
|
mkdir %userprofile%\bin
|
||||||
reg add HKEY_CURRENT_USER\Environment /v PATH /d "%PATH%;%userprofile%\bin"
|
|
||||||
move serviceman.exe %userprofile%\bin\serviceman.exe
|
move serviceman.exe %userprofile%\bin\serviceman.exe
|
||||||
|
reg add HKEY_CURRENT_USER\Environment /v PATH /d "%PATH%;%userprofile%\bin"
|
||||||
```
|
```
|
||||||
|
|
||||||
**All Others**
|
**All Others**
|
||||||
|
@ -184,43 +183,100 @@ sudo mv ./serviceman /usr/local/bin/
|
||||||
|
|
||||||
# Examples
|
# 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>
|
<details>
|
||||||
<summary>Compiled Programs</summary>
|
<summary>Compiled Programs</summary>
|
||||||
|
|
||||||
Normally you might your program somewhat like this:
|
Normally you might your program somewhat like this:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
dinglehopper --port 8421
|
gizmo run --port 8421 --config envs/prod.ini
|
||||||
```
|
```
|
||||||
|
|
||||||
Adding a service for that program with `serviceman` would look like this:
|
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>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Using with scripts</summary>
|
<summary>Using with scripts</summary>
|
||||||
|
|
||||||
Although your text script may be executable, you'll need to specify the interpreter
|
```bash
|
||||||
in order for `serviceman` to configure the service correctly.
|
|
||||||
|
|
||||||
For example, if you had a bash script that you normally ran like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
./snarfblat.sh --port 8421
|
./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
|
1. Put a **hashbang** in your script, such as `#!/bin/bash`.
|
||||||
before the **--**.
|
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**
|
**Background Information**
|
||||||
|
|
||||||
|
@ -244,6 +300,8 @@ like this:
|
||||||
#!/usr/local/bin/node --harmony --inspect
|
#!/usr/local/bin/node --harmony --inspect
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Serviceman understands all 3 of those approaches.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
@ -252,14 +310,37 @@ like this:
|
||||||
If normally you run your node script something like this:
|
If normally you run your node script something like this:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node ./demo.js --foo bar --baz
|
pushd ~/my-node-project/
|
||||||
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you would add it as a system service like this:
|
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.
|
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:
|
If normally you run your python script something like this:
|
||||||
|
|
||||||
```bash
|
```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:
|
Then you would add it as a system service like this:
|
||||||
|
|
||||||
> **serviceman add** python ./demo.py **--** --foo bar --baz
|
```bash
|
||||||
|
sudo serviceman add python ./serve.py --config ./config.ini
|
||||||
It is important that you specify `python ./demo.py` and not just `./demo.py`
|
```
|
||||||
|
|
||||||
See **Using with scripts** for more detailed information.
|
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:
|
If normally you run your ruby script something like this:
|
||||||
|
|
||||||
```bash
|
```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:
|
Then you would add it as a system service like this:
|
||||||
|
|
||||||
> **serviceman add** ruby ./demo.rb **--** --foo bar --baz
|
```bash
|
||||||
|
sudo serviceman add ruby ./serve.rb --config ./config.yaml
|
||||||
It is important that you specify `ruby ./demo.rb` and not just `./demo.rb`
|
```
|
||||||
|
|
||||||
See **Using with scripts** for more detailed information.
|
See **Using with scripts** for more detailed information.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Relative vs Absolute Paths
|
## Hints
|
||||||
|
|
||||||
Although serviceman can expand the executable's path,
|
- If something goes wrong, read the output **completely** - it'll probably be helpful
|
||||||
if you have any arguments with relative paths
|
- Run `serviceman` from your **project directory**, just as you would run it normally
|
||||||
you should switch to using absolute paths.
|
- 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
|
# Example of a / that isn't a path
|
||||||
```
|
# (it needs to be escaped with --)
|
||||||
|
sudo serviceman add dinglehopper config/prod -- --category color/blue
|
||||||
```
|
|
||||||
serviceman add dinglehopper -- --config /Users/me/dinglehopper/conf.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
|
@ -323,6 +406,7 @@ serviceman add dinglehopper -- --config /Users/me/dinglehopper/conf.json
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo journalctl -xef --unit <NAME>
|
sudo journalctl -xef --unit <NAME>
|
||||||
|
sudo journalctl -xef --user-unit <NAME>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mac, Windows
|
### Mac, Windows
|
||||||
|
@ -354,6 +438,9 @@ why your app failed to start.
|
||||||
|
|
||||||
# Debugging
|
# 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
|
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
|
terrible to debug - it's often difficult to find the logs, and nearly impossible
|
||||||
to interpret them, if they exist at all.
|
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
|
// Install will do a best-effort attempt to install a start-on-startup
|
||||||
// user or system service via systemd, launchd, or reg.exe
|
// 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 {
|
if "" == c.Exec {
|
||||||
c.Exec = c.Name
|
c.Exec = c.Name
|
||||||
}
|
}
|
||||||
|
@ -24,23 +24,23 @@ func Install(c *service.Service) error {
|
||||||
if nil != err {
|
if nil != err {
|
||||||
fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
|
fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
|
||||||
os.Exit(4)
|
os.Exit(4)
|
||||||
return err
|
return "", err
|
||||||
} else {
|
} else {
|
||||||
c.Home = home
|
c.Home = home
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := install(c)
|
name, err := install(c)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.MkdirAll(c.Logdir, 0755)
|
err = os.MkdirAll(c.Logdir, 0755)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(conf *service.Service) error {
|
func Start(conf *service.Service) error {
|
||||||
|
|
|
@ -50,12 +50,11 @@ func start(conf *service.Service) error {
|
||||||
|
|
||||||
cmds = adjustPrivs(system, cmds)
|
cmds = adjustPrivs(system, cmds)
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
typ := "USER"
|
typ := "USER"
|
||||||
if system {
|
if system {
|
||||||
typ = "SYSTEM"
|
typ = "SYSTEM"
|
||||||
}
|
}
|
||||||
fmt.Printf("Starting launchd %s service...\n", typ)
|
fmt.Printf("Starting launchd %s service...\n\n", typ)
|
||||||
for i := range cmds {
|
for i := range cmds {
|
||||||
exe := cmds[i]
|
exe := cmds[i]
|
||||||
fmt.Println("\t" + exe.String())
|
fmt.Println("\t" + exe.String())
|
||||||
|
@ -109,11 +108,32 @@ func stop(conf *service.Service) error {
|
||||||
return nil
|
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
|
// Darwin-specific config options
|
||||||
if c.PrivilegedPorts {
|
if c.PrivilegedPorts {
|
||||||
if !c.System {
|
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
|
plistDir := srvSysPath
|
||||||
|
@ -124,32 +144,20 @@ func install(c *service.Service) error {
|
||||||
// Check paths first
|
// Check paths first
|
||||||
err := os.MkdirAll(filepath.Dir(plistDir), 0755)
|
err := os.MkdirAll(filepath.Dir(plistDir), 0755)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create service file from template
|
b, err := Render(c)
|
||||||
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)
|
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the file out
|
// Write the file out
|
||||||
// TODO rdns
|
// TODO rdns
|
||||||
plistName := c.ReverseDNS + ".plist"
|
plistName := c.ReverseDNS + ".plist"
|
||||||
plistPath := filepath.Join(plistDir, plistName)
|
plistPath := filepath.Join(plistDir, plistName)
|
||||||
if err := ioutil.WriteFile(plistPath, rw.Bytes(), 0644); err != nil {
|
if err := ioutil.WriteFile(plistPath, b, 0644); err != nil {
|
||||||
return fmt.Errorf("Error writing %s: %v", plistPath, err)
|
return "", fmt.Errorf("Error writing %s: %v", plistPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO --no-start
|
// 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("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("\tsudo launchctl log level debug\n")
|
||||||
fmt.Printf("\ttail -f /var/log/system.log\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 "launchd", nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,12 +88,11 @@ func start(conf *service.Service) error {
|
||||||
|
|
||||||
cmds = adjustPrivs(system, cmds)
|
cmds = adjustPrivs(system, cmds)
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
typ := "USER MODE"
|
typ := "USER MODE"
|
||||||
if system {
|
if system {
|
||||||
typ = "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 {
|
for i := range cmds {
|
||||||
exe := cmds[i]
|
exe := cmds[i]
|
||||||
fmt.Println("\t" + exe.String())
|
fmt.Println("\t" + exe.String())
|
||||||
|
@ -160,7 +159,28 @@ func stop(conf *service.Service) error {
|
||||||
return nil
|
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
|
// Linux-specific config options
|
||||||
if c.System {
|
if c.System {
|
||||||
if "" == c.User {
|
if "" == c.User {
|
||||||
|
@ -177,32 +197,20 @@ func install(c *service.Service) error {
|
||||||
serviceDir = filepath.Join(c.Home, srvUserPath)
|
serviceDir = filepath.Join(c.Home, srvUserPath)
|
||||||
err := os.MkdirAll(serviceDir, 0755)
|
err := os.MkdirAll(serviceDir, 0755)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create service file from template
|
b, err := Render(c)
|
||||||
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)
|
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the file out
|
// Write the file out
|
||||||
serviceName := c.Name + ".service"
|
serviceName := c.Name + ".service"
|
||||||
servicePath := filepath.Join(serviceDir, serviceName)
|
servicePath := filepath.Join(serviceDir, serviceName)
|
||||||
if err := ioutil.WriteFile(servicePath, rw.Bytes(), 0644); err != nil {
|
if err := ioutil.WriteFile(servicePath, b, 0644); err != nil {
|
||||||
return fmt.Errorf("Error writing %s: %v", servicePath, err)
|
return "", fmt.Errorf("Error writing %s: %v", servicePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO --no-start
|
// 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("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)
|
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 "systemd", nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,10 @@ import (
|
||||||
"git.rootprojects.org/root/go-serviceman/service"
|
"git.rootprojects.org/root/go-serviceman/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func Render(c *service.Service) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func install(c *service.Service) error {
|
func install(c *service.Service) error {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ func init() {
|
||||||
|
|
||||||
// TODO system service requires elevated privileges
|
// TODO system service requires elevated privileges
|
||||||
// See https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
|
// 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
|
// LEAVE THIS DOCUMENTATION HERE
|
||||||
reg.exe
|
reg.exe
|
||||||
|
@ -73,7 +73,7 @@ func install(c *service.Service) error {
|
||||||
|
|
||||||
args, err := installServiceman(c)
|
args, err := installServiceman(c)
|
||||||
if nil != err {
|
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:], " "))
|
regSZ := fmt.Sprintf(`"%s" %s`, args[0], strings.Join(args[1:], " "))
|
||||||
if len(regSZ) > 260 {
|
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,
|
// In order for a windows gui program to not show a console,
|
||||||
// it has to not output any messages?
|
// it has to not output any messages?
|
||||||
|
@ -108,17 +108,22 @@ func install(c *service.Service) error {
|
||||||
//fmt.Println(autorunKey, c.Title, regSZ)
|
//fmt.Println(autorunKey, c.Title, regSZ)
|
||||||
k.SetStringValue(c.Title, regSZ)
|
k.SetStringValue(c.Title, regSZ)
|
||||||
|
|
||||||
// to return ErrDaemonize
|
err = start(c)
|
||||||
return 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 {
|
func start(conf *service.Service) error {
|
||||||
args := getRunnerArgs(conf)
|
args := getRunnerArgs(conf)
|
||||||
return &ErrDaemonize{
|
args = append(args, "--daemon")
|
||||||
DaemonArgs: append(args, "--daemon"),
|
return Run(args[0], args[1:]...)
|
||||||
error: "Not as much an error as a bad value...",
|
|
||||||
}
|
|
||||||
//return runner.Start(conf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop(conf *service.Service) error {
|
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 {
|
if nil != err {
|
||||||
// this should be impossible, so we'll just panic
|
// this should be impossible, so we'll just panic
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
|
@ -227,3 +227,21 @@ func adjustPrivs(system bool, cmds []Runnable) []Runnable {
|
||||||
|
|
||||||
return cmds
|
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)
|
_, err := os.Stat(optpath)
|
||||||
if nil == err {
|
if nil == err {
|
||||||
bad = false
|
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
|
s.Exec = optpath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
385
serviceman.go
385
serviceman.go
|
@ -1,5 +1,6 @@
|
||||||
//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
|
//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
|
||||||
|
|
||||||
|
// main runs the things and does the stuff
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -9,8 +10,11 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"git.rootprojects.org/root/go-serviceman/manager"
|
"git.rootprojects.org/root/go-serviceman/manager"
|
||||||
"git.rootprojects.org/root/go-serviceman/runner"
|
"git.rootprojects.org/root/go-serviceman/runner"
|
||||||
|
@ -62,26 +66,15 @@ func add() {
|
||||||
Restart: true,
|
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
|
force := false
|
||||||
forUser := false
|
forUser := false
|
||||||
forSystem := false
|
forSystem := false
|
||||||
|
dryrun := false
|
||||||
flag.StringVar(&conf.Title, "title", "", "a human-friendly name for the service")
|
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.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.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.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.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(&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")
|
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.User, "username", "", "run the service as this user")
|
||||||
flag.StringVar(&conf.Group, "groupname", "", "run the service as this group")
|
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(&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()
|
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 {
|
if forUser && forSystem {
|
||||||
fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
|
fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return
|
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 {
|
if forUser {
|
||||||
conf.System = false
|
conf.System = false
|
||||||
} else if forSystem {
|
} else if forSystem {
|
||||||
conf.System = true
|
conf.System = true
|
||||||
} else {
|
} else {
|
||||||
conf.System = manager.IsPrivileged()
|
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)
|
exepath, err := findExec(flagargs[0], force)
|
||||||
if 0 == n {
|
if nil != err {
|
||||||
fmt.Println("Usage: serviceman add ./foo-app -- --foo-arg")
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
os.Exit(2)
|
os.Exit(3)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flagargs[0] = exepath
|
||||||
|
|
||||||
|
exeargs, err := testScript(flagargs[0], force)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
|
os.Exit(3)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
execpath, err := manager.WhereIs(args[0])
|
flagargs = append(exeargs, flagargs...)
|
||||||
if nil != err {
|
// TODO
|
||||||
fmt.Fprintf(os.Stderr, "Error: '%s' could not be found in PATH or working directory.\n", args[0])
|
for i := range flagargs {
|
||||||
if !force {
|
arg := flagargs[i]
|
||||||
os.Exit(3)
|
arg = filepath.ToSlash(arg)
|
||||||
return
|
// 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
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
args[0] = execpath
|
|
||||||
}
|
|
||||||
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...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
//fmt.Printf("\n%#v\n\n", conf)
|
||||||
if conf.System && !manager.IsPrivileged() {
|
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)
|
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)
|
if len(ass) > 0 {
|
||||||
switch e := err.(type) {
|
fmt.Println("OPTIONS: Making some assumptions...\n")
|
||||||
case nil:
|
for i := range ass {
|
||||||
// do nothing
|
fmt.Println("\t" + ass[i])
|
||||||
case *manager.ErrDaemonize:
|
}
|
||||||
runAsDaemon(e.DaemonArgs[0], e.DaemonArgs[1:]...)
|
|
||||||
default:
|
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
printLogMessage(conf)
|
||||||
fmt.Println()
|
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() {
|
func start() {
|
||||||
|
@ -185,14 +450,10 @@ func start() {
|
||||||
conf.NormalizeWithoutPath()
|
conf.NormalizeWithoutPath()
|
||||||
|
|
||||||
err := manager.Start(conf)
|
err := manager.Start(conf)
|
||||||
switch e := err.(type) {
|
if nil != err {
|
||||||
case nil:
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
// do nothing
|
os.Exit(500)
|
||||||
case *manager.ErrDaemonize:
|
return
|
||||||
runAsDaemon(e.DaemonArgs[0], e.DaemonArgs[1:]...)
|
|
||||||
default:
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(127)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,7 +503,7 @@ func run() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if "" == confpath {
|
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")
|
fmt.Fprintf(os.Stderr, "--config /path/to/config.json is required\n")
|
||||||
usage()
|
usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -295,23 +556,5 @@ func run() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
runAsDaemon(os.Args[0], "run", "--config", confpath)
|
manager.Run(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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,5 +7,5 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func printLogMessage(conf *service.Service) {
|
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 {
|
} else {
|
||||||
unit = "--user-unit"
|
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)
|
fmt.Printf("\t%sjournalctl -xe %s %s.service\n", sudo, unit, conf.Name)
|
||||||
if !conf.System {
|
if !conf.System {
|
||||||
fmt.Println("\nIf that's not the case, see https://unix.stackexchange.com/a/486566/45554.")
|
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) {
|
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