peilaus alkaen https://github.com/therootcompany/serviceman.git
AJ ONeal
5 vuotta sitten
19 muutettua tiedostoa jossa 1054 lisäystä ja 1 poistoa
@ -1,3 +1,17 @@ |
|||
# 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" |
|||
) |
Ladataan…
Reference in new issue