basic functionality works
This commit is contained in:
parent
17cbc8ece3
commit
84f1dacbac
16
README.md
16
README.md
|
@ -1,3 +1,17 @@
|
||||||
# go-serviceman
|
# go-serviceman
|
||||||
|
|
||||||
A cross-platform service manager.
|
A cross-platform service manager.
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
serviceman install [options] [interpreter] <service> [-- [options]]
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
serviceman install --user ./foo-app -- -c ./
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
serviceman install --user /usr/local/bin/node ./whatever.js -- -c ./
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
module git.rootprojects.org/root/go-serviceman
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.rootprojects.org/root/go-gitver v1.1.2
|
||||||
|
github.com/UnnoTed/fileb0x v1.1.3
|
||||||
|
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f
|
||||||
|
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8
|
||||||
|
)
|
|
@ -0,0 +1,52 @@
|
||||||
|
git.rootprojects.org/root/go-gitver v1.1.2 h1:AQhr8ktJyP+X+jFbtLavCi/FQLSmB6xvdG2Nfp+J2JA=
|
||||||
|
git.rootprojects.org/root/go-gitver v1.1.2/go.mod h1:Rj1v3TBhvdaSphFEqMynUYwAz/4f+wY/+syBTvRrmlI=
|
||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/UnnoTed/fileb0x v1.1.3 h1:TUfJRey+psXuivBqasgp7Du3iXB4hzjI5UXDl+BCrzE=
|
||||||
|
github.com/UnnoTed/fileb0x v1.1.3/go.mod h1:AyTnLP7elx6MM4eHxahl5sBEWBw0QLf6TM/s64LtM4s=
|
||||||
|
github.com/airking05/termui v2.2.0+incompatible h1:S3j2WJzr70u8KjUktaQ0Cmja+R0edOXChltFoQSGG8I=
|
||||||
|
github.com/airking05/termui v2.2.0+incompatible/go.mod h1:B/M5sgOwSZlvGm3TsR98s1BSzlSH4wPQzUUNwZG+uUM=
|
||||||
|
github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ=
|
||||||
|
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/karrick/godirwalk v1.7.8 h1:VfG72pyIxgtC7+3X9CMHI0AOl4LwyRAg98WAgsvffi8=
|
||||||
|
github.com/karrick/godirwalk v1.7.8/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34=
|
||||||
|
github.com/labstack/echo v3.2.1+incompatible h1:J2M7YArHx4gi8p/3fDw8tX19SXhBCoRpviyAZSN3I88=
|
||||||
|
github.com/labstack/echo v3.2.1+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||||
|
github.com/labstack/gommon v0.2.7 h1:2qOPq/twXDrQ6ooBGrn3mrmVOC+biLlatwgIu8lbzRM=
|
||||||
|
github.com/labstack/gommon v0.2.7/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||||
|
github.com/maruel/panicparse v1.1.1 h1:k62YPcEoLncEEpjMt92GtG5ugb8WL/510Ys3/h5IkRc=
|
||||||
|
github.com/maruel/panicparse v1.1.1/go.mod h1:nty42YY5QByNC5MM7q/nj938VbgPU7avs45z6NClpxI=
|
||||||
|
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
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-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=
|
||||||
|
github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||||
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
||||||
|
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
||||||
|
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ=
|
||||||
|
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f h1:QM2QVxvDoW9PFSPp/zy9FgxJLfaWTZlS61KEPtBwacM=
|
||||||
|
golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8 h1:R91KX5nmbbvEd7w370cbVzKC+EzCTGqZq63Zad5IcLM=
|
||||||
|
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
@ -0,0 +1,24 @@
|
||||||
|
# all folders and files are relative to the path where fileb0x was run!
|
||||||
|
|
||||||
|
pkg = "static"
|
||||||
|
dest = "./static/"
|
||||||
|
fmt = true
|
||||||
|
|
||||||
|
# build tags for the main b0x.go file
|
||||||
|
tags = ""
|
||||||
|
|
||||||
|
# default: ab0x.go (so that its init() sorts first)
|
||||||
|
output = "ab0x.go"
|
||||||
|
|
||||||
|
[[custom]]
|
||||||
|
# everything inside the folder
|
||||||
|
# type: array of strings
|
||||||
|
files = ["./dist/"]
|
||||||
|
|
||||||
|
# base is the path that will be removed from all files' path
|
||||||
|
# type: string
|
||||||
|
base = ""
|
||||||
|
|
||||||
|
# prefix is the path that will be added to all files' path
|
||||||
|
# type: string
|
||||||
|
prefix = ""
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>{{ .ReverseDNS }}</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
{{- if .Interpreter }}
|
||||||
|
<string>{{ .Interpreter }}</string>
|
||||||
|
{{- end }}
|
||||||
|
<string>{{ .Local }}/opt/{{ .Name }}/{{ .Exec }}</string>
|
||||||
|
{{- if .Argv }}
|
||||||
|
{{- range $arg := .Argv }}
|
||||||
|
<string>{{ $arg }}</string>
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
</array>
|
||||||
|
{{- if .Envs }}
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
{{- range $key, $value := .Envs }}
|
||||||
|
<key>{{ $key }}</key>
|
||||||
|
<string>{{ $value }}</string>
|
||||||
|
{{- end }}
|
||||||
|
</dict>
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{if .User -}}
|
||||||
|
<key>UserName</key>
|
||||||
|
<string>{{ .User }}</string>
|
||||||
|
<key>GroupName</key>
|
||||||
|
<string>{{ .Group }}</string>
|
||||||
|
<key>InitGroups</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
{{end -}}
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
{{ if .Restart -}}
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<!--dict>
|
||||||
|
<key>Crashed</key>
|
||||||
|
<true/>
|
||||||
|
<key>NetworkState</key>
|
||||||
|
<true/>
|
||||||
|
<key>SuccessfulExit</key>
|
||||||
|
<false/>
|
||||||
|
</dict-->
|
||||||
|
|
||||||
|
{{ end -}}
|
||||||
|
{{ if .Production -}}
|
||||||
|
<key>SoftResourceLimits</key>
|
||||||
|
<dict>
|
||||||
|
<key>NumberOfFiles</key>
|
||||||
|
<integer>8192</integer>
|
||||||
|
</dict>
|
||||||
|
<key>HardResourceLimits</key>
|
||||||
|
<dict/>
|
||||||
|
|
||||||
|
{{ end -}}
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>{{ .Local }}/opt/{{ .Name }}</string>
|
||||||
|
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>{{ .LogDir }}/{{ .Name }}.log</string>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>{{ .LogDir }}/{{ .Name }}.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Pre-req
|
||||||
|
# sudo mkdir -p {{ .Local }}/opt/{{ .Name }}/ {{ .Local }}/var/log/{{ .Name }}
|
||||||
|
{{ if not .Local -}}
|
||||||
|
{{- if and .User ( ne "root" .User ) -}}
|
||||||
|
# sudo adduser {{ .User }} --home /opt/{{ .Name }}
|
||||||
|
# sudo chown -R {{ .User }}:{{ .Group }} /opt/{{ .Name }}/ /var/log/{{ .Name }}
|
||||||
|
{{- end }}
|
||||||
|
{{ end -}}
|
||||||
|
# Post-install
|
||||||
|
# sudo systemctl {{ if .Local -}} --user {{ end -}} daemon-reload
|
||||||
|
# sudo systemctl {{ if .Local -}} --user {{ end -}} restart {{ .Name }}.service
|
||||||
|
# sudo journalctl {{ if .Local -}} --user {{ end -}} -xefu {{ .Name }}
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description={{ .Title }} - {{ .Desc }}
|
||||||
|
Documentation={{ .URL }}
|
||||||
|
{{ if not .Local -}}
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target systemd-networkd-wait-online.service
|
||||||
|
{{- 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
|
||||||
|
StartLimitInterval=10
|
||||||
|
StartLimitBurst=3
|
||||||
|
|
||||||
|
{{ if .User -}}
|
||||||
|
# User and group the process will run as
|
||||||
|
User={{ .User }}
|
||||||
|
Group={{ .Group }}
|
||||||
|
|
||||||
|
{{ end -}}
|
||||||
|
WorkingDirectory={{ .Local }}/opt/{{ .Name }}
|
||||||
|
ExecStart={{if .Interpreter }}{{ .Interpreter }} {{ end }}{{ .Local }}/opt/{{ .Name }}/{{ .Name }} {{ .Args }}
|
||||||
|
ExecReload=/bin/kill -USR1 $MAINPID
|
||||||
|
|
||||||
|
{{if .Production -}}
|
||||||
|
# Limit the number of file descriptors and processes; see `man systemd.exec` for more limit settings.
|
||||||
|
# These are reasonable defaults for a production system.
|
||||||
|
# Note: systemd "user units" do not support this
|
||||||
|
LimitNOFILE=1048576
|
||||||
|
LimitNPROC=64
|
||||||
|
|
||||||
|
{{ end -}}
|
||||||
|
{{if .MultiuserProtection -}}
|
||||||
|
# Use private /tmp and /var/tmp, which are discarded after the service stops.
|
||||||
|
PrivateTmp=true
|
||||||
|
# Use a minimal /dev
|
||||||
|
PrivateDevices=true
|
||||||
|
# Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
|
||||||
|
ProtectHome=true
|
||||||
|
# Make /usr, /boot, /etc and possibly some more folders read-only.
|
||||||
|
ProtectSystem=full
|
||||||
|
# ... except /opt/{{ .Name }} because we want a place for the database
|
||||||
|
# and /var/log/{{ .Name }} because we want a place where logs can go.
|
||||||
|
# This merely retains r/w access rights, it does not add any new.
|
||||||
|
# Must still be writable on the host!
|
||||||
|
ReadWriteDirectories=/opt/{{ .Name }} /var/log/{{ .Name }}
|
||||||
|
|
||||||
|
# Note: in v231 and above ReadWritePaths has been renamed to ReadWriteDirectories
|
||||||
|
; ReadWritePaths=/opt/{{ .Name }} /var/log/{{ .Name }}
|
||||||
|
|
||||||
|
{{ end -}}
|
||||||
|
{{if .PrivilegedPorts -}}
|
||||||
|
# The following additional security directives only work with systemd v229 or later.
|
||||||
|
# They further retrict privileges that can be gained by the service.
|
||||||
|
# Note that you may have to add capabilities required by any plugins in use.
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
# Caveat: Some features may need additional capabilities.
|
||||||
|
# For example an "upload" may need CAP_LEASE
|
||||||
|
; CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_LEASE
|
||||||
|
; AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_LEASE
|
||||||
|
; NoNewPrivileges=true
|
||||||
|
|
||||||
|
{{ end -}}
|
||||||
|
[Install]
|
||||||
|
{{ if not .Local -}}
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
{{- else -}}
|
||||||
|
WantedBy=default.target
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Package installer can be used cross-platform to install apps
|
||||||
|
// as either userspace or system services for fairly simple applications.
|
||||||
|
// This is not intended for complex installers.
|
||||||
|
//
|
||||||
|
// I'm prototyping this out to be useful for more than just watchdog
|
||||||
|
// hence there are a few unnecessary things for the sake of the trying it out
|
||||||
|
package installer
|
|
@ -0,0 +1,23 @@
|
||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// "A little copying is better than a little dependency"
|
||||||
|
// These are here so that we don't need a dependency on http.FileSystem and http.File
|
||||||
|
|
||||||
|
// FileSystem is the same as http.FileSystem
|
||||||
|
type FileSystem interface {
|
||||||
|
Open(name string) (File, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// File is the same as http.File
|
||||||
|
type File interface {
|
||||||
|
io.Closer
|
||||||
|
io.Reader
|
||||||
|
io.Seeker
|
||||||
|
Readdir(count int) ([]os.FileInfo, error)
|
||||||
|
Stat() (os.FileInfo, error)
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
//go:generate go run -mod=vendor github.com/UnnoTed/fileb0x b0x.toml
|
||||||
|
|
||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config should describe the service well-enough for it to
|
||||||
|
// run on Mac, Linux, and Windows.
|
||||||
|
//
|
||||||
|
// &Config{
|
||||||
|
// // A human-friendy name
|
||||||
|
// Title: "Foobar App",
|
||||||
|
// // A computer-friendly name
|
||||||
|
// Name: "foobar-app",
|
||||||
|
// // A name for OS X plist
|
||||||
|
// ReverseDNS: "com.example.foobar-app",
|
||||||
|
// // A human-friendly description
|
||||||
|
// Desc: "Foobar App",
|
||||||
|
// // The app /service homepage
|
||||||
|
// URL: "https://example.com/foobar-app/",
|
||||||
|
// // The full path of the interpreter, if any (ruby, python, node, etc)
|
||||||
|
// Interpreter: "/opt/node/bin/node",
|
||||||
|
// // The name of the executable (or script)
|
||||||
|
// Exec: "foobar-app.js",
|
||||||
|
// // An array of arguments
|
||||||
|
// Argv: []string{"-c", "/path/to/config.json"},
|
||||||
|
// // A map of Environment variables that should be set
|
||||||
|
// Envs: map[string]string{
|
||||||
|
// PORT: "8080",
|
||||||
|
// ENV: "development",
|
||||||
|
// },
|
||||||
|
// // The user (Linux & Mac only).
|
||||||
|
// // This does not apply to userspace services.
|
||||||
|
// // There may be special considerations
|
||||||
|
// User: "www-data",
|
||||||
|
// // If different from User
|
||||||
|
// Group: "",
|
||||||
|
// // Whether to install as a system or user service
|
||||||
|
// System: false,
|
||||||
|
// // Whether or not the service may need privileged ports
|
||||||
|
// PrivilegedPorts: false,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Note that some fields are exported for templating,
|
||||||
|
// but not intended to be set by you.
|
||||||
|
// These are documented as omitted from JSON.
|
||||||
|
// Try to stick to what's outlined above.
|
||||||
|
type Config struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
ReverseDNS string `json:"reverse_dns"` // i.e. com.example.foo-app
|
||||||
|
Interpreter string `json:"interpreter"` // i.e. node, python
|
||||||
|
Exec string `json:"exec"`
|
||||||
|
Argv []string `json:"argv"`
|
||||||
|
Args string `json:"-"`
|
||||||
|
Envs map[string]string `json:"envs"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Group string `json:"group"`
|
||||||
|
home string `json:"-"`
|
||||||
|
Local string `json:"-"`
|
||||||
|
LogDir string `json:"-"`
|
||||||
|
System bool `json:"system"`
|
||||||
|
Restart bool `json:"restart"`
|
||||||
|
Production bool `json:"production"`
|
||||||
|
PrivilegedPorts bool `json:"privileged_ports"`
|
||||||
|
MultiuserProtection bool `json:"multiuser_protection"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 *Config) error {
|
||||||
|
if "" == c.Exec {
|
||||||
|
c.Exec = c.Name
|
||||||
|
}
|
||||||
|
c.Args = strings.Join(c.Argv, " ")
|
||||||
|
|
||||||
|
// TODO handle non-system installs
|
||||||
|
// * ~/.local/opt/watchdog/watchdog
|
||||||
|
// * ~/.local/share/watchdog/var/log/
|
||||||
|
// * ~/.config/watchdog/watchdog.json
|
||||||
|
if !c.System {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.home = home
|
||||||
|
c.Local = filepath.Join(c.home, ".local")
|
||||||
|
c.LogDir = filepath.Join(c.home, ".local", "share", c.Name, "var", "log")
|
||||||
|
} else {
|
||||||
|
c.LogDir = "/var/log/" + c.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
err := install(c)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.MkdirAll(c.LogDir, 0750)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func IsPrivileged() bool {
|
||||||
|
return isPrivileged()
|
||||||
|
}
|
||||||
|
|
||||||
|
func WhereIs(exec string) (string, error) {
|
||||||
|
exec = filepath.ToSlash(exec)
|
||||||
|
if strings.Contains(exec, "/") {
|
||||||
|
// filepath.Clean(exec)
|
||||||
|
// it's a path (don't allow filenames with slashes)
|
||||||
|
// TODO stat
|
||||||
|
return exec, nil
|
||||||
|
}
|
||||||
|
return whereIs(exec)
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/go-serviceman/installer/static"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(c *Config) 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check paths first
|
||||||
|
err := os.MkdirAll(filepath.Dir(plistDir), 0750)
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create service file from template
|
||||||
|
b, err := static.ReadFile("dist/Library/LaunchDaemons/_rdns_.plist.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
|
||||||
|
// TODO rdns
|
||||||
|
plistName := c.Name + ".plist"
|
||||||
|
plistPath := filepath.Join(plistDir, plistName)
|
||||||
|
if err := ioutil.WriteFile(plistPath, rw.Bytes(), 0644); err != nil {
|
||||||
|
fmt.Println("Use 'sudo' to install as a privileged system service.")
|
||||||
|
fmt.Println("Use '--userspace' to install as an user service.")
|
||||||
|
return fmt.Errorf("ioutil.WriteFile error: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Installed. To start '%s' run the following:\n", c.Name)
|
||||||
|
// TODO template config file
|
||||||
|
fmt.Printf("\tlaunchctl load -w %s\n", strings.Replace(plistPath, c.home, "~", 1))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/go-serviceman/installer/static"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(c *Config) error {
|
||||||
|
// Linux-specific config options
|
||||||
|
if c.System {
|
||||||
|
if "" == c.User {
|
||||||
|
c.User = "root"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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), 0750)
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func whereIs(exe string) (string, error) {
|
||||||
|
cmd := exec.Command("command", "-v", exe)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if nil != err {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out)), nil
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
// +build !windows,!linux,!darwin
|
||||||
|
|
||||||
|
package installer
|
||||||
|
|
||||||
|
func install(c *Config) error {
|
||||||
|
return nil, nil
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO system service requires elevated privileges
|
||||||
|
// See https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
|
||||||
|
func install(c *Config) error {
|
||||||
|
//token := windows.Token(0)
|
||||||
|
/*
|
||||||
|
// LEAVE THIS DOCUMENTATION HERE
|
||||||
|
reg.exe
|
||||||
|
/V <value name> - "Telebit"
|
||||||
|
/T <data type> - "REG_SZ" - String
|
||||||
|
/D <value data>
|
||||||
|
/C - case sensitive
|
||||||
|
/F <search data??> - not sure...
|
||||||
|
|
||||||
|
// Special Note:
|
||||||
|
"/c" is similar to -- (*nix), and required within the data string
|
||||||
|
So instead of setting "do.exe --do-arg1 --do-arg2"
|
||||||
|
you must set "do.exe /c --do-arg1 --do-arg2"
|
||||||
|
|
||||||
|
vars.telebitNode += '.exe';
|
||||||
|
var cmd = 'reg.exe add "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"'
|
||||||
|
+ ' /V "Telebit" /t REG_SZ /D '
|
||||||
|
+ '"' + things.argv[0] + ' /c ' // something like C:\Program Files (x64)\nodejs\node.exe
|
||||||
|
+ [ path.join(__dirname, 'bin/telebitd.js')
|
||||||
|
, 'daemon'
|
||||||
|
, '--config'
|
||||||
|
, path.join(os.homedir(), '.config/telebit/telebitd.yml')
|
||||||
|
].join(' ')
|
||||||
|
+ '" /F'
|
||||||
|
;
|
||||||
|
*/
|
||||||
|
autorunKey := `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
|
||||||
|
k, _, err := registry.CreateKey(
|
||||||
|
registry.CURRENT_USER,
|
||||||
|
autorunKey,
|
||||||
|
registry.SET_VALUE,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer k.Close()
|
||||||
|
|
||||||
|
setArgs := ""
|
||||||
|
args := c.Argv
|
||||||
|
exec := filepath.Join(c.home, ".local", "opt", c.Name, c.Exec)
|
||||||
|
bin := c.Interpreter
|
||||||
|
if "" != bin {
|
||||||
|
// If this is something like node or python,
|
||||||
|
// the interpeter must be called as "the main thing"
|
||||||
|
// and "the app" must be an argument
|
||||||
|
args = append([]string{exec}, args...)
|
||||||
|
} else {
|
||||||
|
// Otherwise, if "the app" is a true binary,
|
||||||
|
// it can be "the main thing"
|
||||||
|
bin = exec
|
||||||
|
}
|
||||||
|
if 0 != len(args) {
|
||||||
|
// On Windows the /c acts kinda like -- does on *nix,
|
||||||
|
// at least for commands in the registry that have arguments
|
||||||
|
setArgs = ` /c `
|
||||||
|
}
|
||||||
|
|
||||||
|
// The final string ends up looking something like one of these:
|
||||||
|
// "C:\Users\aj\.local\opt\appname\appname.js /c -p 8080"
|
||||||
|
// "C:\Program Files (x64)\nodejs\node.exe /c C:\Users\aj\.local\opt\appname\appname.js -p 8080"
|
||||||
|
regSZ := bin + setArgs + strings.Join(c.Argv, " ")
|
||||||
|
if len(regSZ) > 260 {
|
||||||
|
return fmt.Errorf("data value is too long for registry entry")
|
||||||
|
}
|
||||||
|
fmt.Println("Set Registry Key:")
|
||||||
|
fmt.Println(autorunKey, c.Title, regSZ)
|
||||||
|
k.SetStringValue(c.Title, regSZ)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func whereIs(exe string) (string, error) {
|
||||||
|
cmd := exec.Command("where.exe", exe)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if nil != err {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out)), nil
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,15 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package installer
|
||||||
|
|
||||||
|
import "os/user"
|
||||||
|
|
||||||
|
func isPrivileged() bool {
|
||||||
|
u, err := user.Current()
|
||||||
|
if nil != err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// not quite, but close enough for now
|
||||||
|
return "0" == u.Uid
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isPrivileged() bool {
|
||||||
|
var sid *windows.SID
|
||||||
|
|
||||||
|
// Although this looks scary, it is directly copied from the
|
||||||
|
// official windows documentation. The Go API for this is a
|
||||||
|
// direct wrap around the official C++ API.
|
||||||
|
// See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
|
||||||
|
err := windows.AllocateAndInitializeSid(
|
||||||
|
&windows.SECURITY_NT_AUTHORITY,
|
||||||
|
2,
|
||||||
|
windows.SECURITY_BUILTIN_DOMAIN_RID,
|
||||||
|
windows.DOMAIN_ALIAS_RID_ADMINS,
|
||||||
|
0, 0, 0, 0, 0, 0,
|
||||||
|
&sid)
|
||||||
|
if err != nil {
|
||||||
|
// we don't believe this _can_ return an error with the given inputs
|
||||||
|
// and if it does, the important info is still the false
|
||||||
|
fmt.Fprintf(os.Stderr, "warning: Unexpected Windows UserID Error: %s\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// This appears to cast a null pointer so I'm not sure why this
|
||||||
|
// works, but this guy says it does and it Works for Me™:
|
||||||
|
// https://github.com/golang/go/issues/28804#issuecomment-438838144
|
||||||
|
token := windows.Token(0)
|
||||||
|
|
||||||
|
isAdmin, err := token.IsMember(sid)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "warning: Unexpected Windows Permission ID Error: %s\n", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAdmin || token.IsElevated()
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.rootprojects.org/root/go-serviceman/installer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var GitRev = "000000000"
|
||||||
|
var GitVersion = "v0.0.0"
|
||||||
|
var GitTimestamp = time.Now().Format(time.RFC3339)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
conf := &installer.Config{
|
||||||
|
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
|
||||||
|
conf.Args = strings.Join(conf.Argv, " ")
|
||||||
|
|
||||||
|
forUser := false
|
||||||
|
forSystem := false
|
||||||
|
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.ReverseDNS, "rdns", "", "a plist-friendly Reverse DNS name for launchctl (ex: com.example.foo-app)")
|
||||||
|
flag.BoolVar(&forSystem, "system", false, "attempt to install system service as an unprivileged/unelevated user")
|
||||||
|
flag.BoolVar(&forUser, "user", false, "install user space / user mode service even when admin/root/sudo/elevated")
|
||||||
|
flag.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.Parse()
|
||||||
|
args = flag.Args()
|
||||||
|
|
||||||
|
if forUser && forSystem {
|
||||||
|
fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if forUser {
|
||||||
|
conf.System = false
|
||||||
|
} else if forSystem {
|
||||||
|
conf.System = true
|
||||||
|
} else {
|
||||||
|
conf.System = installer.IsPrivileged()
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(args)
|
||||||
|
if 0 == n {
|
||||||
|
fmt.Println("Usage: serviceman install ./foo-app -- --foo-arg")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
execpath, err := installer.WhereIs(args[0])
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: '%s' could not be found.", args[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
args[0] = execpath
|
||||||
|
conf.Exec = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
|
||||||
|
if n >= 2 {
|
||||||
|
conf.Interpreter = conf.Exec
|
||||||
|
conf.Exec = args[0]
|
||||||
|
conf.Argv = append(args[1:], conf.Argv...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" == conf.Name {
|
||||||
|
ext := filepath.Ext(conf.Exec)
|
||||||
|
base := filepath.Base(conf.Exec[:len(conf.Exec)-len(ext)])
|
||||||
|
conf.Name = strings.ToLower(base)
|
||||||
|
}
|
||||||
|
if "" == conf.Title {
|
||||||
|
conf.Title = conf.Name
|
||||||
|
}
|
||||||
|
if "" == conf.ReverseDNS {
|
||||||
|
conf.ReverseDNS = "com.example." + conf.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n%#v\n\n", conf)
|
||||||
|
|
||||||
|
err = installer.Install(conf)
|
||||||
|
if nil != err {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// +build tools
|
||||||
|
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "git.rootprojects.org/root/go-gitver"
|
||||||
|
_ "github.com/UnnoTed/fileb0x"
|
||||||
|
)
|
Loading…
Reference in New Issue