Compare commits

...

24 Commits

Author SHA1 Message Date
AJ ONeal 32c71bd698 add webi install instructions 2020-07-06 02:42:08 +00:00
AJ ONeal 8277fa7ac6 add canonical link 2020-04-22 19:22:41 +00:00
AJ ONeal 3b6c4bbb7d update curl command 2020-01-18 02:18:08 +00:00
AJ ONeal 1196f1d389 more generic download command 2020-01-18 02:16:15 +00:00
AJ ONeal 2824ee4c62 test service directory creation 2019-08-10 20:13:51 -06:00
AJ ONeal e7a02191d8 lint: clarify double newline 2019-08-10 20:12:43 -06:00
AJ ONeal 693e61d7d4 address windows bug with incomplete args[0] paths 2019-08-10 15:54:56 -06:00
AJ ONeal 258623ae44 update generated files 2019-08-10 15:54:34 -06:00
AJ ONeal 8b6479dc95 update serviceman on npm 2019-08-09 01:20:17 -06:00
AJ ONeal 3513b64aa7 add 'serviceman list --all' to docs 2019-08-05 10:09:11 -06:00
AJ ONeal b64dbc6ca6 build script: don't publish after build 2019-08-05 10:07:18 -06:00
AJ ONeal 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
AJ ONeal c84dc517a9 add error hints 2019-08-05 05:57:50 -06:00
AJ ONeal 34ed9cc065 list() for Mac and Linux 2019-08-05 05:53:32 -06:00
AJ ONeal f03a0755af list Windows user services 2019-08-05 05:20:50 -06:00
AJ ONeal cc0176e058 add serviceman comments 2019-08-05 05:20:50 -06:00
AJ ONeal b3cbca14c6 template update 2019-08-05 04:51:00 -06:00
AJ ONeal 6ec2de0602 minor doc update for PATH 2019-08-05 04:47:27 -06:00
Ryan Burnette 76710d58fa progress 2019-08-05 04:43:38 -06:00
Ryan Burnette 94c00a777d M manager/dist/etc/systemd/system/_name_.service.tmpl
M  serviceman.go
2019-08-05 04:43:38 -06:00
AJ ONeal c78cd82059 fix #5: only use group when specified in both dry-run and real templates 2019-08-05 04:40:47 -06:00
AJ ONeal c8453f8d54 make chmod instruction more obvious 2019-07-28 05:14:33 -06:00
AJ ONeal 04ee9550ee bump source build ver 2019-07-14 02:06:45 -06:00
17 changed files with 556 additions and 88 deletions

View File

@ -1,4 +1,4 @@
# go-serviceman
# [go-serviceman](https://git.rootprojects.org/root/serviceman)
Cross-platform service management made easy.
@ -35,6 +35,7 @@ Also, I wanted a reasonable way to install [Telebit](https://telebit.io) on Wind
- node
- python
- ruby
- PATH
- Logging
- Debugging
- Windows
@ -50,6 +51,7 @@ The basic pattern of usage:
sudo serviceman add --name "foobar" [options] [interpreter] <service> [--] [service options]
sudo serviceman start <service>
sudo serviceman stop <service>
sudo serviceman list --all
serviceman version
```
@ -75,6 +77,24 @@ The **default** is to register a _user_ services. To register a _system_ service
# 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.
If none of them work for you, or you prefer to build from source,
@ -82,14 +102,25 @@ 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>
@ -122,6 +153,7 @@ powershell.exe "(New-Object Net.WebClient).DownloadFile('https://rootprojects.or
### Linux
<details>
<summary>See download options</summary>
@ -129,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
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>
@ -148,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`
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>
@ -183,7 +221,6 @@ reg add HKEY_CURRENT_USER\Environment /v PATH /d "%PATH%;%userprofile%\bin"
**All Others**
```
chmod a+x ./serviceman
sudo mv ./serviceman /usr/local/bin/
```
@ -392,6 +429,26 @@ 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
@ -592,4 +649,3 @@ MPL-2.0 |
Copyright 2019 AJ ONeal.
<!-- {{ end }} -->
<!-- {{ end }} -->

View File

@ -39,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

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 -}}
@ -35,6 +36,9 @@ User={{ .User }}
Group={{ .Group }}
{{ end -}}
{{- if .Envs }}
Environment="{{- range $key, $value := .Envs }}{{ $key }}={{ $value }};{{- end }}"
{{- end }}
{{ if .Workdir -}}
WorkingDirectory={{ .Workdir }}
{{ end -}}

View File

@ -51,6 +51,10 @@ 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
// to write to system folders, bind to privileged ports, and otherwise
// successfully run a system service.
@ -67,6 +71,16 @@ 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

View File

@ -108,6 +108,7 @@ func stop(conf *service.Service) error {
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")
@ -142,7 +143,7 @@ func install(c *service.Service) (string, error) {
}
// Check paths first
err := os.MkdirAll(filepath.Dir(plistDir), 0755)
err := os.MkdirAll(plistDir, 0755)
if nil != err {
return "", err
}

View File

@ -159,7 +159,10 @@ func stop(conf *service.Service) error {
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 {
@ -181,15 +184,7 @@ func Render(c *service.Service) ([]byte, error) {
}
func install(c *service.Service) (string, error) {
// Linux-specific config options
if c.System {
if "" == c.User {
c.User = "root"
}
}
if "" == c.Group {
c.Group = c.User
}
defaultUserGroup(c)
// Check paths first
serviceDir := srvSysPath
@ -230,3 +225,15 @@ func install(c *service.Service) (string, error) {
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
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,7 @@ import (
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
@ -130,6 +131,48 @@ 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 := ""
@ -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
func installServiceman(c *service.Service) ([]string, error) {
// TODO check version and upgrade or dismiss
@ -168,6 +289,13 @@ func installServiceman(c *service.Service) ([]string, error) {
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

View File

@ -3,6 +3,7 @@ package manager
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
@ -121,7 +122,12 @@ func getSystemSrvs() ([]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"

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/bin/serviceman Normal file
View File

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

View File

@ -1,10 +1,19 @@
{
"name": "serviceman",
"version": "0.5.2",
"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": {
"postinstall": "scripts/fetch-serviceman.js",
"serviceman": "serviceman",
"postinstall": "node scripts/fetch-serviceman.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {

View File

@ -1,6 +1,7 @@
#!/usr/bin/env node
'use strict';
var path = require('path');
var os = require('os');
// https://nodejs.org/api/os.html#os_os_arch
@ -10,7 +11,7 @@ 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 = 'windows' === platform ? '.exe' : '';
var ext = /^win/i.test(platform) ? '.exe' : '';
// This is _probably_ right. It's good enough for us
// https://github.com/nodejs/node/issues/13629
@ -53,15 +54,26 @@ var mkdirp = require('@root/mkdirp');
function needsUpdate(oldVer, newVer) {
// "v1.0.0-pre" is BEHIND "v1.0.0"
newVer = newVer.replace(/^v/, '').split(/[\.\-\+]/);
oldVer = oldVer.replace(/^v/, '').split(/[\.\-\+]/);
//console.log(oldVer, newVer);
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;
@ -77,6 +89,8 @@ function needsUpdate(oldVer, newVer) {
return true;
}
}
// ex: v1.0.1 vs v1.0.0-pre
if (newVer[3]) {
if (newVer[2] > 0) {
newVer[2] -= 1;
@ -93,7 +107,7 @@ function needsUpdate(oldVer, newVer) {
}
}
//console.log(oldVer, newVer);
// ex: v1.0.1 vs v1.0.0
if (oldVer[0] > newVer[0]) {
return false;
} else if (oldVer[0] < newVer[0]) {
@ -128,72 +142,128 @@ console.log(false === needsUpdate('0.5.0', '0.5.0-pre1'));
console.log(false === needsUpdate('0.5.1', '0.5.0'));
*/
exec('serviceman version', { windowsHide: true }, function(err, stdout) {
var oldVer = (stdout || '').split(' ')[0];
console.log(oldVer, newVer);
if (!needsUpdate(oldVer, newVer)) {
console.info(
'Current serviceman version is new enough:',
oldVer,
newVer
);
return;
//} else {
// console.info('Current serviceman version is older:', oldVer, newVer);
}
var url = 'https://rootprojects.org/serviceman/dist/{{ .Platform }}/{{ .Arch }}/serviceman{{ .Ext }}'
.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);
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);
}
//console.log(resp.body.byteLength);
//console.log(typeof resp.body);
var serviceman = 'serviceman' + ext;
return fs.writeFile(serviceman, resp.body, null, function(err) {
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;
}
fs.chmodSync(serviceman, parseInt('0755', 8));
var path = require('path');
var localdir = '/usr/local/bin';
fs.rename(serviceman, path.join(localdir, serviceman), function(
err
) {
if (err) {
//console.error(err);
}
// ignore
});
var homedir = require('os').homedir();
var bindir = path.join(homedir, '.local', 'bin');
return mkdirp(bindir, function(err) {
if (err) {
console.error(err);
//console.log(resp.body.byteLength);
//console.log(typeof resp.body);
var bin = name + ext;
function next() {
if (!bindirs.length) {
return;
}
var localsrv = path.join(bindir, serviceman);
return fs.writeFile(localsrv, resp.body, function(err) {
var bindir = bindirs.pop();
return mkdirp(bindir, function(err) {
if (err) {
console.error(err);
return;
}
fs.chmodSync(localsrv, parseInt('0755', 8));
console.info('Wrote', serviceman, 'to', bindir);
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

@ -78,6 +78,11 @@ func Start(conf *service.Service) error {
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)

View File

@ -22,7 +22,7 @@ import (
)
var GitRev = "000000000"
var GitVersion = "v0.3.2-pre+dirty"
var GitVersion = "v0.5.3-pre+dirty"
var GitTimestamp = time.Now().Format(time.RFC3339)
func usage() {
@ -30,6 +30,7 @@ func 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>")
}
@ -54,6 +55,8 @@ func main() {
start()
case "stop":
stop()
case "list":
list()
default:
fmt.Fprintf(os.Stderr, "Unknown argument %s\n", top)
usage()
@ -70,6 +73,7 @@ func add() {
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)")
@ -79,17 +83,11 @@ func add() {
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.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()
flagargs := flag.Args()
@ -163,6 +161,10 @@ func add() {
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 {
@ -228,7 +230,7 @@ func add() {
}
if len(ass) > 0 {
fmt.Println("OPTIONS: Making some assumptions...\n")
fmt.Printf("OPTIONS: Making some assumptions...\n\n")
for i := range ass {
fmt.Println("\t" + ass[i])
}
@ -304,7 +306,7 @@ func add() {
servicemode = "SYSTEM"
}
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,
servicetype,
servicemode,
@ -313,6 +315,62 @@ func add() {
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) {
// ex: node => /usr/local/bin/node
// ex: ./demo.js => /Users/aj/project/demo.js