v0.2.0
This commit is contained in:
parent
63328251fa
commit
555edebf20
335
README.md
335
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)
|
||||
|
||||
<details>
|
||||
<summary>User Mode Services</summary>
|
||||
* `sytemctl --user` on Linux
|
||||
* `launchctl` on MacOS
|
||||
* `HKEY_CURRENT_USER/.../Run` on Windows
|
||||
</details>
|
||||
<details>
|
||||
<summary>System Services</summary>
|
||||
* `sudo sytemctl` on Linux
|
||||
* `sudo launchctl` on MacOS
|
||||
* _not yet implemented_ on Windows
|
||||
</details>
|
||||
|
||||
- **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 install [options] [interpreter] <service> [-- [options]]
|
||||
serviceman add [options] [interpreter] <service> -- [service options]
|
||||
```
|
||||
|
||||
```bash
|
||||
serviceman install --user ./foo-app -- -c ./
|
||||
serviceman add --help
|
||||
```
|
||||
|
||||
```bash
|
||||
serviceman install --user /usr/local/bin/node ./whatever.js -- -c ./
|
||||
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**
|
||||
|
||||
<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:
|
||||
|
||||
> 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
|
||||
#!/usr/bin/env ruby
|
||||
```
|
||||
|
||||
```bash
|
||||
serviceman run --config conf.json
|
||||
#!/usr/bin/python
|
||||
```
|
||||
|
||||
However, sometimes people get fancy and pass arguments to the interpreter,
|
||||
like this:
|
||||
|
||||
```bash
|
||||
#!/usr/local/bin/node --harmony --inspect
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Using with node.js</summary>
|
||||
|
||||
If normally you run your node script something like this:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Using with python</summary>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Using with ruby</summary>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
# 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/<NAME>/var/log/<NAME>.log
|
||||
```
|
||||
|
||||
```txt
|
||||
/var/log/<NAME>/var/log/<NAME>.log
|
||||
```
|
||||
|
||||
You set it with one of these:
|
||||
|
||||
- `--logdir <path>` (cli)
|
||||
- `"logdir": "<path>"` (json)
|
||||
- `Logdir: "<path>"` (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
|
||||
{
|
||||
"interpreter": "/Program Files (x86)/node/node.exe",
|
||||
"exec": "/Users/aj/demo/demo.js",
|
||||
"argv": ["--foo", "bar", "--baz", "qux"]
|
||||
"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
|
||||
{
|
||||
"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
|
||||
```
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
<!-- {{ if .Legal }} -->
|
||||
|
||||
# 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.
|
||||
|
||||
<!-- {{ end }} -->
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
package installer
|
||||
package manager
|
||||
|
||||
import (
|
||||
"io"
|
|
@ -1,6 +1,6 @@
|
|||
//go:generate go run -mod=vendor github.com/UnnoTed/fileb0x b0x.toml
|
||||
|
||||
package installer
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// +build !windows,!linux,!darwin
|
||||
|
||||
package installer
|
||||
package manager
|
||||
|
||||
import (
|
||||
"git.rootprojects.org/root/go-serviceman/service"
|
|
@ -1,4 +1,4 @@
|
|||
package installer
|
||||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
|
@ -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
|
|
@ -1,6 +1,6 @@
|
|||
// +build !windows
|
||||
|
||||
package installer
|
||||
package manager
|
||||
|
||||
import "os/user"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package installer
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue