AJ ONeal
5 years ago
19 changed files with 1054 additions and 1 deletions
@ -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