diff --git a/README.md b/README.md index 6c2c600..74db9e5 100644 --- a/README.md +++ b/README.md @@ -2,34 +2,347 @@ A cross-platform service manager. -Goal: +Because debugging launchctl, systemd, etc absolutely sucks! + +...and I wanted a reasonable way to install [Telebit](https://telebit.io) on Windows. +(see more in the **Why** section below) + +
+User Mode Services + * `sytemctl --user` on Linux + * `launchctl` on MacOS + * `HKEY_CURRENT_USER/.../Run` on Windows +
+
+System Services + * `sudo sytemctl` on Linux + * `sudo launchctl` on MacOS + * _not yet implemented_ on Windows +
+ +- **Install** +- **Usage** +- **Build** +- **Examples** + - compiled programs + - scripts + - bash + - node + - python + - ruby +- **Logging** +- **Windows** +- **Debugging** +- **Why** +- **Legal** + +# Install + +Download `serviceman` for + +- [MacOS (64-bit darwin)](https://rootprojects.org/serviceman/dist/darwin/amd64/serviceman) +- [Windows 10 (64-bit)](https://rootprojects.org/serviceman/dist/windows/amd64/serviceman.exe) +- [Windows 10 (32-bit)](https://rootprojects.org/serviceman/dist/windows/386/serviceman.exe) +- [Linux (64-bit)](https://rootprojects.org/serviceman/dist/linux/amd64/serviceman) +- [Linux (32-bit)](https://rootprojects.org/serviceman/dist/linux/386/serviceman) +- [Raspberry Pi 4 (64-bit armv8)](https://rootprojects.org/serviceman/dist/linux/armv8/serviceman) +- [Raspberry Pi 3 (armv7)](https://rootprojects.org/serviceman/dist/linux/armv7/serviceman) +- [Raspberry Pi 2 (armv6)](https://rootprojects.org/serviceman/dist/linux/armv6/serviceman) +- [Raspberry Pi Zero (armv5)](https://rootprojects.org/serviceman/dist/linux/armv5/serviceman) + +# Usage + +```bash +serviceman add [options] [interpreter] -- [service options] +``` + +```bash +serviceman add --help +``` + +```bash +serviceman version +``` + +# Examples + +**Compiled Apps** + +Normally you might run your program something like this: + +```bash +dinglehopper --port 8421 +``` + +Adding a service for that program with `serviceman` would look like this: + +> **serviceman add** dinglehopper **--** --port 8421 + +`serviceman` will find `dinglehopper` in your PATH, but if you have +any arguments with relative paths, you should switch to using absolute paths. + +```bash +dinglehopper --config ./conf.json +``` + +becomes + +> **serviceman add** dinglehopper **--** --config **/Users/aj/dinglehopper/conf.json** + +
+Using with scripts + +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: + +> serviceman add **bash** ./snarfblat.sh **--** --port 8421 + +`serviceman` will resolve `./snarfblat.sh` correctly because it comes +before the **--**. + +**Background Information** + +An operating system can't "run" text files (even if the executable bit is set). + +Scripts require an _interpreter_. Often this is denoted at the top of +"executable" scripts with something like one of these: ```bash -serviceman install [options] [interpreter] [-- [options]] +#!/usr/bin/env ruby ``` ```bash -serviceman install --user ./foo-app -- -c ./ +#!/usr/bin/python ``` +However, sometimes people get fancy and pass arguments to the interpreter, +like this: + ```bash -serviceman install --user /usr/local/bin/node ./whatever.js -- -c ./ +#!/usr/local/bin/node --harmony --inspect ``` +
+ +
+Using with node.js + +If normally you run your node script something like this: + ```bash -serviceman run --config conf.json +node ./demo.js --foo bar --baz ``` +Then you would add it as a system service like this: + +> **serviceman add** node ./demo.js **--** --foo bar --baz + +It is important that you specify `node ./demo.js` and not just `./demo.js` + +See **Using with scripts** for more detailed information. + +
+ +
+Using with python + +If normally you run your python script something like this: + +```bash +python ./demo.py --foo bar --baz +``` + +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` + +See **Using with scripts** for more detailed information. + +
+ +
+Using with ruby + +If normally you run your ruby script something like this: + +```bash +ruby ./demo.rb --foo bar --baz +``` + +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` + +See **Using with scripts** for more detailed information. + +
+ +# Logging + +When you run `serviceman add` it will either give you an error or +will print out the location where logs will be found. + +By default it's one of these: + +```txt +~/.local/share//var/log/.log +``` + +```txt +/var/log//var/log/.log +``` + +You set it with one of these: + +- `--logdir ` (cli) +- `"logdir": ""` (json) +- `Logdir: ""` (go) + +If anything about the logging sucks, tell me... unless they're your logs +(which they probably are), in which case _you_ should fix them. + +That said, my goal is that it shouldn't take an IT genius to interpret +why your app failed to start. + +# Peculiarities of Windows + +Windows doesn't have a userspace daemon launcher. +This means that if your application crashes, it won't automatically restart. + +However, `serviceman` handles this by not directly adding your application +to `HKEY_CURRENT_USER/.../Run`, but rather installing a copy of _itself_ +instead, which runs your application and automatically restarts it whenever it +exits. + +If the application fails to start `serviceman` will retry continually, +but it does have an exponential backoff of up to 1 minute between failed +restart attempts. + +See the bit on `serviceman run` in the **Debugging** section down below for more information. + +# Debugging + +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. + +The config files generate by `serviceman` are simple, template-generated and +tested, and therefore gauranteed to work - **_if_** your +application runs with the parameters given, which is big 'if'. + +`serviceman` tries to make sure that all necessary files and folders +exist and give clear error messages if they don't (be sure to check the logs, +mentioned above). + +There's also a `run` utility that can be used to test that the parameters +you've given are being interpreted correctly (absolute paths and such). + +```bash +serviceman run --config ./conf.json +``` + +Where `conf.json` looks something like + +**For Binaries**: + +```json +{ + "title": "Demo", + "exec": "/Users/aj/go-demo/demo", + "argv": ["--foo", "bar", "--baz", "qux"] +} +``` + +**For Scripts**: + +Scripts can't be run directly. They require a binary `interpreter` - bash, node, ruby, python, etc. + +If you're running from the folder containing `./demo.js`, +and `node.exe` is in your PATH, then you can use executable +names and relative paths. + ```json { - "interpreter": "/Program Files (x86)/node/node.exe", - "exec": "/Users/aj/demo/demo.js", - "argv": ["--foo", "bar", "--baz", "qux"] + "title": "Demo", + "interpreter": "node.exe", + "exec": "./bin/demo.js", + "argv": ["--foo", "bar", "--baz", "qux"] } ``` +That's equivalent to this: + +```json +{ + "title": "Demo", + + "name": "demo", + + "exec": "node.exe", + "argv": ["./bin/demo.js", "--foo", "bar", "--baz", "qux"] +} +``` + +Making `add` and `run` take the exact same arguments is on the TODO list. +The fact that they don't is an artifact of `run` being created specifically +for Windows. + +If you have gripes about it, tell me. It shouldn't suck. That's the goal anyway. + +# Building + +```bash +git clone https://git.coolaj86.com/coolaj86/go-serviceman.git +``` + +```bash +pushd ./go-serviceman +``` + ```bash go generate -mod=vendor ./... -go build -mod=vendor -ldflags "-H=windowsgui" -.\\go-serviceman node ./demo.js -- --foo bar --baz qux -``` \ No newline at end of file +``` + +**Windows**: + +```bash +go build -mod=vendor -ldflags "-H=windowsgui" -o serviceman.exe +``` + +**Linux, MacOS**: + +```bash +go build -mod=vendor -o /usr/local/bin/serviceman +``` + +# Why + +I created this for two reasons: + +1. Too often I just run services in `screen -xRS foo` because systemd `.service` files are way too hard to get right and even harder to debug. I make stupid typos or config mistakes and get it wrong. Then I get a notice 18 months later from digital ocean that NYC region 3 is being rebooted and to expect 5 seconds of downtime... and I don't remember if I remembered to go back and set up that service with systemd or not. +2. To make it easier for people to install [Telebit](https://telebit.io) on Windows. + + + +# Legal + +[serviceman](https://git.coolaj86.com/coolaj86/go-serviceman) | +MPL-2.0 | +[Terms of Use](https://therootcompany.com/legal/#terms) | +[Privacy Policy](https://therootcompany.com/legal/#privacy) + +Copyright 2019 AJ ONeal. + + diff --git a/build-all.sh b/build-all.sh new file mode 100644 index 0000000..0eb5b5d --- /dev/null +++ b/build-all.sh @@ -0,0 +1,41 @@ +#GOOS=windows GOARCH=amd64 go install +#go tool dist list + +# TODO move this into tools/build.go + +export CGO_ENABLED=0 +exe=serviceman +gocmd=. + +echo "" +go generate -mod=vendor ./... + +echo "" +echo "Windows amd64" +GOOS=windows GOARCH=amd64 go build -mod=vendor -o dist/windows/amd64/${exe}.exe -ldflags "-H=windowsgui" $gocmd +echo "Windows 386" +GOOS=windows GOARCH=386 go build -mod=vendor -o dist/windows/386/${exe}.exe -ldflags "-H=windowsgui" $gocmd + +echo "" +echo "Darwin (macOS) amd64" +GOOS=darwin GOARCH=amd64 go build -mod=vendor -o dist/darwin/amd64/${exe} $gocmd + +echo "" +echo "Linux amd64" +GOOS=linux GOARCH=amd64 go build -mod=vendor -o dist/linux/amd64/${exe} $gocmd +echo "Linux 386" +GOOS=linux GOARCH=386 go build -mod=vendor -o dist/linux/386/${exe} $gocmd + +echo "" +echo "RPi 4 (64-bit) ARMv8" +GOOS=linux GOARCH=arm64 go build -mod=vendor -o dist/linux/armv8/${exe} $gocmd +echo "RPi 3 B+ ARMv7" +GOOS=linux GOARCH=arm GOARM=7 go build -mod=vendor -o dist/linux/armv7/${exe} $gocmd +echo "ARMv6" +GOOS=linux GOARCH=arm GOARM=6 go build -mod=vendor -o dist/linux/armv6/${exe} $gocmd +echo "RPi Zero ARMv5" +GOOS=linux GOARCH=arm GOARM=5 go build -mod=vendor -o dist/linux/armv5/${exe} $gocmd + +echo "" +rsync -av ./dist/ ubuntu@rootprojects.org:/srv/www/rootprojects.org/serviceman/dist/ +# https://rootprojects.org/serviceman/dist/windows/amd64/serviceman.exe diff --git a/installer/b0x.toml b/manager/b0x.toml similarity index 100% rename from installer/b0x.toml rename to manager/b0x.toml diff --git a/installer/dist/Library/LaunchDaemons/_rdns_.plist.tmpl b/manager/dist/Library/LaunchDaemons/_rdns_.plist.tmpl similarity index 100% rename from installer/dist/Library/LaunchDaemons/_rdns_.plist.tmpl rename to manager/dist/Library/LaunchDaemons/_rdns_.plist.tmpl diff --git a/installer/dist/etc/systemd/system/_name_.service.tmpl b/manager/dist/etc/systemd/system/_name_.service.tmpl similarity index 100% rename from installer/dist/etc/systemd/system/_name_.service.tmpl rename to manager/dist/etc/systemd/system/_name_.service.tmpl diff --git a/installer/doc.go b/manager/doc.go similarity index 58% rename from installer/doc.go rename to manager/doc.go index ec2b059..9c757e8 100644 --- a/installer/doc.go +++ b/manager/doc.go @@ -1,7 +1,7 @@ -// Package installer can be used cross-platform to install apps +// Package manager can be used cross-platform to add apps // as either userspace or system services for fairly simple applications. -// This is not intended for complex installers. +// This is not intended for complex or highly platform-specific service installers. // // I'm prototyping this out to be useful for more than just watchdog // hence there are a few unnecessary things for the sake of the trying it out -package installer +package manager diff --git a/installer/filesystem.go b/manager/filesystem.go similarity index 96% rename from installer/filesystem.go rename to manager/filesystem.go index d0395b5..baaebec 100644 --- a/installer/filesystem.go +++ b/manager/filesystem.go @@ -1,4 +1,4 @@ -package installer +package manager import ( "io" diff --git a/installer/install.go b/manager/install.go similarity index 98% rename from installer/install.go rename to manager/install.go index 18ffd4d..70e0c25 100644 --- a/installer/install.go +++ b/manager/install.go @@ -1,6 +1,6 @@ //go:generate go run -mod=vendor github.com/UnnoTed/fileb0x b0x.toml -package installer +package manager import ( "fmt" diff --git a/installer/install_darwin.go b/manager/install_darwin.go similarity index 95% rename from installer/install_darwin.go rename to manager/install_darwin.go index 5ff21bd..a2ea38e 100644 --- a/installer/install_darwin.go +++ b/manager/install_darwin.go @@ -1,4 +1,4 @@ -package installer +package manager import ( "bytes" @@ -9,7 +9,7 @@ import ( "strings" "text/template" - "git.rootprojects.org/root/go-serviceman/installer/static" + "git.rootprojects.org/root/go-serviceman/manager/static" "git.rootprojects.org/root/go-serviceman/service" ) diff --git a/installer/install_linux.go b/manager/install_linux.go similarity index 95% rename from installer/install_linux.go rename to manager/install_linux.go index 3aa2f39..5d29443 100644 --- a/installer/install_linux.go +++ b/manager/install_linux.go @@ -1,4 +1,4 @@ -package installer +package manager import ( "bytes" @@ -8,7 +8,7 @@ import ( "path/filepath" "text/template" - "git.rootprojects.org/root/go-serviceman/installer/static" + "git.rootprojects.org/root/go-serviceman/manager/static" "git.rootprojects.org/root/go-serviceman/service" ) diff --git a/installer/install_other.go b/manager/install_other.go similarity index 89% rename from installer/install_other.go rename to manager/install_other.go index 3a88a5b..ff4c411 100644 --- a/installer/install_other.go +++ b/manager/install_other.go @@ -1,6 +1,6 @@ // +build !windows,!linux,!darwin -package installer +package manager import ( "git.rootprojects.org/root/go-serviceman/service" diff --git a/installer/install_windows.go b/manager/install_windows.go similarity index 99% rename from installer/install_windows.go rename to manager/install_windows.go index 2c2554c..e466644 100644 --- a/installer/install_windows.go +++ b/manager/install_windows.go @@ -1,4 +1,4 @@ -package installer +package manager import ( "encoding/json" diff --git a/installer/static/ab0x.go b/manager/static/ab0x.go similarity index 99% rename from installer/static/ab0x.go rename to manager/static/ab0x.go index b2497db..686907b 100644 --- a/installer/static/ab0x.go +++ b/manager/static/ab0x.go @@ -1,4 +1,4 @@ -// Code generated by fileb0x at "2019-07-02 00:24:56.633505 -0600 MDT m=+0.003518020" from config file "b0x.toml" DO NOT EDIT. +// Code generated by fileb0x at "2019-07-04 01:21:07.139664 -0600 MDT m=+0.003157447" from config file "b0x.toml" DO NOT EDIT. // modification hash(7ce890c82a9c0a430fe55cf7f579e8b4.acdb557394f98d3c09c0bb4d4b9142f8) package static diff --git a/installer/whoami.go b/manager/whoami.go similarity index 91% rename from installer/whoami.go rename to manager/whoami.go index e3f5e71..eb54b02 100644 --- a/installer/whoami.go +++ b/manager/whoami.go @@ -1,6 +1,6 @@ // +build !windows -package installer +package manager import "os/user" diff --git a/installer/whoami_windows.go b/manager/whoami_windows.go similarity index 98% rename from installer/whoami_windows.go rename to manager/whoami_windows.go index 721da06..504e907 100644 --- a/installer/whoami_windows.go +++ b/manager/whoami_windows.go @@ -1,4 +1,4 @@ -package installer +package manager import ( "fmt" diff --git a/serviceman.go b/serviceman.go index 9d40a6e..0718eea 100644 --- a/serviceman.go +++ b/serviceman.go @@ -12,7 +12,7 @@ import ( "strings" "time" - "git.rootprojects.org/root/go-serviceman/installer" + "git.rootprojects.org/root/go-serviceman/manager" "git.rootprojects.org/root/go-serviceman/runner" "git.rootprojects.org/root/go-serviceman/service" ) @@ -22,7 +22,7 @@ var GitVersion = "v0.0.0" var GitTimestamp = time.Now().Format(time.RFC3339) func usage() { - fmt.Println("Usage: serviceman install ./foo-app -- --foo-arg") + fmt.Println("Usage: serviceman add ./foo-app -- --foo-arg") fmt.Println("Usage: serviceman run --config ./foo-app.json") } @@ -36,8 +36,10 @@ func main() { top := os.Args[1] os.Args = append(os.Args[:1], os.Args[2:]...) switch top { - case "install": - install() + case "version": + fmt.Println(GitVersion, GitTimestamp, GitRev) + case "add": + add() case "run": run() default: @@ -47,7 +49,7 @@ func main() { } } -func install() { +func add() { conf := &service.Service{ Restart: true, } @@ -73,8 +75,8 @@ func install() { 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.ReverseDNS, "rdns", "", "a plist-friendly Reverse DNS name for launchctl (ex: com.example.foo-app)") - flag.BoolVar(&forSystem, "system", false, "attempt to install system service as an unprivileged/unelevated user") - flag.BoolVar(&forUser, "user", false, "install user space / user mode service even when admin/root/sudo/elevated") + 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(&force, "force", false, "if the interpreter or executable doesn't exist, or things don't make sense, try anyway") flag.StringVar(&conf.User, "username", "", "run the service as this user") flag.StringVar(&conf.Group, "groupname", "", "run the service as this group") @@ -92,17 +94,17 @@ func install() { } else if forSystem { conf.System = true } else { - conf.System = installer.IsPrivileged() + conf.System = manager.IsPrivileged() } n := len(args) if 0 == n { - fmt.Println("Usage: serviceman install ./foo-app -- --foo-arg") + fmt.Println("Usage: serviceman add ./foo-app -- --foo-arg") os.Exit(2) return } - execpath, err := installer.WhereIs(args[0]) + execpath, err := manager.WhereIs(args[0]) if nil != err { fmt.Fprintf(os.Stderr, "Error: '%s' could not be found.\n", args[0]) if !force { @@ -125,11 +127,11 @@ func install() { //fmt.Printf("\n%#v\n\n", conf) - err = installer.Install(conf) + err = manager.Install(conf) if nil != err { fmt.Fprintf(os.Stderr, "%s\n", err) - fmt.Fprintf(os.Stderr, "Use 'sudo' to install as a privileged system service.\n") - fmt.Fprintf(os.Stderr, "Use '--user' to install as an user service.\n") + fmt.Fprintf(os.Stderr, "Use 'sudo' to add service as a privileged system service.\n") + fmt.Fprintf(os.Stderr, "Use '--user' to add service as an user service.\n") } }