diff --git a/manager/install_darwin.go b/manager/install_darwin.go index a2ea38e..4cc5c4c 100644 --- a/manager/install_darwin.go +++ b/manager/install_darwin.go @@ -6,13 +6,72 @@ import ( "io/ioutil" "os" "path/filepath" - "strings" "text/template" "git.rootprojects.org/root/go-serviceman/manager/static" "git.rootprojects.org/root/go-serviceman/service" ) +const ( + srvExt = ".plist" + srvSysPath = "/Library/LaunchDaemons" + srvUserPath = "Library/LaunchAgents" +) + +var srvLen int + +func init() { + srvLen = len(srvExt) +} + +func start(system bool, home string, name string) error { + sys, user, err := getMatchingSrvs(home, name) + if nil != err { + return err + } + + var service string + if system { + service, err = getOneSysSrv(sys, user, name) + if nil != err { + return err + } + service = filepath.Join(srvSysPath, service) + } else { + service, err = getOneUserSrv(home, sys, user, name) + if nil != err { + return err + } + service = filepath.Join(home, srvUserPath, service) + } + + cmds := []Runnable{ + Runnable{ + Exec: "launchctl", + Args: []string{"unload", "-w", service}, + Must: false, + }, + Runnable{ + Exec: "launchctl", + Args: []string{"load", "-w", service}, + Must: true, + }, + } + + fmt.Println() + for i := range cmds { + exe := cmds[i] + fmt.Println(exe.String()) + err := exe.Run() + if nil != err { + return err + } + } + fmt.Println() + + return nil +} + func install(c *service.Service) error { // Darwin-specific config options if c.PrivilegedPorts { @@ -20,9 +79,9 @@ func install(c *service.Service) error { return fmt.Errorf("You must use root-owned LaunchDaemons (not user-owned LaunchAgents) to use priveleged ports on OS X") } } - plistDir := "/Library/LaunchDaemons/" + plistDir := srvSysPath if !c.System { - plistDir = filepath.Join(c.Home, "Library/LaunchAgents") + plistDir = filepath.Join(c.Home, srvUserPath) } // Check paths first @@ -56,16 +115,32 @@ func install(c *service.Service) error { return fmt.Errorf("ioutil.WriteFile error: %v", err) } - fmt.Printf("Installed. To start '%s' run the following:\n", c.Name) - // TODO template config file - if "" != c.Home { - plistPath = strings.Replace(plistPath, c.Home, "~", 1) - } - sudo := "" - if c.System { - sudo = "sudo " + + // TODO --no-start + err = start(c.System, c.Home, c.ReverseDNS) + if nil != err { + fmt.Printf("If things don't go well you should be able to get additional logging from launchctl:\n") + fmt.Printf("\tsudo launchctl log level debug\n") + fmt.Printf("\ttail -f /var/log/system.log\n") + return err } - fmt.Printf("\t%slaunchctl load -w %s\n", sudo, plistPath) + fmt.Printf("Added and started '%s' as a launchctl service.\n", c.Name) return nil + + /* + fmt.Printf("Installed. To start '%s' run the following:\n", c.Name) + // TODO template config file + if "" != c.Home { + plistPath = strings.Replace(plistPath, c.Home, "~", 1) + } + sudo := "" + if c.System { + sudo = "sudo " + } + fmt.Printf("\t%slaunchctl load -w %s\n", sudo, plistPath) + + + return nil + */ } diff --git a/manager/install_linux.go b/manager/install_linux.go index 5d29443..abcaf41 100644 --- a/manager/install_linux.go +++ b/manager/install_linux.go @@ -12,6 +12,17 @@ import ( "git.rootprojects.org/root/go-serviceman/service" ) +var ( + srvLen int + srvExt = ".service" + srvSysPath = "/etc/systemd/system" + srvUserPath = ".local/share/systemd/user" +) + +func init() { + srvLen = len(srvExt) +} + func install(c *service.Service) error { // Linux-specific config options if c.System { @@ -22,7 +33,7 @@ func install(c *service.Service) error { if "" == c.Group { c.Group = c.User } - serviceDir := "/etc/systemd/system/" + serviceDir := srvSysPath // Check paths first serviceName := c.Name + ".service" @@ -31,7 +42,7 @@ func install(c *service.Service) error { // * ~/.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") + serviceDir = filepath.Join(c.Home, srvUserPath) err := os.MkdirAll(filepath.Dir(serviceDir), 0755) if nil != err { return err diff --git a/manager/install_windows.go b/manager/install_windows.go index 01c2578..2bc3bbb 100644 --- a/manager/install_windows.go +++ b/manager/install_windows.go @@ -14,6 +14,17 @@ import ( "golang.org/x/sys/windows/registry" ) +var ( + srvLen int + srvExt = ".json" + srvSysPath = "/opt/serviceman/etc" + srvUserPath = ".local/opt/serviceman/etc" +) + +func init() { + srvLen = len(srvExt) +} + // TODO nab some goodness from https://github.com/takama/daemon // TODO system service requires elevated privileges diff --git a/manager/start.go b/manager/start.go new file mode 100644 index 0000000..5cd7eaa --- /dev/null +++ b/manager/start.go @@ -0,0 +1,205 @@ +package manager + +import ( + "fmt" + "io/ioutil" + "os/exec" + "path/filepath" + "strings" +) + +// Runnable defines a command to run, along with its arguments, +// and whether or not failing to exit successfully matters. +// It also defines whether certains words must exist (or not exist) +// in its output, apart from existing successfully, to determine +// whether or not it was actually successful. +type Runnable struct { + Exec string + Args []string + Must bool + Keywords []string + Badwords []string +} + +func (x Runnable) Run() error { + cmd := exec.Command(x.Exec, x.Args...) + out, err := cmd.CombinedOutput() + if !x.Must { + return nil + } + + good := true + str := string(out) + for j := range x.Keywords { + if !strings.Contains(str, x.Keywords[j]) { + good = false + break + } + } + if good && 0 != len(x.Badwords) { + for j := range x.Badwords { + if "" != x.Badwords[j] && !strings.Contains(str, x.Badwords[j]) { + good = false + break + } + } + } + if nil != err { + return fmt.Errorf("Failed to run %s %s\n%s\n", x.Exec, strings.Join(x.Args, " "), str) + } + + return nil +} + +func (x Runnable) String() string { + var comment string + var must = "true" + + if x.Must { + must = "exit" + if len(x.Keywords) > 0 { + comment += "# output must match all of:\n" + comment += "\t" + strings.Join(x.Keywords, "#\t \n") + "\n" + } + if len(x.Badwords) > 0 { + comment += "# output must not match any of:\n" + comment += "\t" + strings.Join(x.Keywords, "#\t \n") + "\n" + } + } + + return strings.TrimSpace(fmt.Sprintf( + "%s %s || %s\n%s", + x.Exec, + strings.Join(x.Args, " "), + must, + comment, + )) +} + +func getSrvs(dir string) ([]string, error) { + plists := []string{} + + infos, err := ioutil.ReadDir(dir) + if nil != err { + return nil, err + } + + for i := range infos { + x := infos[i] + fname := strings.ToLower(x.Name()) + if strings.HasSuffix(fname, srvExt) { + plists = append(plists, x.Name()) + } + } + + return plists, nil +} + +func getSystemSrvs() ([]string, error) { + return getSrvs(srvSysPath) +} + +func getUserSrvs(home string) ([]string, error) { + dir := filepath.Join(home, srvUserPath) + return getSrvs(dir) +} + +func filterMatchingSrvs(plists []string, name string) []string { + filtered := []string{} + + for i := range plists { + pname := plists[i] + lname := strings.ToLower(pname) + n := len(lname) + if strings.HasSuffix(lname[:n-srvLen], strings.ToLower(name)) { + filtered = append(filtered, pname) + } + } + + return filtered +} + +func getMatchingSrvs(home string, name string) ([]string, []string, error) { + sysPlists, err := getSystemSrvs() + if nil != err { + return nil, nil, err + } + + var userPlists []string + if "" != home { + userPlists, err = getUserSrvs(home) + if nil != err { + return nil, nil, err + } + } + + return filterMatchingSrvs(sysPlists, name), filterMatchingSrvs(userPlists, name), nil +} + +func getExactSrvMatch(srvs []string, name string) string { + for i := range srvs { + srv := srvs[i] + n := len(srv) + if srv[:n-srvLen] == strings.ToLower(name) { + return srv + } + } + + return "" +} + +func getOneSysSrv(sys []string, user []string, name string) (string, error) { + service := getExactSrvMatch(user, name) + if "" != service { + return service, nil + } + + var errstr string + // system service was wanted + n := len(sys) + switch { + case 0 == n: + errstr += fmt.Sprintf("Didn't find user service matching %q\n", name) + if 0 != len(user) { + errstr += fmt.Sprintf("Did you intend to run a user service instead?\n\t%s\n", strings.Join(user, "\n\t")) + } + case n > 1: + errstr += fmt.Sprintf("Found more than one matching service:\n\t%s\n", strings.Join(sys, "\n\t")) + default: + service = filepath.Join(srvSysPath, sys[0]) + } + + if "" != errstr { + return "", fmt.Errorf(errstr) + } + + return service, nil +} + +func getOneUserSrv(home string, sys []string, user []string, name string) (string, error) { + service := getExactSrvMatch(user, name) + if "" != service { + return service, nil + } + + var errstr string + // user service was wanted + n := len(user) + switch { + case 0 == n: + errstr += fmt.Sprintf("Didn't find user service matching %q\n", name) + if 0 != len(sys) { + errstr += fmt.Sprintf("Did you intend to run a system service instead?\n\t%s\n", strings.Join(sys, "\n\t")) + } + case n > 1: + errstr += fmt.Sprintf("Found more than one matching service:\n\t%s\n", strings.Join(user, "\n\t")) + default: + service = filepath.Join(home, srvUserPath, user[0]+srvExt) + } + + if "" != errstr { + return "", fmt.Errorf(errstr) + } + + return service, nil +} diff --git a/serviceman.go b/serviceman.go index cedd451..c38a2bc 100644 --- a/serviceman.go +++ b/serviceman.go @@ -134,7 +134,8 @@ func add() { fmt.Fprintf(os.Stderr, "Use '--user' to add service as an user service.\n") } - fmt.Printf("Once started, logs will be found at:\n\t%s\n", conf.Logdir) + fmt.Printf("If all went well the logs should have been created at:\n\t%s\n", conf.Logdir) + fmt.Println() } func run() {