Browse Source

basic functionality works

tags/v0.2.0
AJ ONeal 9 months ago
parent
commit
84f1dacbac
19 changed files with 1054 additions and 1 deletions
  1. +15
    -1
      README.md
  2. +10
    -0
      go.mod
  3. +52
    -0
      go.sum
  4. +24
    -0
      installer/b0x.toml
  5. +71
    -0
      installer/dist/Library/LaunchDaemons/_rdns_.plist.tmpl
  6. +87
    -0
      installer/dist/etc/systemd/system/_name_.service.tmpl
  7. +7
    -0
      installer/doc.go
  8. +23
    -0
      installer/filesystem.go
  9. +128
    -0
      installer/install.go
  10. +64
    -0
      installer/install_darwin.go
  11. +76
    -0
      installer/install_linux.go
  12. +17
    -0
      installer/install_notwindows.go
  13. +7
    -0
      installer/install_other.go
  14. +95
    -0
      installer/install_windows.go
  15. +207
    -0
      installer/static/ab0x.go
  16. +15
    -0
      installer/whoami.go
  17. +43
    -0
      installer/whoami_windows.go
  18. +105
    -0
      serviceman.go
  19. +8
    -0
      tools/tools.go

+ 15
- 1
README.md View File

@@ -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 ./
```

+ 10
- 0
go.mod View File

@@ -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
)

+ 52
- 0
go.sum View File

@@ -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=

+ 24
- 0
installer/b0x.toml View File

@@ -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 = ""

+ 71
- 0
installer/dist/Library/LaunchDaemons/_rdns_.plist.tmpl View File

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

+ 87
- 0
installer/dist/etc/systemd/system/_name_.service.tmpl View File

@@ -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 }}

+ 7
- 0
installer/doc.go View File

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

+ 23
- 0
installer/filesystem.go View File

@@ -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)
}

+ 128
- 0
installer/install.go View File

@@ -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)
}

+ 64
- 0
installer/install_darwin.go View File

@@ -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
}

+ 76
- 0
installer/install_linux.go View File

@@ -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
}

+ 17
- 0
installer/install_notwindows.go View File

@@ -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
}

+ 7
- 0
installer/install_other.go View File

@@ -0,0 +1,7 @@
// +build !windows,!linux,!darwin

package installer

func install(c *Config) error {
return nil, nil
}

+ 95
- 0
installer/install_windows.go View File

@@ -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
}

+ 207
- 0
installer/static/ab0x.go
File diff suppressed because it is too large
View File


+ 15
- 0
installer/whoami.go View File

@@ -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
}

+ 43
- 0
installer/whoami_windows.go View File

@@ -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()
}

+ 105
- 0
serviceman.go View File

@@ -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)
}
}

+ 8
- 0
tools/tools.go View File

@@ -0,0 +1,8 @@
// +build tools

package tools

import (
_ "git.rootprojects.org/root/go-gitver"
_ "github.com/UnnoTed/fileb0x"
)

Loading…
Cancel
Save