better arg handling, more descriptive output

This commit is contained in:
AJ ONeal 2019-07-13 20:50:00 -06:00
parent 389b88331d
commit 40a82f26c4
12 changed files with 563 additions and 192 deletions

193
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>
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.

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 {

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
}

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
}

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
}

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)

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
}

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
}
}

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
exeargs, err := testScript(flagargs[0], force)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(3)
return
}
execpath, err := manager.WhereIs(args[0])
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
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
}
} 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)
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)
}

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)
}

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.")

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)
}