Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32c71bd698 | |||
| 8277fa7ac6 | |||
| 3b6c4bbb7d | |||
| 1196f1d389 | |||
| 2824ee4c62 | |||
| e7a02191d8 | |||
| 693e61d7d4 | |||
| 258623ae44 | |||
| 8b6479dc95 | |||
| 3513b64aa7 | |||
| b64dbc6ca6 | |||
| 6c6c0123ed | |||
|
|
b7989893cd | ||
| c84dc517a9 | |||
| 34ed9cc065 | |||
| f03a0755af | |||
| cc0176e058 | |||
| b3cbca14c6 | |||
| 6ec2de0602 | |||
| 76710d58fa | |||
| 94c00a777d | |||
| c78cd82059 | |||
| c8453f8d54 | |||
| 04ee9550ee | |||
| a31ba75927 | |||
| 386a6694e3 | |||
| f97f217bc6 | |||
| f95897cf30 |
79
README.md
79
README.md
@ -1,11 +1,17 @@
|
|||||||
# 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.
|
> Success: "foo" started as a "launchd" SYSTEM service, running as "root"
|
||||||
(see more in the **Why** section below)
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
Because it sucks to debug launchctl, systemd, etc.
|
||||||
|
|
||||||
|
Also, I wanted a reasonable way to install [Telebit](https://telebit.io) on Windows.
|
||||||
|
(see more in the **More Why** section below)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -29,11 +35,12 @@ Because debugging launchctl, systemd, etc absolutely sucks!
|
|||||||
- node
|
- node
|
||||||
- python
|
- python
|
||||||
- ruby
|
- ruby
|
||||||
|
- PATH
|
||||||
- Logging
|
- Logging
|
||||||
- Debugging
|
- Debugging
|
||||||
- Windows
|
- Windows
|
||||||
- Building
|
- Building
|
||||||
- Why
|
- More Why
|
||||||
- Legal
|
- Legal
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
@ -44,6 +51,7 @@ The basic pattern of usage:
|
|||||||
sudo serviceman add --name "foobar" [options] [interpreter] <service> [--] [service options]
|
sudo serviceman add --name "foobar" [options] [interpreter] <service> [--] [service options]
|
||||||
sudo serviceman start <service>
|
sudo serviceman start <service>
|
||||||
sudo serviceman stop <service>
|
sudo serviceman stop <service>
|
||||||
|
sudo serviceman list --all
|
||||||
serviceman version
|
serviceman version
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -69,6 +77,24 @@ The **default** is to register a _user_ services. To register a _system_ service
|
|||||||
|
|
||||||
# Install
|
# Install
|
||||||
|
|
||||||
|
You can install `serviceman` directly from the official git releases with [`webi`](https://webinstall.dev/serviceman):
|
||||||
|
|
||||||
|
**Mac**, **Linux**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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.
|
There are a number of pre-built binaries.
|
||||||
|
|
||||||
If none of them work for you, or you prefer to build from source,
|
If none of them work for you, or you prefer to build from source,
|
||||||
@ -76,14 +102,25 @@ see the instructions for building far down below.
|
|||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -fsSL "https://rootprojects.org/serviceman/dist/$(uname -s)/$(uname -m)/serviceman" -o serviceman
|
||||||
|
chmod +x ./serviceman
|
||||||
|
```
|
||||||
|
|
||||||
### MacOS
|
### MacOS
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>See download options</summary>
|
||||||
|
|
||||||
MacOS (darwin): [64-bit Download ](https://rootprojects.org/serviceman/dist/darwin/amd64/serviceman)
|
MacOS (darwin): [64-bit Download ](https://rootprojects.org/serviceman/dist/darwin/amd64/serviceman)
|
||||||
|
|
||||||
```
|
```
|
||||||
curl https://rootprojects.org/serviceman/dist/darwin/amd64/serviceman -o serviceman
|
curl https://rootprojects.org/serviceman/dist/darwin/amd64/serviceman -o serviceman
|
||||||
|
chmod +x ./serviceman
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@ -116,6 +153,7 @@ powershell.exe "(New-Object Net.WebClient).DownloadFile('https://rootprojects.or
|
|||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>See download options</summary>
|
<summary>See download options</summary>
|
||||||
|
|
||||||
@ -123,12 +161,14 @@ Linux (64-bit): [Download](https://rootprojects.org/serviceman/dist/linux/amd64/
|
|||||||
|
|
||||||
```
|
```
|
||||||
curl https://rootprojects.org/serviceman/dist/linux/amd64/serviceman -o 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)
|
Linux (32-bit): [Download](https://rootprojects.org/serviceman/dist/linux/386/serviceman)
|
||||||
|
|
||||||
```
|
```
|
||||||
curl https://rootprojects.org/serviceman/dist/linux/386/serviceman -o serviceman
|
curl https://rootprojects.org/serviceman/dist/linux/386/serviceman -o serviceman
|
||||||
|
chmod +x ./serviceman
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@ -142,24 +182,28 @@ RPi 4 (64-bit armv8): [Download](https://rootprojects.org/serviceman/dist/linux/
|
|||||||
|
|
||||||
```
|
```
|
||||||
curl https://rootprojects.org/serviceman/dist/linux/armv8/serviceman -o 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)
|
RPi 3 (armv7): [Download](https://rootprojects.org/serviceman/dist/linux/armv7/serviceman)
|
||||||
|
|
||||||
```
|
```
|
||||||
curl https://rootprojects.org/serviceman/dist/linux/armv7/serviceman -o 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)
|
ARMv6: [Download](https://rootprojects.org/serviceman/dist/linux/armv6/serviceman)
|
||||||
|
|
||||||
```
|
```
|
||||||
curl https://rootprojects.org/serviceman/dist/linux/armv6/serviceman -o 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)
|
RPi Zero (armv5): [Download](https://rootprojects.org/serviceman/dist/linux/armv5/serviceman)
|
||||||
|
|
||||||
```
|
```
|
||||||
curl https://rootprojects.org/serviceman/dist/linux/armv5/serviceman -o serviceman
|
curl https://rootprojects.org/serviceman/dist/linux/armv5/serviceman -o serviceman
|
||||||
|
chmod +x ./serviceman
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@ -177,7 +221,6 @@ reg add HKEY_CURRENT_USER\Environment /v PATH /d "%PATH%;%userprofile%\bin"
|
|||||||
**All Others**
|
**All Others**
|
||||||
|
|
||||||
```
|
```
|
||||||
chmod a+x ./serviceman
|
|
||||||
sudo mv ./serviceman /usr/local/bin/
|
sudo mv ./serviceman /usr/local/bin/
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -386,6 +429,26 @@ See **Using with scripts** for more detailed information.
|
|||||||
|
|
||||||
</details>
|
</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
|
## Hints
|
||||||
|
|
||||||
- If something goes wrong, read the output **completely** - it'll probably be helpful
|
- If something goes wrong, read the output **completely** - it'll probably be helpful
|
||||||
@ -567,7 +630,7 @@ go build -mod=vendor -ldflags "-H=windowsgui" -o serviceman.exe
|
|||||||
go build -mod=vendor -o /usr/local/bin/serviceman
|
go build -mod=vendor -o /usr/local/bin/serviceman
|
||||||
```
|
```
|
||||||
|
|
||||||
# Why
|
# More Why
|
||||||
|
|
||||||
I created this for two reasons:
|
I created this for two reasons:
|
||||||
|
|
||||||
|
|||||||
@ -39,5 +39,5 @@ echo "RPi Zero ARMv5"
|
|||||||
GOOS=linux GOARCH=arm GOARM=5 go build -mod=vendor -o dist/linux/armv5/${exe} $gocmd
|
GOOS=linux GOARCH=arm GOARM=5 go build -mod=vendor -o dist/linux/armv5/${exe} $gocmd
|
||||||
|
|
||||||
echo ""
|
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
|
# https://rootprojects.org/serviceman/dist/windows/amd64/serviceman.exe
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
@ -27,6 +28,8 @@
|
|||||||
{{if .User -}}
|
{{if .User -}}
|
||||||
<key>UserName</key>
|
<key>UserName</key>
|
||||||
<string>{{ .User }}</string>
|
<string>{{ .User }}</string>
|
||||||
|
{{end -}}
|
||||||
|
{{if .Group -}}
|
||||||
<key>GroupName</key>
|
<key>GroupName</key>
|
||||||
<string>{{ .Group }}</string>
|
<string>{{ .Group }}</string>
|
||||||
<key>InitGroups</key>
|
<key>InitGroups</key>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
# Generated for serviceman. Edit as you wish, but leave this line.
|
||||||
# Pre-req
|
# Pre-req
|
||||||
# sudo mkdir -p {{ .Local }}/opt/{{ .Name }}/ {{ .Local }}/var/log/{{ .Name }}
|
# sudo mkdir -p {{ .Local }}/opt/{{ .Name }}/ {{ .Local }}/var/log/{{ .Name }}
|
||||||
{{ if .System -}}
|
{{ if .System -}}
|
||||||
@ -35,6 +36,9 @@ User={{ .User }}
|
|||||||
Group={{ .Group }}
|
Group={{ .Group }}
|
||||||
|
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
|
{{- if .Envs }}
|
||||||
|
Environment="{{- range $key, $value := .Envs }}{{ $key }}={{ $value }};{{- end }}"
|
||||||
|
{{- end }}
|
||||||
{{ if .Workdir -}}
|
{{ if .Workdir -}}
|
||||||
WorkingDirectory={{ .Workdir }}
|
WorkingDirectory={{ .Workdir }}
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
|
|||||||
@ -51,6 +51,10 @@ func Stop(conf *service.Service) error {
|
|||||||
return stop(conf)
|
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
|
// IsPrivileged returns true if we suspect that the current user (or process) will be able
|
||||||
// to write to system folders, bind to privileged ports, and otherwise
|
// to write to system folders, bind to privileged ports, and otherwise
|
||||||
// successfully run a system service.
|
// successfully run a system service.
|
||||||
@ -67,6 +71,16 @@ func WhereIs(exe string) (string, error) {
|
|||||||
return filepath.Abs(filepath.ToSlash(exepath))
|
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 {
|
type ErrDaemonize struct {
|
||||||
DaemonArgs []string
|
DaemonArgs []string
|
||||||
error string
|
error string
|
||||||
|
|||||||
@ -108,6 +108,7 @@ func stop(conf *service.Service) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render will create a launchd .plist file using the simple internal template
|
||||||
func Render(c *service.Service) ([]byte, error) {
|
func Render(c *service.Service) ([]byte, error) {
|
||||||
// Create service file from template
|
// Create service file from template
|
||||||
b, err := static.ReadFile("dist/Library/LaunchDaemons/_rdns_.plist.tmpl")
|
b, err := static.ReadFile("dist/Library/LaunchDaemons/_rdns_.plist.tmpl")
|
||||||
@ -142,7 +143,7 @@ func install(c *service.Service) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check paths first
|
// Check paths first
|
||||||
err := os.MkdirAll(filepath.Dir(plistDir), 0755)
|
err := os.MkdirAll(plistDir, 0755)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -159,7 +159,10 @@ func stop(conf *service.Service) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render will create a systemd .service file using the simple internal template
|
||||||
func Render(c *service.Service) ([]byte, error) {
|
func Render(c *service.Service) ([]byte, error) {
|
||||||
|
defaultUserGroup(c)
|
||||||
|
|
||||||
// Create service file from template
|
// Create service file from template
|
||||||
b, err := static.ReadFile("dist/etc/systemd/system/_name_.service.tmpl")
|
b, err := static.ReadFile("dist/etc/systemd/system/_name_.service.tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -181,15 +184,7 @@ func Render(c *service.Service) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func install(c *service.Service) (string, error) {
|
func install(c *service.Service) (string, error) {
|
||||||
// Linux-specific config options
|
defaultUserGroup(c)
|
||||||
if c.System {
|
|
||||||
if "" == c.User {
|
|
||||||
c.User = "root"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if "" == c.Group {
|
|
||||||
c.Group = c.User
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check paths first
|
// Check paths first
|
||||||
serviceDir := srvSysPath
|
serviceDir := srvSysPath
|
||||||
@ -230,3 +225,15 @@ func install(c *service.Service) (string, error) {
|
|||||||
|
|
||||||
return "systemd", nil
|
return "systemd", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultUserGroup(c *service.Service) {
|
||||||
|
// Linux-specific config options
|
||||||
|
if c.System {
|
||||||
|
if "" == c.User {
|
||||||
|
c.User = "root"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if "" == c.Group {
|
||||||
|
c.Group = c.User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
74
manager/install_nixes.go
Normal file
74
manager/install_nixes.go
Normal 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
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -130,6 +131,48 @@ func stop(conf *service.Service) error {
|
|||||||
return runner.Stop(conf)
|
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 {
|
func getRunnerArgs(c *service.Service) []string {
|
||||||
self := os.Args[0]
|
self := os.Args[0]
|
||||||
debug := ""
|
debug := ""
|
||||||
@ -154,6 +197,84 @@ func getRunnerArgs(c *service.Service) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// copies self to install path and returns config path
|
||||||
func installServiceman(c *service.Service) ([]string, error) {
|
func installServiceman(c *service.Service) ([]string, error) {
|
||||||
// TODO check version and upgrade or dismiss
|
// TODO check version and upgrade or dismiss
|
||||||
@ -168,6 +289,13 @@ func installServiceman(c *service.Service) ([]string, error) {
|
|||||||
if nil != err {
|
if nil != err {
|
||||||
return 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)
|
bin, err := ioutil.ReadFile(self)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package manager
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@ -121,7 +122,12 @@ func getSystemSrvs() ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getUserSrvs(home string) ([]string, error) {
|
func getUserSrvs(home string) ([]string, error) {
|
||||||
return getSrvs(filepath.Join(home, srvUserPath))
|
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"
|
// "come.example.foo.plist" matches "foo"
|
||||||
|
|||||||
32
manager/start_stop_test.go
Normal file
32
manager/start_stop_test.go
Normal 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
1
npm/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
28
npm/README.md
Normal file
28
npm/README.md
Normal 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
1
npm/bin/serviceman
Normal file
@ -0,0 +1 @@
|
|||||||
|
# this will be replaced by the postinstall script
|
||||||
18
npm/package-lock.json
generated
Normal file
18
npm/package-lock.json
generated
Normal 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
39
npm/package.json
Normal 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
269
npm/scripts/fetch-serviceman.js
Executable 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();
|
||||||
|
}
|
||||||
@ -78,6 +78,11 @@ func Start(conf *service.Service) error {
|
|||||||
if "" != conf.Workdir {
|
if "" != conf.Workdir {
|
||||||
cmd.Dir = 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()
|
err = cmd.Start()
|
||||||
if nil != err {
|
if nil != err {
|
||||||
fmt.Fprintf(lf, "[%s] Could not start %q process: %s\n", time.Now(), conf.Name, err)
|
fmt.Fprintf(lf, "[%s] Could not start %q process: %s\n", time.Now(), conf.Name, err)
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var GitRev = "000000000"
|
var GitRev = "000000000"
|
||||||
var GitVersion = "v0.3.2-pre+dirty"
|
var GitVersion = "v0.5.3-pre+dirty"
|
||||||
var GitTimestamp = time.Now().Format(time.RFC3339)
|
var GitTimestamp = time.Now().Format(time.RFC3339)
|
||||||
|
|
||||||
func usage() {
|
func usage() {
|
||||||
@ -30,6 +30,7 @@ func usage() {
|
|||||||
fmt.Println("\tserviceman <command> --help")
|
fmt.Println("\tserviceman <command> --help")
|
||||||
fmt.Println("\tserviceman add ./foo-app -- --foo-arg")
|
fmt.Println("\tserviceman add ./foo-app -- --foo-arg")
|
||||||
fmt.Println("\tserviceman run --config ./foo-app.json")
|
fmt.Println("\tserviceman run --config ./foo-app.json")
|
||||||
|
fmt.Println("\tserviceman list --all")
|
||||||
fmt.Println("\tserviceman start <name>")
|
fmt.Println("\tserviceman start <name>")
|
||||||
fmt.Println("\tserviceman stop <name>")
|
fmt.Println("\tserviceman stop <name>")
|
||||||
}
|
}
|
||||||
@ -54,6 +55,8 @@ func main() {
|
|||||||
start()
|
start()
|
||||||
case "stop":
|
case "stop":
|
||||||
stop()
|
stop()
|
||||||
|
case "list":
|
||||||
|
list()
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "Unknown argument %s\n", top)
|
fmt.Fprintf(os.Stderr, "Unknown argument %s\n", top)
|
||||||
usage()
|
usage()
|
||||||
@ -70,6 +73,7 @@ func add() {
|
|||||||
forUser := false
|
forUser := false
|
||||||
forSystem := false
|
forSystem := false
|
||||||
dryrun := false
|
dryrun := false
|
||||||
|
pathEnv := ""
|
||||||
flag.StringVar(&conf.Title, "title", "", "a human-friendly name for the service")
|
flag.StringVar(&conf.Title, "title", "", "a human-friendly name for the service")
|
||||||
flag.StringVar(&conf.Desc, "desc", "", "a human-friendly description of the service (ex: Foo App)")
|
flag.StringVar(&conf.Desc, "desc", "", "a human-friendly description of the service (ex: Foo App)")
|
||||||
flag.StringVar(&conf.Name, "name", "", "a computer-friendly name for the service (ex: foo-app)")
|
flag.StringVar(&conf.Name, "name", "", "a computer-friendly name for the service (ex: foo-app)")
|
||||||
@ -79,17 +83,11 @@ func add() {
|
|||||||
flag.BoolVar(&forSystem, "system", false, "attempt to add system service as an unprivileged/unelevated user")
|
flag.BoolVar(&forSystem, "system", false, "attempt to add system service as an unprivileged/unelevated user")
|
||||||
flag.BoolVar(&forUser, "user", false, "add user space / user mode service even when admin/root/sudo/elevated")
|
flag.BoolVar(&forUser, "user", false, "add user space / user mode service even when admin/root/sudo/elevated")
|
||||||
flag.BoolVar(&force, "force", false, "if the interpreter or executable doesn't exist, or things don't make sense, try anyway")
|
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.User, "username", "", "run the service as this user")
|
||||||
flag.StringVar(&conf.Group, "groupname", "", "run the service as this group")
|
flag.StringVar(&conf.Group, "groupname", "", "run the service as this group")
|
||||||
flag.BoolVar(&conf.PrivilegedPorts, "cap-net-bind", false, "this service should have access to privileged ports")
|
flag.BoolVar(&conf.PrivilegedPorts, "cap-net-bind", false, "this service should have access to privileged ports")
|
||||||
flag.BoolVar(&dryrun, "dryrun", false, "output the service file without modifying anything on disk")
|
flag.BoolVar(&dryrun, "dryrun", false, "output the service file without modifying anything on disk")
|
||||||
flag.Usage = func() {
|
|
||||||
fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0])
|
|
||||||
|
|
||||||
flag.PrintDefaults()
|
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "Flags and arguments after \"--\" will be completely ignored by serviceman\n", os.Args[0])
|
|
||||||
}
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
flagargs := flag.Args()
|
flagargs := flag.Args()
|
||||||
|
|
||||||
@ -163,6 +161,10 @@ func add() {
|
|||||||
ass = append(ass, fmt.Sprintf(" --name %s", conf.Name))
|
ass = append(ass, fmt.Sprintf(" --name %s", conf.Name))
|
||||||
ass = append(ass, "")
|
ass = append(ass, "")
|
||||||
}
|
}
|
||||||
|
if "" != pathEnv {
|
||||||
|
conf.Envs = make(map[string]string)
|
||||||
|
conf.Envs["PATH"] = pathEnv
|
||||||
|
}
|
||||||
|
|
||||||
exepath, err := findExec(flagargs[0], force)
|
exepath, err := findExec(flagargs[0], force)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
@ -228,7 +230,7 @@ func add() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(ass) > 0 {
|
if len(ass) > 0 {
|
||||||
fmt.Println("OPTIONS: Making some assumptions...\n")
|
fmt.Printf("OPTIONS: Making some assumptions...\n\n")
|
||||||
for i := range ass {
|
for i := range ass {
|
||||||
fmt.Println("\t" + ass[i])
|
fmt.Println("\t" + ass[i])
|
||||||
}
|
}
|
||||||
@ -304,7 +306,7 @@ func add() {
|
|||||||
servicemode = "SYSTEM"
|
servicemode = "SYSTEM"
|
||||||
}
|
}
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
"SUCCESS:\n\n\t%q started as a %q %s service, running as %q\n",
|
"SUCCESS:\n\n\t%q started as a %s %s service, running as %q\n",
|
||||||
conf.Name,
|
conf.Name,
|
||||||
servicetype,
|
servicetype,
|
||||||
servicemode,
|
servicemode,
|
||||||
@ -313,6 +315,62 @@ func add() {
|
|||||||
fmt.Println()
|
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 {
|
||||||
|
conf.System = true
|
||||||
|
} else {
|
||||||
|
conf.System = manager.IsPrivileged()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func findExec(exe string, force bool) (string, error) {
|
||||||
// ex: node => /usr/local/bin/node
|
// ex: node => /usr/local/bin/node
|
||||||
// ex: ./demo.js => /Users/aj/project/demo.js
|
// ex: ./demo.js => /Users/aj/project/demo.js
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user