Compare commits

...

50 Commits

Author SHA1 Message Date
32c71bd698 add webi install instructions 2020-07-06 02:42:08 +00:00
8277fa7ac6 add canonical link 2020-04-22 19:22:41 +00:00
3b6c4bbb7d update curl command 2020-01-18 02:18:08 +00:00
1196f1d389 more generic download command 2020-01-18 02:16:15 +00:00
2824ee4c62 test service directory creation 2019-08-10 20:13:51 -06:00
e7a02191d8 lint: clarify double newline 2019-08-10 20:12:43 -06:00
693e61d7d4 address windows bug with incomplete args[0] paths 2019-08-10 15:54:56 -06:00
258623ae44 update generated files 2019-08-10 15:54:34 -06:00
8b6479dc95 update serviceman on npm 2019-08-09 01:20:17 -06:00
3513b64aa7 add 'serviceman list --all' to docs 2019-08-05 10:09:11 -06:00
b64dbc6ca6 build script: don't publish after build 2019-08-05 10:07:18 -06:00
6c6c0123ed clarify generated comment 2019-08-05 10:05:39 -06:00
AJ ONeal
b7989893cd tested and fixed on Windows 2019-08-05 10:03:07 -06:00
c84dc517a9 add error hints 2019-08-05 05:57:50 -06:00
34ed9cc065 list() for Mac and Linux 2019-08-05 05:53:32 -06:00
f03a0755af list Windows user services 2019-08-05 05:20:50 -06:00
cc0176e058 add serviceman comments 2019-08-05 05:20:50 -06:00
b3cbca14c6 template update 2019-08-05 04:51:00 -06:00
6ec2de0602 minor doc update for PATH 2019-08-05 04:47:27 -06:00
76710d58fa progress 2019-08-05 04:43:38 -06:00
94c00a777d M manager/dist/etc/systemd/system/_name_.service.tmpl
M  serviceman.go
2019-08-05 04:43:38 -06:00
c78cd82059 fix #5: only use group when specified in both dry-run and real templates 2019-08-05 04:40:47 -06:00
c8453f8d54 make chmod instruction more obvious 2019-07-28 05:14:33 -06:00
04ee9550ee bump source build ver 2019-07-14 02:06:45 -06:00
a31ba75927 v0.5.2: fix postinstall, really 2019-07-14 01:57:43 -06:00
386a6694e3 v0.5.1: fix postinstall 2019-07-14 01:55:35 -06:00
f97f217bc6 v0.5.0: Initial Release 2019-07-14 01:53:17 -06:00
f95897cf30 show simple example sooner 2019-07-13 21:14:32 -06:00
40a82f26c4 better arg handling, more descriptive output 2019-07-13 20:50:00 -06:00
389b88331d bump 2019-07-10 02:01:12 -06:00
8e1bd12df7 bugfix: also enable on restart on linux 2019-07-10 02:00:43 -06:00
dd25ba0787 update README 2019-07-10 01:55:01 -06:00
f9631d852a update README 2019-07-10 01:48:55 -06:00
b74d9f4332 mark latest dirty version for source builds 2019-07-10 01:26:09 -06:00
4c44f70ec3 Windows: start on install. Add stop/start to all. 2019-07-10 01:18:06 -06:00
58cf28df8e Update 'README.md' 2019-07-09 03:02:25 +00:00
7035ede0b9 bugfix service name handling 2019-07-07 02:14:25 -06:00
d610b8cb61 static: update timestamp 2019-07-07 01:34:17 -06:00
AJ ONeal
75ae3eea07 slightly more friendly service start output 2019-07-07 06:55:05 +00:00
AJ ONeal
04e162b20f Ubuntu: start on install 2019-07-07 06:48:25 +00:00
2b7148c3ae MacOS: start on install 2019-07-05 12:48:58 -06:00
00749b3465 win: copy debug self to debug location 2019-07-04 03:36:58 -06:00
3252a7f39f note more windows peculiarities 2019-07-04 03:31:18 -06:00
1c4bfd4baf windows: show powershell command with silenced download 2019-07-04 03:17:18 -06:00
829247b4c0 output message about logs 2019-07-04 03:02:27 -06:00
f1e3cf1b9c clarify usage 2019-07-04 02:55:59 -06:00
9e01cacf05 cleaning up docs 2019-07-04 02:51:37 -06:00
dc43f6fd7b make curl-able (even on windows!) 2019-07-04 01:56:38 -06:00
2d16076d7b workaround gitea markdown limitations 2019-07-04 01:40:22 -06:00
1019300efb workaround gitea markdown limitations 2019-07-04 01:39:43 -06:00
43 changed files with 3323 additions and 288 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
*~
.*~
# ---> Go
# Binaries for programs and plugins
*.exe

505
README.md
View File

@ -1,112 +1,325 @@
# go-serviceman
# [go-serviceman](https://git.rootprojects.org/root/serviceman)
A cross-platform service manager.
Cross-platform service management made easy.
Because debugging launchctl, systemd, etc absolutely sucks!
> sudo serviceman add --name foo ./serve.js --port 3000
...and I wanted a reasonable way to install [Telebit](https://telebit.io) on Windows.
(see more in the **Why** section below)
> Success: "foo" started as a "launchd" SYSTEM service, running as "root"
<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>
## Why?
- **Install**
- **Usage**
- **Build**
- **Examples**
- compiled programs
- scripts
- bash
- node
- python
- ruby
- **Logging**
- **Windows**
- **Debugging**
- **Why**
- **Legal**
Because it sucks to debug launchctl, systemd, etc.
# Install
Also, I wanted a reasonable way to install [Telebit](https://telebit.io) on Windows.
(see more in the **More Why** section below)
Download `serviceman` for
## Features
- [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)
- Unprivileged (User Mode) Services with `--user` (_Default_)
- [x] Linux (`sytemctl --user`)
- [x] MacOS (`launchctl`)
- [x] Windows (`HKEY_CURRENT_USER/.../Run`)
- Privileged (System) Services with `--system` (_Default_ for `root`)
- [x] Linux (`sudo sytemctl`)
- [x] MacOS (`sudo launchctl`)
- [ ] Windows (_not yet implemented_)
# Table of Contents
- Usage
- Install
- Examples
- compiled programs
- scripts
- bash
- node
- python
- ruby
- PATH
- Logging
- Debugging
- Windows
- Building
- More Why
- Legal
# Usage
```bash
serviceman add [options] [interpreter] <service> -- [service options]
```
The basic pattern of usage:
```bash
sudo serviceman add --name "foobar" [options] [interpreter] <service> [--] [service options]
sudo serviceman start <service>
sudo serviceman stop <service>
sudo serviceman list --all
serviceman version
```
And what that might look like:
```bash
sudo serviceman add --name "foo" foo.exe -c ./config.json
```
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
You can install `serviceman` directly from the official git releases with [`webi`](https://webinstall.dev/serviceman):
**Mac**, **Linux**:
```bash
serviceman version
curl -sL https://webinstall.dev/serviceman | bash
```
**Windows 10**:
```pwsh
curl.exe -sLA "MS" https://webinstall.dev/serviceman | powershell
```
You can run this from cmd.exe or PowerShell (curl.exe is a native part of Windows 10).
## Manual Install
There are a number of pre-built binaries.
If none of them work for you, or you prefer to build from source,
see the instructions for building far down below.
## Downloads
```
curl -fsSL "https://rootprojects.org/serviceman/dist/$(uname -s)/$(uname -m)/serviceman" -o serviceman
chmod +x ./serviceman
```
### MacOS
<details>
<summary>See download options</summary>
MacOS (darwin): [64-bit Download ](https://rootprojects.org/serviceman/dist/darwin/amd64/serviceman)
```
curl https://rootprojects.org/serviceman/dist/darwin/amd64/serviceman -o serviceman
chmod +x ./serviceman
```
</details>
### Windows
<details>
<summary>See download options</summary>
Windows 10: [64-bit Download](https://rootprojects.org/serviceman/dist/windows/amd64/serviceman.exe)
```
powershell.exe $ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest https://rootprojects.org/serviceman/dist/windows/amd64/serviceman.exe -OutFile serviceman.exe
```
**Debug version**:
```
powershell.exe $ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest https://rootprojects.org/serviceman/dist/windows/amd64/serviceman.debug.exe -OutFile serviceman.debug.exe
```
Windows 7: [32-bit Download](https://rootprojects.org/serviceman/dist/windows/386/serviceman.exe)
```
powershell.exe "(New-Object Net.WebClient).DownloadFile('https://rootprojects.org/serviceman/dist/windows/386/serviceman.exe', 'serviceman.exe')"
```
**Debug version**:
```
powershell.exe "(New-Object Net.WebClient).DownloadFile('https://rootprojects.org/serviceman/dist/windows/386/serviceman.debug.exe', 'serviceman.debug.exe')"
```
</details>
### Linux
<details>
<summary>See download options</summary>
Linux (64-bit): [Download](https://rootprojects.org/serviceman/dist/linux/amd64/serviceman)
```
curl https://rootprojects.org/serviceman/dist/linux/amd64/serviceman -o serviceman
chmod +x ./serviceman
```
Linux (32-bit): [Download](https://rootprojects.org/serviceman/dist/linux/386/serviceman)
```
curl https://rootprojects.org/serviceman/dist/linux/386/serviceman -o serviceman
chmod +x ./serviceman
```
</details>
### Raspberry Pi (Linux ARM)
<details>
<summary>See download options</summary>
RPi 4 (64-bit armv8): [Download](https://rootprojects.org/serviceman/dist/linux/armv8/serviceman)
```
curl https://rootprojects.org/serviceman/dist/linux/armv8/serviceman -o serviceman`
chmod +x ./serviceman
```
RPi 3 (armv7): [Download](https://rootprojects.org/serviceman/dist/linux/armv7/serviceman)
```
curl https://rootprojects.org/serviceman/dist/linux/armv7/serviceman -o serviceman
chmod +x ./serviceman
```
ARMv6: [Download](https://rootprojects.org/serviceman/dist/linux/armv6/serviceman)
```
curl https://rootprojects.org/serviceman/dist/linux/armv6/serviceman -o serviceman
chmod +x ./serviceman
```
RPi Zero (armv5): [Download](https://rootprojects.org/serviceman/dist/linux/armv5/serviceman)
```
curl https://rootprojects.org/serviceman/dist/linux/armv5/serviceman -o serviceman
chmod +x ./serviceman
```
</details>
### Add to PATH
**Windows**
```
mkdir %userprofile%\bin
move serviceman.exe %userprofile%\bin\serviceman.exe
reg add HKEY_CURRENT_USER\Environment /v PATH /d "%PATH%;%userprofile%\bin"
```
**All Others**
```
sudo mv ./serviceman /usr/local/bin/
```
# Examples
**Compiled Apps**
```bash
sudo serviceman add --name <name> <program> [options] [--] [raw options]
Normally you might run your program something like this:
# 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
dinglehopper --port 8421
# 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:
```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
`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
sudo serviceman add --name "gizmo" gizmo run --port 8421 --config envs/prod.ini
```
becomes
serviceman will find `gizmo` in your PATH and resolve `envs/prod.ini` to its absolute path.
> **serviceman add** dinglehopper **--** --config **/Users/aj/dinglehopper/conf.json**
</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**
@ -115,7 +328,7 @@ 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
```
@ -130,6 +343,8 @@ like this:
#!/usr/local/bin/node --harmony --inspect
```
Serviceman understands all 3 of those approaches.
</details>
<details>
@ -138,14 +353,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.
@ -157,14 +395,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.
@ -176,21 +415,65 @@ 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>
<details>
<summary>Setting PATH</summary>
You can set the `$PATH` (`%PATH%` on Windows) for your service like this:
```bash
sudo serviceman add ./myservice --path "/home/myuser/bin"
```
Snapshot your actual path like this:
```bash
sudo serviceman add ./myservice --path "$PATH"
```
Remember that this takes a snapshot and sets it in the configuration, it's not
a live reference to your path.
</details>
## Hints
- 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`
```
# Example of a / that isn't a path
# (it needs to be escaped with --)
sudo serviceman add dinglehopper config/prod -- --category color/blue
```
# Logging
### Linux
```bash
sudo journalctl -xef --unit <NAME>
sudo journalctl -xef --user-unit <NAME>
```
### Mac, Windows
When you run `serviceman add` it will either give you an error or
will print out the location where logs will be found.
@ -201,14 +484,14 @@ By default it's one of these:
```
```txt
/var/log/<NAME>/var/log/<NAME>.log
/opt/<NAME>/var/log/<NAME>.log
```
You set it with one of these:
- `--logdir <path>` (cli)
- `"logdir": "<path>"` (json)
- `Logdir: "<path>"` (go)
- `--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.
@ -216,24 +499,11 @@ If anything about the logging sucks, tell me... unless they're your logs
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
- `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.
@ -260,7 +530,7 @@ Where `conf.json` looks something like
```json
{
"title": "Demo",
"exec": "/Users/aj/go-demo/demo",
"exec": "/Users/me/go-demo/demo",
"argv": ["--foo", "bar", "--baz", "qux"]
}
```
@ -301,6 +571,39 @@ for Windows.
If you have gripes about it, tell me. It shouldn't suck. That's the goal anyway.
## Peculiarities of Windows
# Console vs No Console
Windows binaries can be built either for the console or the GUI.
When they're built for the console they can hide themselves when they start.
They must open up a terminal window.
When they're built for the GUI they can't print any output - even if they're started in the terminal.
This is why there's a **Debug version** for the windows binaries -
so that you can get your arguments correct with the one and then
switch to the other.
There's probably a clever way to work around this, but I don't know what it is yet.
# No userspace launcher
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 up above for more information.
# Building
```bash
@ -327,7 +630,7 @@ go build -mod=vendor -ldflags "-H=windowsgui" -o serviceman.exe
go build -mod=vendor -o /usr/local/bin/serviceman
```
# Why
# More Why
I created this for two reasons:

View File

@ -13,8 +13,10 @@ 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
GOOS=windows GOARCH=amd64 go build -mod=vendor -o dist/windows/amd64/${exe}.debug.exe
echo "Windows 386"
GOOS=windows GOARCH=386 go build -mod=vendor -o dist/windows/386/${exe}.exe -ldflags "-H=windowsgui" $gocmd
GOOS=windows GOARCH=386 go build -mod=vendor -o dist/windows/386/${exe}.debug.exe
echo ""
echo "Darwin (macOS) amd64"
@ -37,5 +39,5 @@ 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/
#rsync -av ./dist/ ubuntu@rootprojects.org:/srv/www/rootprojects.org/serviceman/dist/
# https://rootprojects.org/serviceman/dist/windows/amd64/serviceman.exe

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.12
require (
git.rootprojects.org/root/go-gitver v1.1.2
github.com/UnnoTed/fileb0x v1.1.3
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb
)

2
go.sum
View File

@ -26,6 +26,8 @@ github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936 h1:kw1v0NlnN+GZcU8Ma8CLF2Zzgjfx95gs3/GN3vYAPpo=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e h1:fvw0uluMptljaRKSU8459cJ4bmi3qUYyMs5kzpic2fY=

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated for serviceman. Edit as you wish, but leave this line. -->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
@ -27,6 +28,8 @@
{{if .User -}}
<key>UserName</key>
<string>{{ .User }}</string>
{{end -}}
{{if .Group -}}
<key>GroupName</key>
<string>{{ .Group }}</string>
<key>InitGroups</key>

View File

@ -1,3 +1,4 @@
# Generated for serviceman. Edit as you wish, but leave this line.
# Pre-req
# sudo mkdir -p {{ .Local }}/opt/{{ .Name }}/ {{ .Local }}/var/log/{{ .Name }}
{{ if .System -}}
@ -12,18 +13,20 @@
# sudo journalctl {{ if not .System -}} --user {{ end -}} -xefu {{ .Name }}
[Unit]
Description={{ .Title }} - {{ .Desc }}
Description={{ .Title }} {{ if .Desc }}- {{ .Desc }}{{ end }}
{{ if .URL -}}
Documentation={{ .URL }}
{{ end -}}
{{ if .System -}}
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service
{{- end }}
{{ end -}}
[Service]
# Restart on crash (bad signal), but not on 'clean' failure (error exit code)
# Allow up to 3 restarts within 10 seconds
# (it's unlikely that a user or properly-running script will do this)
Restart=on-abnormal
Restart=always
StartLimitInterval=10
StartLimitBurst=3
@ -33,10 +36,13 @@ User={{ .User }}
Group={{ .Group }}
{{ end -}}
{{- if .Envs }}
Environment="{{- range $key, $value := .Envs }}{{ $key }}={{ $value }};{{- end }}"
{{- end }}
{{ if .Workdir -}}
WorkingDirectory={{ .Workdir }}
{{ end -}}
ExecStart={{if .Interpreter }}{{ .Interpreter }} {{ end }}{{ .Exec }} {{- range $arg := .Argv }}{{ $arg }} {{- end }}
ExecStart={{if .Interpreter }}{{ .Interpreter }} {{ end }}{{ .Exec }}{{ range $arg := .Argv }} {{ $arg }}{{ end }}
ExecReload=/bin/kill -USR1 $MAINPID
{{if .Production -}}

View File

@ -7,13 +7,14 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"git.rootprojects.org/root/go-serviceman/service"
)
// 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
}
@ -23,23 +24,35 @@ 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 {
return start(conf)
}
func Stop(conf *service.Service) error {
return stop(conf)
}
func List(conf *service.Service) ([]string, []string, []error) {
return list(conf)
}
// IsPrivileged returns true if we suspect that the current user (or process) will be able
@ -57,3 +70,22 @@ func WhereIs(exe string) (string, error) {
}
return filepath.Abs(filepath.ToSlash(exepath))
}
type ManageError struct {
Name string
Hint string
Parent error
}
func (e *ManageError) Error() string {
return e.Name + ": " + e.Hint + ": " + e.Parent.Error()
}
type ErrDaemonize struct {
DaemonArgs []string
error string
}
func (e *ErrDaemonize) Error() string {
return e.error + "\nYou need to switch on ErrDaemonize, and use .DaemonArgs, which would run this:" + strings.Join(e.DaemonArgs, " ")
}

View File

@ -6,66 +6,169 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"
"git.rootprojects.org/root/go-serviceman/manager/static"
"git.rootprojects.org/root/go-serviceman/service"
)
func install(c *service.Service) 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")
}
}
plistDir := "/Library/LaunchDaemons/"
if !c.System {
plistDir = filepath.Join(c.Home, "Library/LaunchAgents")
}
const (
srvExt = ".plist"
srvSysPath = "/Library/LaunchDaemons"
srvUserPath = "Library/LaunchAgents"
)
// Check paths first
err := os.MkdirAll(filepath.Dir(plistDir), 0755)
var srvLen int
func init() {
srvLen = len(srvExt)
}
func start(conf *service.Service) error {
system := conf.System
home := conf.Home
rdns := conf.ReverseDNS
service, err := getService(system, home, rdns)
if nil != err {
return err
}
cmds := []Runnable{
Runnable{
Exec: "launchctl",
Args: []string{"unload", "-w", service},
Must: false,
},
Runnable{
Exec: "launchctl",
Args: []string{"load", "-w", service},
Must: true,
Badwords: []string{"No such file or directory", "service already loaded"},
},
}
cmds = adjustPrivs(system, cmds)
typ := "USER"
if system {
typ = "SYSTEM"
}
fmt.Printf("Starting launchd %s service...\n\n", typ)
for i := range cmds {
exe := cmds[i]
fmt.Println("\t" + exe.String())
err := exe.Run()
if nil != err {
return err
}
}
fmt.Println()
return nil
}
func stop(conf *service.Service) error {
system := conf.System
home := conf.Home
rdns := conf.ReverseDNS
service, err := getService(system, home, rdns)
if nil != err {
return err
}
cmds := []Runnable{
Runnable{
Exec: "launchctl",
Args: []string{"unload", service},
Must: false,
Badwords: []string{"No such file or directory", "Cound not find specified service"},
},
}
cmds = adjustPrivs(system, cmds)
fmt.Println()
typ := "USER"
if system {
typ = "SYSTEM"
}
fmt.Printf("Stopping launchd %s service...\n", typ)
for i := range cmds {
exe := cmds[i]
fmt.Println("\t" + exe.String())
err := exe.Run()
if nil != err {
return err
}
}
fmt.Println()
return nil
}
// Render will create a launchd .plist file using the simple internal template
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 err
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 err
return nil, err
}
err = tmpl.Execute(rw, c)
if nil != err {
return 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")
}
}
plistDir := srvSysPath
if !c.System {
plistDir = filepath.Join(c.Home, srvUserPath)
}
// Check paths first
err := os.MkdirAll(plistDir, 0755)
if nil != err {
return "", err
}
b, err := Render(c)
if nil != 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 {
if err := ioutil.WriteFile(plistPath, b, 0644); err != nil {
return "", fmt.Errorf("Error writing %s: %v", plistPath, err)
}
return fmt.Errorf("ioutil.WriteFile error: %v", err)
// TODO --no-start
err = start(c)
if nil != err {
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
}
fmt.Printf("Installed. To start '%s' run the following:\n", c.Name)
// TODO template config file
if "" != c.Home {
plistPath = strings.Replace(plistPath, c.Home, "~", 1)
}
sudo := ""
if c.System {
sudo = "sudo "
}
fmt.Printf("\t%slaunchctl load -w %s\n", sudo, plistPath)
return nil
return "launchd", nil
}

View File

@ -12,7 +12,221 @@ import (
"git.rootprojects.org/root/go-serviceman/service"
)
func install(c *service.Service) error {
var (
srvLen int
srvExt = ".service"
srvSysPath = "/etc/systemd/system"
// Not sure which of these it's supposed to be...
// * ~/.local/share/systemd/user/watchdog.service
// * ~/.config/systemd/user/watchdog.service
// https://wiki.archlinux.org/index.php/Systemd/User
// This seems to work on Ubuntu
srvUserPath = ".config/systemd/user"
)
func init() {
srvLen = len(srvExt)
}
func start(conf *service.Service) error {
system := conf.System
home := conf.Home
name := conf.ReverseDNS
_, err := getService(system, home, name)
if nil != err {
return err
}
var cmds []Runnable
if system {
cmds = []Runnable{
Runnable{
Exec: "systemctl",
Args: []string{"daemon-reload"},
Must: false,
},
Runnable{
Exec: "systemctl",
Args: []string{"stop", name + ".service"},
Must: false,
},
Runnable{
Exec: "systemctl",
Args: []string{"enable", name + ".service"},
Badwords: []string{"not found", "failed"},
Must: true,
},
Runnable{
Exec: "systemctl",
Args: []string{"start", name + ".service"},
Badwords: []string{"not found", "failed"},
Must: true,
},
}
} else {
cmds = []Runnable{
Runnable{
Exec: "systemctl",
Args: []string{"--user", "daemon-reload"},
Must: false,
},
Runnable{
Exec: "systemctl",
Args: []string{"stop", "--user", name + ".service"},
Must: false,
},
Runnable{
Exec: "systemctl",
Args: []string{"start", "--user", name + ".service"},
Badwords: []string{"not found", "failed"},
Must: true,
},
}
}
cmds = adjustPrivs(system, cmds)
typ := "USER MODE"
if system {
typ = "SYSTEM"
}
fmt.Printf("Starting systemd %s service unit...\n\n", typ)
for i := range cmds {
exe := cmds[i]
fmt.Println("\t" + exe.String())
err := exe.Run()
if nil != err {
return err
}
}
fmt.Println()
return nil
}
func stop(conf *service.Service) error {
system := conf.System
home := conf.Home
name := conf.ReverseDNS
_, err := getService(system, home, name)
if nil != err {
return err
}
var cmds []Runnable
badwords := []string{"Failed to stop"}
if system {
cmds = []Runnable{
Runnable{
Exec: "systemctl",
Args: []string{"stop", name + ".service"},
Must: true,
Badwords: badwords,
},
}
} else {
cmds = []Runnable{
Runnable{
Exec: "systemctl",
Args: []string{"stop", "--user", name + ".service"},
Must: true,
Badwords: badwords,
},
}
}
cmds = adjustPrivs(system, cmds)
fmt.Println()
typ := "USER MODE"
if system {
typ = "SYSTEM"
}
fmt.Printf("Stopping systemd %s service...\n", typ)
for i := range cmds {
exe := cmds[i]
fmt.Println("\t" + exe.String())
err := exe.Run()
if nil != err {
return err
}
}
fmt.Println()
return nil
}
// Render will create a systemd .service file using the simple internal template
func Render(c *service.Service) ([]byte, error) {
defaultUserGroup(c)
// 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) {
defaultUserGroup(c)
// Check paths first
serviceDir := srvSysPath
if !c.System {
serviceDir = filepath.Join(c.Home, srvUserPath)
err := os.MkdirAll(serviceDir, 0755)
if nil != err {
return "", err
}
}
b, err := Render(c)
if nil != err {
return "", err
}
// Write the file out
serviceName := c.Name + ".service"
servicePath := filepath.Join(serviceDir, serviceName)
if err := ioutil.WriteFile(servicePath, b, 0644); err != nil {
return "", fmt.Errorf("Error writing %s: %v", servicePath, err)
}
// TODO --no-start
err = start(c)
if nil != err {
sudo := ""
// --user-unit rather than --user --unit for older systemd
unit := "--user-unit"
if c.System {
sudo = "sudo "
unit = "--unit"
}
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 "systemd", nil
}
func defaultUserGroup(c *service.Service) {
// Linux-specific config options
if c.System {
if "" == c.User {
@ -22,56 +236,4 @@ func install(c *service.Service) error {
if "" == c.Group {
c.Group = c.User
}
serviceDir := "/etc/systemd/system/"
// Check paths first
serviceName := c.Name + ".service"
if !c.System {
// Not sure which of these it's supposed to be...
// * ~/.local/share/systemd/user/watchdog.service
// * ~/.config/systemd/user/watchdog.service
// https://wiki.archlinux.org/index.php/Systemd/User
serviceDir = filepath.Join(c.Home, ".local/share/systemd/user")
err := os.MkdirAll(filepath.Dir(serviceDir), 0755)
if nil != 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)
if nil != err {
return err
}
// Write the file out
servicePath := filepath.Join(serviceDir, serviceName)
if err := ioutil.WriteFile(servicePath, rw.Bytes(), 0644); err != nil {
return fmt.Errorf("ioutil.WriteFile error: %v", err)
}
// TODO template this as well?
userspace := ""
sudo := "sudo "
if !c.System {
userspace = "--user "
sudo = ""
}
fmt.Printf("System service installed as '%s'.\n", servicePath)
fmt.Printf("Run the following to start '%s':\n", c.Name)
fmt.Printf("\t" + sudo + "systemctl " + userspace + "daemon-reload\n")
fmt.Printf("\t"+sudo+"systemctl "+userspace+"restart %s.service\n", c.Name)
fmt.Printf("\t"+sudo+"journalctl "+userspace+"-xefu %s\n", c.Name)
return nil
}

74
manager/install_nixes.go Normal file
View File

@ -0,0 +1,74 @@
// +build !windows
package manager
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"strings"
"git.rootprojects.org/root/go-serviceman/service"
)
// this code is shared between Mac and Linux, but may diverge in the future
func list(c *service.Service) ([]string, []string, []error) {
confDir := srvSysPath
if !c.System {
confDir = filepath.Join(c.Home, srvUserPath)
}
// Enuser path exists
err := os.MkdirAll(confDir, 0755)
if nil != err {
return nil, nil, []error{err}
}
fis, err := ioutil.ReadDir(confDir)
if nil != err {
return nil, nil, []error{err}
}
managed := []string{}
others := []string{}
errs := []error{}
b := make([]byte, 256)
for i := range fis {
fi := fis[i]
if !strings.HasSuffix(strings.ToLower(fi.Name()), srvExt) || len(fi.Name()) <= srvLen {
continue
}
confFile := filepath.Join(confDir, fi.Name())
r, err := os.Open(confFile)
if nil != err {
errs = append(errs, &ManageError{
Name: confFile,
Hint: "Open file",
Parent: err,
})
continue
}
n, err := r.Read(b)
if nil != err {
errs = append(errs, &ManageError{
Name: confFile,
Hint: "Read file",
Parent: err,
})
continue
}
b = b[:n]
name := fi.Name()[:len(fi.Name())-srvLen]
if bytes.Contains(b, []byte("for serviceman.")) {
managed = append(managed, name)
} else {
others = append(others, name)
}
}
return managed, others, errs
}

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

@ -6,19 +6,32 @@ import (
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"git.rootprojects.org/root/go-serviceman/runner"
"git.rootprojects.org/root/go-serviceman/service"
"golang.org/x/sys/windows/registry"
)
var (
srvLen int
srvExt = ".json"
srvSysPath = "/opt/serviceman/etc"
srvUserPath = ".local/opt/serviceman/etc"
)
func init() {
srvLen = len(srvExt)
}
// TODO nab some goodness from https://github.com/takama/daemon
// 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
@ -56,9 +69,12 @@ func install(c *service.Service) error {
}
defer k.Close()
// Try to stop before trying to copy the file
_ = runner.Stop(c)
args, err := installServiceman(c)
if nil != err {
return err
return "", err
}
/*
@ -85,7 +101,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?
@ -93,24 +109,193 @@ func install(c *service.Service) error {
//fmt.Println(autorunKey, c.Title, regSZ)
k.SetStringValue(c.Title, regSZ)
return nil
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)
args = append(args, "--daemon")
return Run(args[0], args[1:]...)
}
func stop(conf *service.Service) error {
return runner.Stop(conf)
}
func list(c *service.Service) ([]string, []string, []error) {
var errs []error
regs, err := listRegistry(c)
if nil != err {
errs = append(errs, err)
}
cfgs, errors := listConfigs(c)
if 0 != len(errors) {
errs = append(errs, errors...)
}
managed := []string{}
for i := range cfgs {
managed = append(managed, cfgs[i].Name)
}
others := []string{}
for i := range regs {
reg := regs[i]
if 0 == len(cfgs) {
others = append(others, reg)
continue
}
var found bool
for j := range cfgs {
cfg := cfgs[j]
// Registry Value Names are case-insensitive
if strings.ToLower(reg) == strings.ToLower(cfg.Title) {
found = true
}
}
if !found {
others = append(others, reg)
}
}
return managed, others, errs
}
func getRunnerArgs(c *service.Service) []string {
self := os.Args[0]
debug := ""
if strings.Contains(self, "debug.exe") {
debug = "debug."
}
smdir := `\opt\serviceman`
// TODO support service level services (which probably wouldn't need serviceman)
smdir = filepath.Join(c.Home, ".local", smdir)
// for now we'll scope the runner to the name of the application
smbin := filepath.Join(smdir, `bin\serviceman.`+debug+c.Name+`.exe`)
confpath := filepath.Join(smdir, `etc`)
conffile := filepath.Join(confpath, c.Name+`.json`)
return []string{
smbin,
"run",
"--config",
conffile,
}
}
type winConf struct {
Filename string `json:"-"`
Name string `json:"name"`
Title string `json:"title"`
}
func listConfigs(c *service.Service) ([]winConf, []error) {
var errs []error
smdir := `\opt\serviceman`
if !c.System {
smdir = filepath.Join(c.Home, ".local", smdir)
}
confpath := filepath.Join(smdir, `etc`)
infos, err := ioutil.ReadDir(confpath)
if nil != err {
if os.IsNotExist(err) {
return nil, nil
}
errs = append(errs, &ManageError{
Name: confpath,
Hint: "Read directory",
Parent: err,
})
return nil, errs
}
// TODO report active status
srvs := []winConf{}
for i := range infos {
filename := strings.ToLower(infos[i].Name())
if len(filename) <= srvLen || !strings.HasSuffix(filename, srvExt) {
continue
}
name := filename[:len(filename)-srvLen]
b, err := ioutil.ReadFile(filepath.Join(confpath, filename))
if nil != err {
errs = append(errs, &ManageError{
Name: name,
Hint: "Read file",
Parent: err,
})
continue
}
cfg := winConf{Filename: filename}
err = json.Unmarshal(b, &cfg)
if nil != err {
errs = append(errs, &ManageError{
Name: name,
Hint: "Parse JSON",
Parent: err,
})
continue
}
srvs = append(srvs, cfg)
}
return srvs, errs
}
func listRegistry(c *service.Service) ([]string, error) {
autorunKey := `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
k, _, err := registry.CreateKey(
registry.CURRENT_USER,
autorunKey,
registry.QUERY_VALUE,
)
if err != nil {
log.Fatal(err)
}
defer k.Close()
return k.ReadValueNames(-1)
}
// copies self to install path and returns config path
func installServiceman(c *service.Service) ([]string, error) {
// TODO check version and upgrade or dismiss
self := os.Args[0]
smdir := `\opt\serviceman`
// TODO support service level services (which probably wouldn't need serviceman)
smdir = filepath.Join(c.Home, ".local", smdir)
// for now we'll scope the runner to the name of the application
smbin := filepath.Join(smdir, `bin\serviceman.`+c.Name+`.exe`)
args := getRunnerArgs(c)
smbin := args[0]
conffile := args[len(args)-1]
if smbin != self {
err := os.MkdirAll(filepath.Dir(smbin), 0755)
if nil != err {
return nil, err
}
// Note: self may be the short name, in which case
// we should just use whatever is closest in the path
// exec.LookPath will handle this correctly
self, err = exec.LookPath(self)
if nil != err {
return nil, err
}
bin, err := ioutil.ReadFile(self)
if nil != err {
return nil, err
@ -121,26 +306,19 @@ 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)
}
confpath := filepath.Join(smdir, `etc`)
err = os.MkdirAll(confpath, 0755)
err = os.MkdirAll(filepath.Dir(conffile), 0755)
if nil != err {
return nil, err
}
conffile := filepath.Join(confpath, c.Name+`.json`)
err = ioutil.WriteFile(conffile, b, 0640)
if nil != err {
return nil, err
}
return []string{
smbin,
"run",
"--config",
conffile,
}, nil
return args, nil
}

253
manager/start.go Normal file
View File

@ -0,0 +1,253 @@
package manager
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
)
func getService(system bool, home string, name string) (string, error) {
sys, user, err := getMatchingSrvs(home, name)
if nil != err {
return "", err
}
var service string
if system {
service, err = getOneSysSrv(sys, user, name)
if nil != err {
return "", err
}
} else {
service, err = getOneUserSrv(home, sys, user, name)
if nil != err {
return "", err
}
}
return service, nil
}
// Runnable defines a command to run, along with its arguments,
// and whether or not failing to exit successfully matters.
// It also defines whether certains words must exist (or not exist)
// in its output, apart from existing successfully, to determine
// whether or not it was actually successful.
type Runnable struct {
Exec string
Args []string
Must bool
Keywords []string
Badwords []string
}
func (x Runnable) Run() error {
cmd := exec.Command(x.Exec, x.Args...)
out, err := cmd.CombinedOutput()
if !x.Must {
return nil
}
good := true
str := string(out)
for j := range x.Keywords {
if !strings.Contains(str, x.Keywords[j]) {
good = false
break
}
}
if good && 0 != len(x.Badwords) {
for j := range x.Badwords {
if "" != x.Badwords[j] && !strings.Contains(str, x.Badwords[j]) {
good = false
break
}
}
}
if nil != err {
var comment string
if len(x.Keywords) > 0 {
comment += "# output must match all of:\n"
comment += "# \t" + strings.Join(x.Keywords, "\n#\t") + "\n"
}
if len(x.Badwords) > 0 {
comment += "# output must not match any of:\n"
comment += "# \t" + strings.Join(x.Badwords, "\n#\t") + "\n"
}
return fmt.Errorf("Failed to run %s %s\n%s\n%s\n", x.Exec, strings.Join(x.Args, " "), str, comment)
}
return nil
}
func (x Runnable) String() string {
var must = "true"
if x.Must {
must = "exit"
}
return strings.TrimSpace(fmt.Sprintf(
"%s %s || %s\n",
x.Exec,
strings.Join(x.Args, " "),
must,
))
}
func getSrvs(dir string) ([]string, error) {
plists := []string{}
infos, err := ioutil.ReadDir(dir)
if nil != err {
return nil, err
}
for i := range infos {
x := infos[i]
fname := strings.ToLower(x.Name())
if strings.HasSuffix(fname, srvExt) {
plists = append(plists, x.Name())
}
}
return plists, nil
}
func getSystemSrvs() ([]string, error) {
return getSrvs(srvSysPath)
}
func getUserSrvs(home string) ([]string, error) {
confDir := filepath.Join(home, srvUserPath)
err := os.MkdirAll(confDir, 0755)
if nil != err {
return nil, err
}
return getSrvs(confDir)
}
// "come.example.foo.plist" matches "foo"
func filterMatchingSrvs(plists []string, name string) []string {
filtered := []string{}
for i := range plists {
pname := plists[i]
lname := strings.ToLower(pname)
n := len(lname)
if strings.HasSuffix(lname[:n-srvLen], strings.ToLower(name)) {
filtered = append(filtered, pname)
}
}
return filtered
}
func getMatchingSrvs(home string, name string) ([]string, []string, error) {
sysPlists, err := getSystemSrvs()
if nil != err {
return nil, nil, err
}
var userPlists []string
if "" != home {
userPlists, err = getUserSrvs(home)
if nil != err {
return nil, nil, err
}
}
return filterMatchingSrvs(sysPlists, name), filterMatchingSrvs(userPlists, name), nil
}
func getExactSrvMatch(srvs []string, name string) string {
for i := range srvs {
srv := srvs[i]
n := len(srv)
if srv[:n-srvLen] == strings.ToLower(name) {
return srv
}
}
return ""
}
func getOneSysSrv(sys []string, user []string, name string) (string, error) {
if service := getExactSrvMatch(user, name); "" != service {
return filepath.Join(srvSysPath, service), nil
}
n := len(sys)
switch {
case 0 == n:
errstr := fmt.Sprintf("Didn't find user service matching %q\n", name)
if 0 != len(user) {
errstr += fmt.Sprintf("Did you intend to run a user service instead?\n\t%s\n", strings.Join(user, "\n\t"))
}
return "", fmt.Errorf(errstr)
case n > 1:
errstr := fmt.Sprintf("Found more than one matching service:\n\t%s\n", strings.Join(sys, "\n\t"))
return "", fmt.Errorf(errstr)
default:
return filepath.Join(srvSysPath, sys[0]), nil
}
}
func getOneUserSrv(home string, sys []string, user []string, name string) (string, error) {
if service := getExactSrvMatch(user, name); "" != service {
return filepath.Join(home, srvUserPath, service), nil
}
n := len(user)
switch {
case 0 == n:
errstr := fmt.Sprintf("Didn't find user service matching %q\n", name)
if 0 != len(sys) {
errstr += fmt.Sprintf("Did you intend to run a system service instead?\n\t%s\n", strings.Join(sys, "\n\t"))
}
return "", fmt.Errorf(errstr)
case n > 1:
errstr := fmt.Sprintf("Found more than one matching service:\n\t%s\n", strings.Join(user, "\n\t"))
return "", fmt.Errorf(errstr)
default:
return filepath.Join(home, srvUserPath, user[0]), nil
}
}
func adjustPrivs(system bool, cmds []Runnable) []Runnable {
if !system || isPrivileged() {
return cmds
}
sudos := cmds
cmds = []Runnable{}
for i := range sudos {
exe := sudos[i]
exe.Args = append([]string{exe.Exec}, exe.Args...)
exe.Exec = "sudo"
cmds = append(cmds, exe)
}
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

@ -0,0 +1,32 @@
package manager
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestEmptyUserServicePath(t *testing.T) {
srvs, err := getUserSrvs("/tmp/fakeuser")
if nil != err {
t.Fatal(err)
}
if len(srvs) > 0 {
t.Fatal(fmt.Errorf("sanity fail: shouldn't get services from empty directory"))
}
dirs, err := ioutil.ReadDir(filepath.Join("/tmp/fakeuser", srvUserPath))
if nil != err {
t.Fatal(err)
}
if len(dirs) > 0 {
t.Fatal(fmt.Errorf("sanity fail: shouldn't get listing from empty directory"))
}
err = os.RemoveAll("/tmp/fakeuser")
if nil != err {
panic("couldn't remove /tmp/fakeuser")
}
}

File diff suppressed because one or more lines are too long

1
npm/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

28
npm/README.md Normal file
View File

@ -0,0 +1,28 @@
# serviceman
A cross-platform service manager
```bash
serviceman add --name "my-project" node ./serve.js --port 3000
serviceman stop my-project
serviceman start my-project
```
Works with launchd (Mac), systemd (Linux), or standalone (Windows).
## Meta Package
This is a meta-package to fetch and install the correction version of
[go-serviceman](https://git.rootprojects.org/root/serviceman)
for your architecture and platform.
```bash
npm install serviceman
```
## How does it work?
1. Resolves executable from PATH, or hashbang (ex: `#!/usr/bin/env node`)
2. Resolves file and directory paths to absolute paths (ex: `/Users/me/my-project/serve.js`)
3. Creates a template `.plist` (Mac), `.service` (Linux), or `.json` (Windows) file
4. Calls `launchd` (Mac), `systemd` (Linux), or `serviceman-runner` (Windows) to enable/start/stop/etc

1
npm/bin/serviceman Normal file
View File

@ -0,0 +1 @@
# this will be replaced by the postinstall script

18
npm/package-lock.json generated Normal file
View File

@ -0,0 +1,18 @@
{
"name": "serviceman",
"version": "0.5.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@root/mkdirp": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz",
"integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA=="
},
"@root/request": {
"version": "1.3.11",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw=="
}
}
}

39
npm/package.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "serviceman",
"version": "0.7.0",
"description": "A cross-platform service manager",
"main": "index.js",
"homepage": "https://git.rootprojects.org/root/serviceman/src/branch/master/npm",
"files": [
"bin/",
"scripts/"
],
"bin": {
"serviceman": "bin/serviceman"
},
"scripts": {
"serviceman": "serviceman",
"postinstall": "node scripts/fetch-serviceman.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.rootprojects.org/root/serviceman.git"
},
"keywords": [
"launchd",
"systemd",
"winsvc",
"launchctl",
"systemctl",
"HKEY_CURRENT_USER",
"HKCU",
"Run"
],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0",
"dependencies": {
"@root/mkdirp": "^1.0.0",
"@root/request": "^1.3.11"
}
}

269
npm/scripts/fetch-serviceman.js Executable file
View File

@ -0,0 +1,269 @@
#!/usr/bin/env node
'use strict';
var path = require('path');
var os = require('os');
// https://nodejs.org/api/os.html#os_os_arch
// 'arm', 'arm64', 'ia32', 'mips', 'mipsel', 'ppc', 'ppc64', 's390', 's390x', 'x32', and 'x64'
var arch = os.arch(); // process.arch
// https://nodejs.org/api/os.html#os_os_platform
// 'aix', 'darwin', 'freebsd', 'linux', 'openbsd', 'sunos', 'win32'
var platform = os.platform(); // process.platform
var ext = /^win/i.test(platform) ? '.exe' : '';
// This is _probably_ right. It's good enough for us
// https://github.com/nodejs/node/issues/13629
if ('arm' === arch) {
arch += 'v' + process.config.variables.arm_version;
}
var map = {
// arches
armv6: 'armv6',
armv7: 'armv7',
arm64: 'armv8',
ia32: '386',
x32: '386',
x64: 'amd64',
// platforms
darwin: 'darwin',
linux: 'linux',
win32: 'windows'
};
arch = map[arch];
platform = map[platform];
if (!arch || !platform) {
console.error(
"'" + os.platform() + "' on '" + os.arch() + "' isn't supported yet."
);
console.error(
'Please open an issue at https://git.rootprojects.org/root/serviceman/issues'
);
process.exit(1);
}
var newVer = require('../package.json').version;
var fs = require('fs');
var exec = require('child_process').exec;
var request = require('@root/request');
var mkdirp = require('@root/mkdirp');
function needsUpdate(oldVer, newVer) {
// "v1.0.0-pre" is BEHIND "v1.0.0"
newVer = newVer
.replace(/^v/, '')
.split(/[\.\-\+]/)
.filter(Boolean);
oldVer = oldVer
.replace(/^v/, '')
.split(/[\.\-\+]/)
.filter(Boolean);
if (!oldVer.length) {
return true;
}
// ex: v1.0.0-pre vs v1.0.0
if (newVer[3] && !oldVer[3]) {
// don't install beta over stable
return false;
}
// ex: old is v1.0.0-pre
if (oldVer[3]) {
if (oldVer[2] > 0) {
oldVer[2] -= 1;
} else if (oldVer[1] > 0) {
oldVer[2] = 999;
oldVer[1] -= 1;
} else if (oldVer[0] > 0) {
oldVer[2] = 999;
oldVer[1] = 999;
oldVer[0] -= 1;
} else {
// v0.0.0
return true;
}
}
// ex: v1.0.1 vs v1.0.0-pre
if (newVer[3]) {
if (newVer[2] > 0) {
newVer[2] -= 1;
} else if (newVer[1] > 0) {
newVer[2] = 999;
newVer[1] -= 1;
} else if (newVer[0] > 0) {
newVer[2] = 999;
newVer[1] = 999;
newVer[0] -= 1;
} else {
// v0.0.0
return false;
}
}
// ex: v1.0.1 vs v1.0.0
if (oldVer[0] > newVer[0]) {
return false;
} else if (oldVer[0] < newVer[0]) {
return true;
} else if (oldVer[1] > newVer[1]) {
return false;
} else if (oldVer[1] < newVer[1]) {
return true;
} else if (oldVer[2] > newVer[2]) {
return false;
} else if (oldVer[2] < newVer[2]) {
return true;
} else if (!oldVer[3] && newVer[3]) {
return false;
} else if (oldVer[3] && !newVer[3]) {
return true;
} else {
return false;
}
}
/*
// Same version
console.log(false === needsUpdate('0.5.0', '0.5.0'));
// No previous version
console.log(true === needsUpdate('', '0.5.1'));
// The new version is slightly newer
console.log(true === needsUpdate('0.5.0', '0.5.1'));
console.log(true === needsUpdate('0.4.999-pre1', '0.5.0-pre1'));
// The new version is slightly older
console.log(false === needsUpdate('0.5.0', '0.5.0-pre1'));
console.log(false === needsUpdate('0.5.1', '0.5.0'));
*/
function install(name, bindirs, getVersion, parseVersion, urlTpl) {
exec(getVersion, { windowsHide: true }, function(err, stdout) {
var oldVer = parseVersion(stdout);
//console.log('old:', oldVer, 'new:', newVer);
if (!needsUpdate(oldVer, newVer)) {
console.info(
'Current ' + name + ' version is new enough:',
oldVer,
newVer
);
return;
//} else {
// console.info('Current serviceman version is older:', oldVer, newVer);
}
var url = urlTpl
.replace(/{{ .Version }}/g, newVer)
.replace(/{{ .Platform }}/g, platform)
.replace(/{{ .Arch }}/g, arch)
.replace(/{{ .Ext }}/g, ext);
console.info('Installing from', url);
return request({ uri: url, encoding: null }, function(err, resp) {
if (err) {
console.error(err);
return;
}
//console.log(resp.body.byteLength);
//console.log(typeof resp.body);
var bin = name + ext;
function next() {
if (!bindirs.length) {
return;
}
var bindir = bindirs.pop();
return mkdirp(bindir, function(err) {
if (err) {
console.error(err);
return;
}
var localsrv = path.join(bindir, bin);
return fs.writeFile(localsrv, resp.body, function(err) {
next();
if (err) {
console.error(err);
return;
}
fs.chmodSync(localsrv, parseInt('0755', 8));
console.info('Wrote', bin, 'to', bindir);
});
});
}
next();
});
});
}
function winstall(name, bindir) {
try {
fs.writeFileSync(
path.join(bindir, name),
'#!/usr/bin/env bash\n"$(dirname "$0")/serviceman.exe" "$@"\nexit $?'
);
} catch (e) {
// ignore
}
// because bugs in npm + git bash oddities, of course
// https://npm.community/t/globally-installed-package-does-not-execute-in-git-bash-on-windows/9394
try {
fs.writeFileSync(
path.join(path.join(__dirname, '../../.bin'), name),
[
'#!/bin/sh',
'# manual bugfix patch for npm on windows',
'basedir=$(dirname "$(echo "$0" | sed -e \'s,\\\\,/,g\')")',
'"$basedir/../' + name + '/bin/' + name + '" "$@"',
'exit $?'
].join('\n')
);
} catch (e) {
// ignore
}
try {
fs.writeFileSync(
path.join(path.join(__dirname, '../../..'), name),
[
'#!/bin/sh',
'# manual bugfix patch for npm on windows',
'basedir=$(dirname "$(echo "$0" | sed -e \'s,\\\\,/,g\')")',
'"$basedir/node_modules/' + name + '/bin/' + name + '" "$@"',
'exit $?'
].join('\n')
);
} catch (e) {
// ignore
}
// end bugfix
}
function run() {
//var homedir = require('os').homedir();
//var bindir = path.join(homedir, '.local', 'bin');
var bindir = path.resolve(__dirname, '..', 'bin');
var name = 'serviceman';
if ('.exe' === ext) {
winstall(name, bindir);
}
return install(
name,
[bindir],
'serviceman version',
function parseVersion(stdout) {
return (stdout || '').split(' ')[0];
},
'https://rootprojects.org/serviceman/dist/{{ .Platform }}/{{ .Arch }}/serviceman{{ .Ext }}'
);
}
if (require.main === module) {
run();
}

View File

@ -2,13 +2,17 @@ package runner
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"git.rootprojects.org/root/go-serviceman/service"
ps "github.com/mitchellh/go-ps"
)
// Filled in on init by runner_windows.go
@ -17,7 +21,9 @@ var shellArgs = []string{}
// Notes on spawning a child process
// https://groups.google.com/forum/#!topic/golang-nuts/shST-SDqIp4
func Run(conf *service.Service) {
// Start will execute the service, and write the PID and logs out to the log directory
func Start(conf *service.Service) error {
pid := os.Getpid()
originalBackoff := 1 * time.Second
maxBackoff := 1 * time.Minute
threshold := 5 * time.Second
@ -26,6 +32,17 @@ func Run(conf *service.Service) {
failures := 0
logfile := filepath.Join(conf.Logdir, conf.Name+".log")
if oldPid, exename, err := getProcess(conf); nil == err {
return fmt.Errorf("%q may already be running as %q (pid %d)", conf.Name, exename, oldPid)
}
go func() {
for {
maybeWritePidFile(pid, conf)
time.Sleep(1 * time.Second)
}
}()
binpath := conf.Exec
args := []string{}
if "" != conf.Interpreter {
@ -61,6 +78,11 @@ func Run(conf *service.Service) {
if "" != conf.Workdir {
cmd.Dir = conf.Workdir
}
if len(conf.Envs) > 0 {
for k, v := range conf.Envs {
cmd.Env = append(cmd.Env, k+"="+v)
}
}
err = cmd.Start()
if nil != err {
fmt.Fprintf(lf, "[%s] Could not start %q process: %s\n", time.Now(), conf.Name, err)
@ -84,7 +106,7 @@ func Run(conf *service.Service) {
backoff = originalBackoff
failures = 0
} else {
failures += 1
failures++
fmt.Fprintf(lf, "Waiting %s to restart %q (%d consequtive immediate exits)\n", backoff, conf.Name, failures)
time.Sleep(backoff)
backoff *= 2
@ -93,4 +115,125 @@ func Run(conf *service.Service) {
}
}
}
return nil
}
// Stop will find and stop another serviceman runner instance by it's PID
func Stop(conf *service.Service) error {
i := 0
var err error
for {
if i >= 3 {
return err
}
i++
oldPid, exename, err2 := getProcess(conf)
err = err2
switch err {
case nil:
fmt.Printf("killing old process %q with pid %d\n", exename, oldPid)
err := kill(oldPid)
if nil != err {
return err
}
return waitForProcessToDie(oldPid)
case ErrNoPidFile:
return err
case ErrNoProcess:
return err
case ErrInvalidPidFile:
fallthrough
default:
// waiting a little bit since the PID is written every second
time.Sleep(400 * time.Millisecond)
}
}
return fmt.Errorf("unexpected error: %s", err)
}
// Restart calls Stop, ignoring any failure, and then Start, returning any failure
func Restart(conf *service.Service) error {
_ = Stop(conf)
return Start(conf)
}
var ErrNoPidFile = fmt.Errorf("no pid file")
var ErrInvalidPidFile = fmt.Errorf("malformed pid file")
var ErrNoProcess = fmt.Errorf("process not found by pid")
func waitForProcessToDie(pid int) error {
exename := "unknown"
for i := 0; i < 10; i++ {
px, err := ps.FindProcess(pid)
if nil != err {
return nil
}
if nil == px {
return nil
}
exename = px.Executable()
time.Sleep(1 * time.Second)
}
return fmt.Errorf("process %q (%d) just won't die", exename, pid)
}
func getProcess(conf *service.Service) (int, string, error) {
// TODO make Pidfile() a property of conf?
pidFile := filepath.Join(conf.Logdir, conf.Name+".pid")
b, err := ioutil.ReadFile(pidFile)
if nil != err {
return 0, "", ErrNoPidFile
}
s := strings.TrimSpace(string(b))
oldPid, err := strconv.Atoi(s)
if nil != err {
return 0, "", ErrInvalidPidFile
}
px, err := ps.FindProcess(oldPid)
if nil != err {
return 0, "", err
}
if nil == px {
return 0, "", ErrNoProcess
}
_, err = os.FindProcess(oldPid)
if nil != err {
return 0, "", err
}
exename := px.Executable()
return oldPid, exename, nil
}
// TODO error out if can't write to PID or log
func maybeWritePidFile(pid int, conf *service.Service) bool {
newPid := []byte(strconv.Itoa(pid))
// TODO use a specific PID dir? meh...
pidFile := filepath.Join(conf.Logdir, conf.Name+".pid")
b, err := ioutil.ReadFile(pidFile)
if nil != err {
ioutil.WriteFile(pidFile, newPid, 0644)
return true
}
s := strings.TrimSpace(string(b))
oldPid, err := strconv.Atoi(s)
if nil != err {
ioutil.WriteFile(pidFile, newPid, 0644)
return true
}
if oldPid != pid {
Stop(conf)
ioutil.WriteFile(pidFile, newPid, 0644)
return true
}
return false
}

View File

@ -2,7 +2,19 @@
package runner
import "os/exec"
import (
"os"
"os/exec"
)
func backgroundCmd(cmd *exec.Cmd) {
}
func kill(pid int) error {
p, err := os.FindProcess(pid)
// already died
if nil != err {
return nil
}
return p.Kill()
}

View File

@ -1,10 +1,23 @@
package runner
import (
"fmt"
"os/exec"
"strconv"
"syscall"
)
func backgroundCmd(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
}
func kill(pid int) error {
// Kill the whole processes tree (all children and grandchildren)
cmd := exec.Command("taskkill", "/pid", strconv.Itoa(pid), "/T", "/F")
b, err := cmd.CombinedOutput()
if nil != err {
return fmt.Errorf("%s: %s", err.Error(), string(b))
}
return nil
}

View File

@ -71,7 +71,7 @@ type Service struct {
MultiuserProtection bool `json:"multiuser_protection,omitempty"`
}
func (s *Service) Normalize(force bool) {
func (s *Service) NormalizeWithoutPath() {
if "" == s.Name {
ext := filepath.Ext(s.Exec)
base := filepath.Base(s.Exec[:len(s.Exec)-len(ext)])
@ -93,11 +93,16 @@ func (s *Service) Normalize(force bool) {
os.Exit(4)
return
}
s.Home = home
s.Local = filepath.Join(home, ".local")
s.Logdir = filepath.Join(home, ".local", "share", s.Name, "var", "log")
} else {
s.Logdir = "/var/log/" + s.Name
}
}
func (s *Service) Normalize(force bool) {
s.NormalizeWithoutPath()
// Check to see if Exec exists
// /whatever => must exist exactly
@ -111,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"
@ -18,12 +22,17 @@ import (
)
var GitRev = "000000000"
var GitVersion = "v0.0.0"
var GitVersion = "v0.5.3-pre+dirty"
var GitTimestamp = time.Now().Format(time.RFC3339)
func usage() {
fmt.Println("Usage: serviceman add ./foo-app -- --foo-arg")
fmt.Println("Usage: serviceman run --config ./foo-app.json")
fmt.Println("Usage:")
fmt.Println("\tserviceman <command> --help")
fmt.Println("\tserviceman add ./foo-app -- --foo-arg")
fmt.Println("\tserviceman run --config ./foo-app.json")
fmt.Println("\tserviceman list --all")
fmt.Println("\tserviceman start <name>")
fmt.Println("\tserviceman stop <name>")
}
func main() {
@ -38,10 +47,16 @@ func main() {
switch top {
case "version":
fmt.Println(GitVersion, GitTimestamp, GitRev)
case "add":
add()
case "run":
run()
case "add":
add()
case "start":
start()
case "stop":
stop()
case "list":
list()
default:
fmt.Fprintf(os.Stderr, "Unknown argument %s\n", top)
usage()
@ -54,41 +69,268 @@ 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
pathEnv := ""
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")
flag.BoolVar(&force, "force", false, "if the interpreter or executable doesn't exist, or things don't make sense, try anyway")
flag.StringVar(&pathEnv, "path", "", "set the path for the resulting systemd service")
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.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, "")
}
if "" != pathEnv {
conf.Envs = make(map[string]string)
conf.Envs["PATH"] = pathEnv
}
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
}
flagargs = append(exeargs, flagargs...)
// TODO
for i := range flagargs {
arg := flagargs[i]
arg = filepath.ToSlash(arg)
// Paths considered to be anything starting with ./, .\, /, \, C:
if "." == arg || strings.Contains(arg, "/") {
//if "." == arg || (len(arg) >= 2 && "./" == arg[:2] || '/' == arg[0] || "C:" == strings.ToUpper(arg[:1])) {
var err error
arg, err = filepath.Abs(arg)
if nil == err {
_, err = os.Stat(arg)
}
if nil != err {
fmt.Printf("%q appears to be a file path, but %q could not be read\n", flagargs[i], arg)
if !force {
os.Exit(7)
return
}
continue
}
if '\\' != os.PathSeparator {
// Convert paths back to .\ for Windows
arg = filepath.FromSlash(arg)
}
// Lookin' good
flagargs[i] = arg
}
}
// We won't bother with Interpreter here
// (it's really just for documentation),
// but we will add any and all unchecked args to the full slice
conf.Exec = flagargs[0]
conf.Argv = append(flagargs[1:], rawargs...)
// TODO update docs: go to the work directory
// TODO test with "npm start"
conf.NormalizeWithoutPath()
//fmt.Printf("\n%#v\n\n", conf)
if conf.System && !manager.IsPrivileged() {
fmt.Fprintf(os.Stderr, "Warning: You may need to use 'sudo' to add %q as a privileged system service.\n", conf.Name)
}
if len(ass) > 0 {
fmt.Printf("OPTIONS: Making some assumptions...\n\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 %s %s service, running as %q\n",
conf.Name,
servicetype,
servicemode,
runAs,
)
fmt.Println()
}
func list() {
var verbose bool
forUser := false
forSystem := false
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(&verbose, "all", false, "show all services (even those not managed by serviceman)")
flag.Parse()
if forUser && forSystem {
fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
os.Exit(1)
return
}
conf := &service.Service{}
if forUser {
conf.System = false
} else if forSystem {
@ -97,41 +339,217 @@ func add() {
conf.System = manager.IsPrivileged()
}
n := len(args)
if 0 == n {
fmt.Println("Usage: serviceman add ./foo-app -- --foo-arg")
os.Exit(2)
// Pretty much just for HomeDir
conf.NormalizeWithoutPath()
managed, others, errs := manager.List(conf)
for i := range errs {
fmt.Fprintf(os.Stderr, "possible error: %s\n", errs[i])
}
if len(errs) > 0 {
fmt.Fprintf(os.Stderr, "\n")
}
fmt.Printf("serviceman-managed services:\n\n")
for i := range managed {
fmt.Println("\t" + managed[i])
}
if 0 == len(managed) {
fmt.Println("\t(none)")
}
fmt.Println("")
if verbose {
fmt.Printf("other services:\n\n")
for i := range others {
fmt.Println("\t" + others[i])
}
if 0 == len(others) {
fmt.Println("\t(none)")
}
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() {
forUser := false
forSystem := false
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.Parse()
args := flag.Args()
if 1 != len(args) {
fmt.Println("Usage: serviceman start <name>")
os.Exit(1)
}
if forUser && forSystem {
fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
os.Exit(1)
return
}
execpath, err := manager.WhereIs(args[0])
if nil != err {
fmt.Fprintf(os.Stderr, "Error: '%s' could not be found.\n", args[0])
if !force {
os.Exit(3)
return
}
conf := &service.Service{
Name: args[0],
Restart: false,
}
if forUser {
conf.System = false
} else if forSystem {
conf.System = true
} else {
args[0] = execpath
conf.System = manager.IsPrivileged()
}
conf.Exec = args[0]
args = args[1:]
conf.NormalizeWithoutPath()
if n >= 2 {
conf.Interpreter = conf.Exec
conf.Exec = args[0]
conf.Argv = append(args[1:], conf.Argv...)
}
conf.Normalize(force)
//fmt.Printf("\n%#v\n\n", conf)
err = manager.Install(conf)
err := manager.Start(conf)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
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")
os.Exit(500)
return
}
}
func stop() {
forUser := false
forSystem := false
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.Parse()
args := flag.Args()
if 1 != len(args) {
fmt.Println("Usage: serviceman stop <name>")
os.Exit(1)
}
if forUser && forSystem {
fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
os.Exit(1)
return
}
conf := &service.Service{
Name: args[0],
Restart: false,
}
if forUser {
conf.System = false
} else if forSystem {
conf.System = true
} else {
conf.System = manager.IsPrivileged()
}
conf.NormalizeWithoutPath()
if err := manager.Stop(conf); nil != err {
fmt.Println(err)
os.Exit(127)
}
}
@ -143,7 +561,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)
@ -179,8 +597,9 @@ func run() {
os.Exit(400)
}
s.Normalize(false)
//fmt.Fprintf(os.Stdout, "Logdir: %s\n", s.Logdir)
force := false
s.Normalize(force)
fmt.Printf("All output will be directed to the logs at:\n\t%s\n", s.Logdir)
err = os.MkdirAll(s.Logdir, 0755)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
@ -189,23 +608,11 @@ func run() {
if !daemonize {
//fmt.Fprintf(os.Stdout, "Running %s %s %s\n", s.Interpreter, s.Exec, strings.Join(s.Argv, " "))
runner.Run(s)
if err := runner.Start(s); nil != err {
fmt.Println("Error:", err)
}
return
}
cmd := exec.Command(os.Args[0], "run", "--config", confpath)
// 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)
}

11
serviceman_darwin.go Normal file
View File

@ -0,0 +1,11 @@
package main
import (
"fmt"
"git.rootprojects.org/root/go-serviceman/service"
)
func printLogMessage(conf *service.Service) {
fmt.Printf("If all went well the logs should have been created at:\n\n\t%s\n", conf.Logdir)
}

26
serviceman_linux.go Normal file
View File

@ -0,0 +1,26 @@
package main
import (
"fmt"
"git.rootprojects.org/root/go-serviceman/manager"
"git.rootprojects.org/root/go-serviceman/service"
)
func printLogMessage(conf *service.Service) {
sudo := ""
unit := "--unit"
if conf.System {
if !manager.IsPrivileged() {
sudo = "sudo"
}
} else {
unit = "--user-unit"
}
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.")
fmt.Println("(you may need to run `systemctl restart systemd-journald`)")
}
}

11
serviceman_windows.go Normal file
View File

@ -0,0 +1,11 @@
package main
import (
"fmt"
"git.rootprojects.org/root/go-serviceman/service"
)
func printLogMessage(conf *service.Service) {
fmt.Printf("If all went well the logs should have been created at:\n\n\t%s\n", conf.Logdir)
}

1
vendor/github.com/mitchellh/go-ps/.gitignore generated vendored Normal file
View File

@ -0,0 +1 @@
.vagrant/

4
vendor/github.com/mitchellh/go-ps/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,4 @@
language: go
go:
- 1.2.1

21
vendor/github.com/mitchellh/go-ps/LICENSE.md generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Mitchell Hashimoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

34
vendor/github.com/mitchellh/go-ps/README.md generated vendored Normal file
View File

@ -0,0 +1,34 @@
# Process List Library for Go
go-ps is a library for Go that implements OS-specific APIs to list and
manipulate processes in a platform-safe way. The library can find and
list processes on Linux, Mac OS X, Solaris, and Windows.
If you're new to Go, this library has a good amount of advanced Go educational
value as well. It uses some advanced features of Go: build tags, accessing
DLL methods for Windows, cgo for Darwin, etc.
How it works:
* **Darwin** uses the `sysctl` syscall to retrieve the process table.
* **Unix** uses the procfs at `/proc` to inspect the process tree.
* **Windows** uses the Windows API, and methods such as
`CreateToolhelp32Snapshot` to get a point-in-time snapshot of
the process table.
## Installation
Install using standard `go get`:
```
$ go get github.com/mitchellh/go-ps
...
```
## TODO
Want to contribute? Here is a short TODO list of things that aren't
implemented for this library that would be nice:
* FreeBSD support
* Plan9 support

43
vendor/github.com/mitchellh/go-ps/Vagrantfile generated vendored Normal file
View File

@ -0,0 +1,43 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "chef/ubuntu-12.04"
config.vm.provision "shell", inline: $script
["vmware_fusion", "vmware_workstation"].each do |p|
config.vm.provider "p" do |v|
v.vmx["memsize"] = "1024"
v.vmx["numvcpus"] = "2"
v.vmx["cpuid.coresPerSocket"] = "1"
end
end
end
$script = <<SCRIPT
SRCROOT="/opt/go"
# Install Go
sudo apt-get update
sudo apt-get install -y build-essential mercurial
sudo hg clone -u release https://code.google.com/p/go ${SRCROOT}
cd ${SRCROOT}/src
sudo ./all.bash
# Setup the GOPATH
sudo mkdir -p /opt/gopath
cat <<EOF >/tmp/gopath.sh
export GOPATH="/opt/gopath"
export PATH="/opt/go/bin:\$GOPATH/bin:\$PATH"
EOF
sudo mv /tmp/gopath.sh /etc/profile.d/gopath.sh
sudo chmod 0755 /etc/profile.d/gopath.sh
# Make sure the gopath is usable by bamboo
sudo chown -R vagrant:vagrant $SRCROOT
sudo chown -R vagrant:vagrant /opt/gopath
SCRIPT

40
vendor/github.com/mitchellh/go-ps/process.go generated vendored Normal file
View File

@ -0,0 +1,40 @@
// ps provides an API for finding and listing processes in a platform-agnostic
// way.
//
// NOTE: If you're reading these docs online via GoDocs or some other system,
// you might only see the Unix docs. This project makes heavy use of
// platform-specific implementations. We recommend reading the source if you
// are interested.
package ps
// Process is the generic interface that is implemented on every platform
// and provides common operations for processes.
type Process interface {
// Pid is the process ID for this process.
Pid() int
// PPid is the parent process ID for this process.
PPid() int
// Executable name running this process. This is not a path to the
// executable.
Executable() string
}
// Processes returns all processes.
//
// This of course will be a point-in-time snapshot of when this method was
// called. Some operating systems don't provide snapshot capability of the
// process table, in which case the process table returned might contain
// ephemeral entities that happened to be running when this was called.
func Processes() ([]Process, error) {
return processes()
}
// FindProcess looks up a single process by pid.
//
// Process will be nil and error will be nil if a matching process is
// not found.
func FindProcess(pid int) (Process, error) {
return findProcess(pid)
}

138
vendor/github.com/mitchellh/go-ps/process_darwin.go generated vendored Normal file
View File

@ -0,0 +1,138 @@
// +build darwin
package ps
import (
"bytes"
"encoding/binary"
"syscall"
"unsafe"
)
type DarwinProcess struct {
pid int
ppid int
binary string
}
func (p *DarwinProcess) Pid() int {
return p.pid
}
func (p *DarwinProcess) PPid() int {
return p.ppid
}
func (p *DarwinProcess) Executable() string {
return p.binary
}
func findProcess(pid int) (Process, error) {
ps, err := processes()
if err != nil {
return nil, err
}
for _, p := range ps {
if p.Pid() == pid {
return p, nil
}
}
return nil, nil
}
func processes() ([]Process, error) {
buf, err := darwinSyscall()
if err != nil {
return nil, err
}
procs := make([]*kinfoProc, 0, 50)
k := 0
for i := _KINFO_STRUCT_SIZE; i < buf.Len(); i += _KINFO_STRUCT_SIZE {
proc := &kinfoProc{}
err = binary.Read(bytes.NewBuffer(buf.Bytes()[k:i]), binary.LittleEndian, proc)
if err != nil {
return nil, err
}
k = i
procs = append(procs, proc)
}
darwinProcs := make([]Process, len(procs))
for i, p := range procs {
darwinProcs[i] = &DarwinProcess{
pid: int(p.Pid),
ppid: int(p.PPid),
binary: darwinCstring(p.Comm),
}
}
return darwinProcs, nil
}
func darwinCstring(s [16]byte) string {
i := 0
for _, b := range s {
if b != 0 {
i++
} else {
break
}
}
return string(s[:i])
}
func darwinSyscall() (*bytes.Buffer, error) {
mib := [4]int32{_CTRL_KERN, _KERN_PROC, _KERN_PROC_ALL, 0}
size := uintptr(0)
_, _, errno := syscall.Syscall6(
syscall.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
4,
0,
uintptr(unsafe.Pointer(&size)),
0,
0)
if errno != 0 {
return nil, errno
}
bs := make([]byte, size)
_, _, errno = syscall.Syscall6(
syscall.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
4,
uintptr(unsafe.Pointer(&bs[0])),
uintptr(unsafe.Pointer(&size)),
0,
0)
if errno != 0 {
return nil, errno
}
return bytes.NewBuffer(bs[0:size]), nil
}
const (
_CTRL_KERN = 1
_KERN_PROC = 14
_KERN_PROC_ALL = 0
_KINFO_STRUCT_SIZE = 648
)
type kinfoProc struct {
_ [40]byte
Pid int32
_ [199]byte
Comm [16]byte
_ [301]byte
PPid int32
_ [84]byte
}

260
vendor/github.com/mitchellh/go-ps/process_freebsd.go generated vendored Normal file
View File

@ -0,0 +1,260 @@
// +build freebsd,amd64
package ps
import (
"bytes"
"encoding/binary"
"syscall"
"unsafe"
)
// copied from sys/sysctl.h
const (
CTL_KERN = 1 // "high kernel": proc, limits
KERN_PROC = 14 // struct: process entries
KERN_PROC_PID = 1 // by process id
KERN_PROC_PROC = 8 // only return procs
KERN_PROC_PATHNAME = 12 // path to executable
)
// copied from sys/user.h
type Kinfo_proc struct {
Ki_structsize int32
Ki_layout int32
Ki_args int64
Ki_paddr int64
Ki_addr int64
Ki_tracep int64
Ki_textvp int64
Ki_fd int64
Ki_vmspace int64
Ki_wchan int64
Ki_pid int32
Ki_ppid int32
Ki_pgid int32
Ki_tpgid int32
Ki_sid int32
Ki_tsid int32
Ki_jobc [2]byte
Ki_spare_short1 [2]byte
Ki_tdev int32
Ki_siglist [16]byte
Ki_sigmask [16]byte
Ki_sigignore [16]byte
Ki_sigcatch [16]byte
Ki_uid int32
Ki_ruid int32
Ki_svuid int32
Ki_rgid int32
Ki_svgid int32
Ki_ngroups [2]byte
Ki_spare_short2 [2]byte
Ki_groups [64]byte
Ki_size int64
Ki_rssize int64
Ki_swrss int64
Ki_tsize int64
Ki_dsize int64
Ki_ssize int64
Ki_xstat [2]byte
Ki_acflag [2]byte
Ki_pctcpu int32
Ki_estcpu int32
Ki_slptime int32
Ki_swtime int32
Ki_cow int32
Ki_runtime int64
Ki_start [16]byte
Ki_childtime [16]byte
Ki_flag int64
Ki_kiflag int64
Ki_traceflag int32
Ki_stat [1]byte
Ki_nice [1]byte
Ki_lock [1]byte
Ki_rqindex [1]byte
Ki_oncpu [1]byte
Ki_lastcpu [1]byte
Ki_ocomm [17]byte
Ki_wmesg [9]byte
Ki_login [18]byte
Ki_lockname [9]byte
Ki_comm [20]byte
Ki_emul [17]byte
Ki_sparestrings [68]byte
Ki_spareints [36]byte
Ki_cr_flags int32
Ki_jid int32
Ki_numthreads int32
Ki_tid int32
Ki_pri int32
Ki_rusage [144]byte
Ki_rusage_ch [144]byte
Ki_pcb int64
Ki_kstack int64
Ki_udata int64
Ki_tdaddr int64
Ki_spareptrs [48]byte
Ki_spareint64s [96]byte
Ki_sflag int64
Ki_tdflags int64
}
// UnixProcess is an implementation of Process that contains Unix-specific
// fields and information.
type UnixProcess struct {
pid int
ppid int
state rune
pgrp int
sid int
binary string
}
func (p *UnixProcess) Pid() int {
return p.pid
}
func (p *UnixProcess) PPid() int {
return p.ppid
}
func (p *UnixProcess) Executable() string {
return p.binary
}
// Refresh reloads all the data associated with this process.
func (p *UnixProcess) Refresh() error {
mib := []int32{CTL_KERN, KERN_PROC, KERN_PROC_PID, int32(p.pid)}
buf, length, err := call_syscall(mib)
if err != nil {
return err
}
proc_k := Kinfo_proc{}
if length != uint64(unsafe.Sizeof(proc_k)) {
return err
}
k, err := parse_kinfo_proc(buf)
if err != nil {
return err
}
p.ppid, p.pgrp, p.sid, p.binary = copy_params(&k)
return nil
}
func copy_params(k *Kinfo_proc) (int, int, int, string) {
n := -1
for i, b := range k.Ki_comm {
if b == 0 {
break
}
n = i + 1
}
comm := string(k.Ki_comm[:n])
return int(k.Ki_ppid), int(k.Ki_pgid), int(k.Ki_sid), comm
}
func findProcess(pid int) (Process, error) {
mib := []int32{CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, int32(pid)}
_, _, err := call_syscall(mib)
if err != nil {
return nil, err
}
return newUnixProcess(pid)
}
func processes() ([]Process, error) {
results := make([]Process, 0, 50)
mib := []int32{CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0}
buf, length, err := call_syscall(mib)
if err != nil {
return results, err
}
// get kinfo_proc size
k := Kinfo_proc{}
procinfo_len := int(unsafe.Sizeof(k))
count := int(length / uint64(procinfo_len))
// parse buf to procs
for i := 0; i < count; i++ {
b := buf[i*procinfo_len : i*procinfo_len+procinfo_len]
k, err := parse_kinfo_proc(b)
if err != nil {
continue
}
p, err := newUnixProcess(int(k.Ki_pid))
if err != nil {
continue
}
p.ppid, p.pgrp, p.sid, p.binary = copy_params(&k)
results = append(results, p)
}
return results, nil
}
func parse_kinfo_proc(buf []byte) (Kinfo_proc, error) {
var k Kinfo_proc
br := bytes.NewReader(buf)
err := binary.Read(br, binary.LittleEndian, &k)
if err != nil {
return k, err
}
return k, nil
}
func call_syscall(mib []int32) ([]byte, uint64, error) {
miblen := uint64(len(mib))
// get required buffer size
length := uint64(0)
_, _, err := syscall.RawSyscall6(
syscall.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
uintptr(miblen),
0,
uintptr(unsafe.Pointer(&length)),
0,
0)
if err != 0 {
b := make([]byte, 0)
return b, length, err
}
if length == 0 {
b := make([]byte, 0)
return b, length, err
}
// get proc info itself
buf := make([]byte, length)
_, _, err = syscall.RawSyscall6(
syscall.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
uintptr(miblen),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&length)),
0,
0)
if err != 0 {
return buf, length, err
}
return buf, length, nil
}
func newUnixProcess(pid int) (*UnixProcess, error) {
p := &UnixProcess{pid: pid}
return p, p.Refresh()
}

35
vendor/github.com/mitchellh/go-ps/process_linux.go generated vendored Normal file
View File

@ -0,0 +1,35 @@
// +build linux
package ps
import (
"fmt"
"io/ioutil"
"strings"
)
// Refresh reloads all the data associated with this process.
func (p *UnixProcess) Refresh() error {
statPath := fmt.Sprintf("/proc/%d/stat", p.pid)
dataBytes, err := ioutil.ReadFile(statPath)
if err != nil {
return err
}
// First, parse out the image name
data := string(dataBytes)
binStart := strings.IndexRune(data, '(') + 1
binEnd := strings.IndexRune(data[binStart:], ')')
p.binary = data[binStart : binStart+binEnd]
// Move past the image name and start parsing the rest
data = data[binStart+binEnd+2:]
_, err = fmt.Sscanf(data,
"%c %d %d %d",
&p.state,
&p.ppid,
&p.pgrp,
&p.sid)
return err
}

96
vendor/github.com/mitchellh/go-ps/process_solaris.go generated vendored Normal file
View File

@ -0,0 +1,96 @@
// +build solaris
package ps
import (
"encoding/binary"
"fmt"
"os"
)
type ushort_t uint16
type id_t int32
type pid_t int32
type uid_t int32
type gid_t int32
type dev_t uint64
type size_t uint64
type uintptr_t uint64
type timestruc_t [16]byte
// This is copy from /usr/include/sys/procfs.h
type psinfo_t struct {
Pr_flag int32 /* process flags (DEPRECATED; do not use) */
Pr_nlwp int32 /* number of active lwps in the process */
Pr_pid pid_t /* unique process id */
Pr_ppid pid_t /* process id of parent */
Pr_pgid pid_t /* pid of process group leader */
Pr_sid pid_t /* session id */
Pr_uid uid_t /* real user id */
Pr_euid uid_t /* effective user id */
Pr_gid gid_t /* real group id */
Pr_egid gid_t /* effective group id */
Pr_addr uintptr_t /* address of process */
Pr_size size_t /* size of process image in Kbytes */
Pr_rssize size_t /* resident set size in Kbytes */
Pr_pad1 size_t
Pr_ttydev dev_t /* controlling tty device (or PRNODEV) */
// Guess this following 2 ushort_t values require a padding to properly
// align to the 64bit mark.
Pr_pctcpu ushort_t /* % of recent cpu time used by all lwps */
Pr_pctmem ushort_t /* % of system memory used by process */
Pr_pad64bit [4]byte
Pr_start timestruc_t /* process start time, from the epoch */
Pr_time timestruc_t /* usr+sys cpu time for this process */
Pr_ctime timestruc_t /* usr+sys cpu time for reaped children */
Pr_fname [16]byte /* name of execed file */
Pr_psargs [80]byte /* initial characters of arg list */
Pr_wstat int32 /* if zombie, the wait() status */
Pr_argc int32 /* initial argument count */
Pr_argv uintptr_t /* address of initial argument vector */
Pr_envp uintptr_t /* address of initial environment vector */
Pr_dmodel [1]byte /* data model of the process */
Pr_pad2 [3]byte
Pr_taskid id_t /* task id */
Pr_projid id_t /* project id */
Pr_nzomb int32 /* number of zombie lwps in the process */
Pr_poolid id_t /* pool id */
Pr_zoneid id_t /* zone id */
Pr_contract id_t /* process contract */
Pr_filler int32 /* reserved for future use */
Pr_lwp [128]byte /* information for representative lwp */
}
func (p *UnixProcess) Refresh() error {
var psinfo psinfo_t
path := fmt.Sprintf("/proc/%d/psinfo", p.pid)
fh, err := os.Open(path)
if err != nil {
return err
}
defer fh.Close()
err = binary.Read(fh, binary.LittleEndian, &psinfo)
if err != nil {
return err
}
p.ppid = int(psinfo.Pr_ppid)
p.binary = toString(psinfo.Pr_fname[:], 16)
return nil
}
func toString(array []byte, len int) string {
for i := 0; i < len; i++ {
if array[i] == 0 {
return string(array[:i])
}
}
return string(array[:])
}

101
vendor/github.com/mitchellh/go-ps/process_unix.go generated vendored Normal file
View File

@ -0,0 +1,101 @@
// +build linux solaris
package ps
import (
"fmt"
"io"
"os"
"strconv"
)
// UnixProcess is an implementation of Process that contains Unix-specific
// fields and information.
type UnixProcess struct {
pid int
ppid int
state rune
pgrp int
sid int
binary string
}
func (p *UnixProcess) Pid() int {
return p.pid
}
func (p *UnixProcess) PPid() int {
return p.ppid
}
func (p *UnixProcess) Executable() string {
return p.binary
}
func findProcess(pid int) (Process, error) {
dir := fmt.Sprintf("/proc/%d", pid)
_, err := os.Stat(dir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
return newUnixProcess(pid)
}
func processes() ([]Process, error) {
d, err := os.Open("/proc")
if err != nil {
return nil, err
}
defer d.Close()
results := make([]Process, 0, 50)
for {
fis, err := d.Readdir(10)
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
for _, fi := range fis {
// We only care about directories, since all pids are dirs
if !fi.IsDir() {
continue
}
// We only care if the name starts with a numeric
name := fi.Name()
if name[0] < '0' || name[0] > '9' {
continue
}
// From this point forward, any errors we just ignore, because
// it might simply be that the process doesn't exist anymore.
pid, err := strconv.ParseInt(name, 10, 0)
if err != nil {
continue
}
p, err := newUnixProcess(int(pid))
if err != nil {
continue
}
results = append(results, p)
}
}
return results, nil
}
func newUnixProcess(pid int) (*UnixProcess, error) {
p := &UnixProcess{pid: pid}
return p, p.Refresh()
}

119
vendor/github.com/mitchellh/go-ps/process_windows.go generated vendored Normal file
View File

@ -0,0 +1,119 @@
// +build windows
package ps
import (
"fmt"
"syscall"
"unsafe"
)
// Windows API functions
var (
modKernel32 = syscall.NewLazyDLL("kernel32.dll")
procCloseHandle = modKernel32.NewProc("CloseHandle")
procCreateToolhelp32Snapshot = modKernel32.NewProc("CreateToolhelp32Snapshot")
procProcess32First = modKernel32.NewProc("Process32FirstW")
procProcess32Next = modKernel32.NewProc("Process32NextW")
)
// Some constants from the Windows API
const (
ERROR_NO_MORE_FILES = 0x12
MAX_PATH = 260
)
// PROCESSENTRY32 is the Windows API structure that contains a process's
// information.
type PROCESSENTRY32 struct {
Size uint32
CntUsage uint32
ProcessID uint32
DefaultHeapID uintptr
ModuleID uint32
CntThreads uint32
ParentProcessID uint32
PriorityClassBase int32
Flags uint32
ExeFile [MAX_PATH]uint16
}
// WindowsProcess is an implementation of Process for Windows.
type WindowsProcess struct {
pid int
ppid int
exe string
}
func (p *WindowsProcess) Pid() int {
return p.pid
}
func (p *WindowsProcess) PPid() int {
return p.ppid
}
func (p *WindowsProcess) Executable() string {
return p.exe
}
func newWindowsProcess(e *PROCESSENTRY32) *WindowsProcess {
// Find when the string ends for decoding
end := 0
for {
if e.ExeFile[end] == 0 {
break
}
end++
}
return &WindowsProcess{
pid: int(e.ProcessID),
ppid: int(e.ParentProcessID),
exe: syscall.UTF16ToString(e.ExeFile[:end]),
}
}
func findProcess(pid int) (Process, error) {
ps, err := processes()
if err != nil {
return nil, err
}
for _, p := range ps {
if p.Pid() == pid {
return p, nil
}
}
return nil, nil
}
func processes() ([]Process, error) {
handle, _, _ := procCreateToolhelp32Snapshot.Call(
0x00000002,
0)
if handle < 0 {
return nil, syscall.GetLastError()
}
defer procCloseHandle.Call(handle)
var entry PROCESSENTRY32
entry.Size = uint32(unsafe.Sizeof(entry))
ret, _, _ := procProcess32First.Call(handle, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
return nil, fmt.Errorf("Error retrieving process info.")
}
results := make([]Process, 0, 50)
for {
results = append(results, newWindowsProcess(&entry))
ret, _, _ := procProcess32Next.Call(handle, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
break
}
}
return results, nil
}

2
vendor/modules.txt vendored
View File

@ -32,6 +32,8 @@ github.com/mattn/go-colorable
github.com/mattn/go-isatty
# github.com/mattn/go-runewidth v0.0.3
github.com/mattn/go-runewidth
# github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936
github.com/mitchellh/go-ps
# github.com/mitchellh/go-wordwrap v1.0.0
github.com/mitchellh/go-wordwrap
# github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e