serviceman/service/service.go

147 lines
4.5 KiB
Go

package service
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// Service should describe the service well-enough for it to
// run on Mac, Linux, and Windows.
//
// &Service{
// // 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 Service struct {
Title string `json:"title,omitempty"`
Name string `json:"name"`
Desc string `json:"desc,omitempty"`
URL string `json:"url,omitempty"`
ReverseDNS string `json:"reverse_dns"` // i.e. com.example.foo-app
Interpreter string `json:"interpreter,omitempty"` // i.e. node, python
Exec string `json:"exec"`
Argv []string `json:"argv,omitempty"`
Workdir string `json:"workdir,omitempty"`
Envs map[string]string `json:"envs,omitempty"`
User string `json:"user,omitempty"`
Group string `json:"group,omitempty"`
Home string `json:"-"`
Local string `json:"-"`
Logdir string `json:"logdir"`
System bool `json:"system"`
Restart bool `json:"restart"`
Production bool `json:"production,omitempty"`
PrivilegedPorts bool `json:"privileged_ports,omitempty"`
MultiuserProtection bool `json:"multiuser_protection,omitempty"`
}
func (s *Service) NormalizeWithoutPath() {
if "" == s.Name {
ext := filepath.Ext(s.Exec)
base := filepath.Base(s.Exec[:len(s.Exec)-len(ext)])
s.Name = strings.ToLower(base)
}
if "" == s.Title {
s.Title = s.Name
}
if "" == s.ReverseDNS {
// technically should be something more like "com.example." + s.Name,
// but whatever
s.ReverseDNS = s.Name
}
if !s.System {
home, err := os.UserHomeDir()
if nil != err {
fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
os.Exit(4)
return
}
s.Home = home
s.Local = filepath.Join(home, ".local")
s.Logdir = filepath.Join(home, ".local", "share", s.Name, "var", "log")
} else {
s.Logdir = "/var/log/" + s.Name
}
}
func (s *Service) Normalize(force bool) {
s.NormalizeWithoutPath()
// Check to see if Exec exists
// /whatever => must exist exactly
// ./whatever => must exist in current or WorkDir(TODO)
// whatever => may also exist in {{ .Local }}/opt/{{ .Name }}/{{ .Exec }}
_, err := os.Stat(s.Exec)
if nil != err {
bad := true
if !strings.Contains(filepath.ToSlash(s.Exec), "/") {
optpath := filepath.Join(s.Local, "/opt", s.Name, s.Exec)
_, err := os.Stat(optpath)
if nil == err {
bad = false
//fmt.Fprintf(os.Stderr, "Using '%s' for '%s'\n", optpath, s.Exec)
s.Exec = optpath
}
}
if bad {
// TODO look for it in WorkDir?
fmt.Fprintf(os.Stderr, "Error: '%s' could not be found.\n", s.Exec)
if !force {
os.Exit(5)
return
}
execpath, err := filepath.Abs(s.Exec)
if nil == err {
s.Exec = execpath
}
fmt.Fprintf(os.Stderr, "Using '%s' anyway.\n", s.Exec)
}
} else {
execpath, err := filepath.Abs(s.Exec)
if nil != err {
fmt.Fprintf(os.Stderr, "Unrecoverable Error: %s", err)
os.Exit(4)
} else {
s.Exec = execpath
}
}
}