diff --git a/service_linux.go b/service_linux.go index 7f74e01..7ab4b7d 100644 --- a/service_linux.go +++ b/service_linux.go @@ -8,157 +8,105 @@ import ( "errors" "fmt" "os" - "os/exec" - "os/signal" "strings" - "text/template" - "time" - - "github.com/kardianos/osext" ) -const ( - initSystemV = initFlavor(iota) - initUpstart - initSystemd -) +type newServiceFunc func(i Interface, c *Config) (Service, error) -func getFlavor() initFlavor { - flavor := initSystemV - if isSystemd() { - flavor = initSystemd - } else if isUpstart() { - flavor = initUpstart - } - return flavor +type linuxSystem struct { + interactive bool + selectedName string + selectedNew newServiceFunc } -func isUpstart() bool { - if _, err := os.Stat("/sbin/upstart-udev-bridge"); err == nil { - return true - } - return false -} - -func isSystemd() bool { - if _, err := os.Stat("/run/systemd/system"); err == nil { - return true - } - return false -} - -type linuxService struct { - i Interface - *Config - - interactive bool -} - -var flavor = getFlavor() - -type linuxSystem struct{} - func (ls linuxSystem) String() string { - return fmt.Sprintf("Linux %s", flavor.String()) + return fmt.Sprintf("Linux %s", ls.selectedName) } func (ls linuxSystem) Interactive() bool { - return interactive + return ls.interactive } -var system = linuxSystem{} +type systemChoice interface { + Name() string + Detect() bool + Interactive() bool + New(i Interface, c *Config) (Service, error) +} + +type linuxSystemChoice struct { + name string + detect func() bool + interactive func() bool + new func(i Interface, c *Config) (Service, error) +} + +func (sc linuxSystemChoice) Name() string { + return sc.name +} +func (sc linuxSystemChoice) Detect() bool { + return sc.detect() +} +func (sc linuxSystemChoice) Interactive() bool { + return sc.interactive() +} +func (sc linuxSystemChoice) New(i Interface, c *Config) (Service, error) { + return sc.new(i, c) +} + +var systemRegistry = []systemChoice{ + linuxSystemChoice{ + name: "systemd", + detect: isSystemd, + interactive: func() bool { + is, _ := isInteractive() + return is + }, + new: newSystemdService, + }, + linuxSystemChoice{ + name: "Upstart", + detect: isUpstart, + interactive: func() bool { + is, _ := isInteractive() + return is + }, + new: newUpstartService, + }, + linuxSystemChoice{ + name: "System-V", + detect: func() bool { return true }, + interactive: func() bool { + is, _ := isInteractive() + return is + }, + new: newSystemVService, + }, +} + +func newLinuxSystem() linuxSystem { + for _, choice := range systemRegistry { + if choice.Detect() == false { + continue + } + return linuxSystem{ + interactive: choice.Interactive(), + selectedName: choice.Name(), + selectedNew: choice.New, + } + } + return linuxSystem{} +} + +var system = newLinuxSystem() + +var errNoServiceSystemDetected = errors.New("No service system detected.") func newService(i Interface, c *Config) (Service, error) { - s := &linuxService{ - i: i, - Config: c, - } - var err error - s.interactive, err = isInteractive() - - return s, err -} - -func (s *linuxService) String() string { - if len(s.DisplayName) > 0 { - return s.DisplayName - } - return s.Name -} - -type initFlavor uint8 - -func (f initFlavor) String() string { - switch f { - case initSystemV: - return "System-V" - case initUpstart: - return "Upstart" - case initSystemd: - return "systemd" - default: - panic("Invalid flavor") - } -} - -// Systemd services should be supported, but are not currently. -var errNoUserServiceSystemd = errors.New("User services are not supported on systemd.") - -var errNoUserServiceSystemV = errors.New("User services are not supported on SystemV.") - -// Upstart has some support for user services in graphical sessions. -// Due to the mix of actual support for user services over versions, just don't bother. -// Upstart will be replaced by systemd in most cases anyway. -var errNoUserServiceUpstart = errors.New("User services are not supported on Upstart.") - -func (f initFlavor) ConfigPath(name string, c *Config) (cp string, err error) { - if c.UserService { - switch f { - case initSystemd: - err = errNoUserServiceSystemd - case initSystemV: - err = errNoUserServiceSystemV - case initUpstart: - err = errNoUserServiceUpstart - default: - panic("Invalid flavor") - } - return - } - switch f { - case initSystemd: - cp = "/etc/systemd/system/" + name + ".service" - case initSystemV: - cp = "/etc/init.d/" + name - case initUpstart: - cp = "/etc/init/" + name + ".conf" - default: - panic("Invalid flavor") - } - return -} - -func (f initFlavor) Template() *template.Template { - var templ string - switch f { - case initSystemd: - templ = systemdScript - case initSystemV: - templ = systemVScript - case initUpstart: - templ = upstartScript - } - return template.Must(template.New(f.String() + "Script").Funcs(tf).Parse(templ)) -} - -var interactive = false - -func init() { - var err error - interactive, err = isInteractive() - if err != nil { - panic(err) + if system.selectedNew == nil { + return nil, errNoServiceSystemDetected } + return system.selectedNew(i, c) } func isInteractive() (bool, error) { @@ -166,277 +114,8 @@ func isInteractive() (bool, error) { return os.Getppid() != 1, nil } -func (s *linuxService) Install() error { - confPath, err := flavor.ConfigPath(s.Name, s.Config) - if err != nil { - return err - } - _, err = os.Stat(confPath) - if err == nil { - return fmt.Errorf("Init already exists: %s", confPath) - } - - f, err := os.Create(confPath) - if err != nil { - return err - } - defer f.Close() - - path, err := osext.Executable() - if err != nil { - return err - } - - var to = &struct { - *Config - Path string - }{ - s.Config, - path, - } - - err = flavor.Template().Execute(f, to) - if err != nil { - return err - } - - if flavor == initSystemV { - if err = os.Chmod(confPath, 0755); err != nil { - return err - } - for _, i := range [...]string{"2", "3", "4", "5"} { - if err = os.Symlink(confPath, "/etc/rc"+i+".d/S50"+s.Name); err != nil { - continue - } - } - for _, i := range [...]string{"0", "1", "6"} { - if err = os.Symlink(confPath, "/etc/rc"+i+".d/K02"+s.Name); err != nil { - continue - } - } - } - - if flavor == initSystemd { - err = exec.Command("systemctl", "enable", s.Name+".service").Run() - if err != nil { - return err - } - return exec.Command("systemctl", "daemon-reload").Run() - } - - return nil -} - -func (s *linuxService) Uninstall() error { - if flavor == initSystemd { - exec.Command("systemctl", "disable", s.Name+".service").Run() - } - cp, err := flavor.ConfigPath(s.Name, s.Config) - if err != nil { - return err - } - if err := os.Remove(cp); err != nil { - return err - } - return nil -} - -func (s *linuxService) Logger(errs chan<- error) (Logger, error) { - if s.interactive { - return ConsoleLogger, nil - } - return s.SystemLogger(errs) -} -func (s *linuxService) SystemLogger(errs chan<- error) (Logger, error) { - return newSysLogger(s.Name, errs) -} - -func (s *linuxService) Run() (err error) { - err = s.i.Start(s) - if err != nil { - return err - } - - sigChan := make(chan os.Signal, 3) - - signal.Notify(sigChan, os.Interrupt, os.Kill) - - <-sigChan - - return s.i.Stop(s) -} - -func (s *linuxService) Start() error { - switch flavor { - case initSystemd: - return exec.Command("systemctl", "start", s.Name+".service").Run() - case initUpstart: - return exec.Command("initctl", "start", s.Name).Run() - default: - return exec.Command("service", s.Name, "start").Run() - } -} - -func (s *linuxService) Stop() error { - switch flavor { - case initSystemd: - return exec.Command("systemctl", "stop", s.Name+".service").Run() - case initUpstart: - return exec.Command("initctl", "stop", s.Name).Run() - default: - return exec.Command("service", s.Name, "stop").Run() - } -} - -func (s *linuxService) Restart() error { - err := s.Stop() - if err != nil { - return err - } - time.Sleep(50 * time.Millisecond) - return s.Start() -} - var tf = map[string]interface{}{ "cmd": func(s string) string { return `"` + strings.Replace(s, `"`, `\"`, -1) + `"` }, } - -const systemVScript = `#!/bin/sh -# For RedHat and cousins: -# chkconfig: - 99 01 -# description: {{.Description}} -# processname: {{.Path}} - -### BEGIN INIT INFO -# Provides: {{.Path}} -# Required-Start: -# Required-Stop: -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: {{.DisplayName}} -# Description: {{.Description}} -### END INIT INFO - -cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}" - -name=$(basename $0) -pid_file="/var/run/$name.pid" -stdout_log="/var/log/$name.log" -stderr_log="/var/log/$name.err" - -get_pid() { - cat "$pid_file" -} - -is_running() { - [ -f "$pid_file" ] && ps $(get_pid) > /dev/null 2>&1 -} - -case "$1" in - start) - if is_running; then - echo "Already started" - else - echo "Starting $name" - $cmd >> "$stdout_log" 2>> "$stderr_log" & - echo $! > "$pid_file" - if ! is_running; then - echo "Unable to start, see $stdout_log and $stderr_log" - exit 1 - fi - fi - ;; - stop) - if is_running; then - echo -n "Stopping $name.." - kill $(get_pid) - for i in {1..10} - do - if ! is_running; then - break - fi - echo -n "." - sleep 1 - done - echo - if is_running; then - echo "Not stopped; may still be shutting down or shutdown may have failed" - exit 1 - else - echo "Stopped" - if [ -f "$pid_file" ]; then - rm "$pid_file" - fi - fi - else - echo "Not running" - fi - ;; - restart) - $0 stop - if is_running; then - echo "Unable to stop, will not attempt to start" - exit 1 - fi - $0 start - ;; - status) - if is_running; then - echo "Running" - else - echo "Stopped" - exit 1 - fi - ;; - *) - echo "Usage: $0 {start|stop|restart|status}" - exit 1 - ;; -esac -exit 0` - -// The upstart script should stop with an INT or the Go runtime will terminate -// the program before the Stop handler can run. -const upstartScript = `# {{.Description}} - - {{if .DisplayName}}description "{{.DisplayName}}"{{end}} - -kill signal INT -start on filesystem or runlevel [2345] -stop on runlevel [!2345] - -#setuid username - -respawn -respawn limit 10 5 -umask 022 - -console none - -pre-start script - test -x {{.Path}} || { stop; exit 0; } -end script - -# Start -exec {{.Path}}{{range .Arguments}} {{.|cmd}}{{end}} -` - -const systemdScript = `[Unit] -Description={{.Description}} -ConditionFileIsExecutable={{.Path|cmd}} - -[Service] -StartLimitInterval=5 -StartLimitBurst=10 -ExecStart={{.Path|cmd}}{{range .Arguments}} {{.|cmd}}{{end}} -{{if .ChRoot}}RootDirectory={{.ChRoot|cmd}}{{end}} -{{if .WorkingDirectory}}WorkingDirectory={{.WorkingDirectory|cmd}}{{end}} -{{if .UserName}}User={{.UserName}}{{end}} -Restart=always -RestartSec=120 - -[Install] -WantedBy=multi-user.target -` diff --git a/service_systemd_linux.go b/service_systemd_linux.go new file mode 100644 index 0000000..56a7fad --- /dev/null +++ b/service_systemd_linux.go @@ -0,0 +1,175 @@ +// Copyright 2015 Daniel Theophanes. +// Use of this source code is governed by a zlib-style +// license that can be found in the LICENSE file.package service + +package service + +import ( + "errors" + "fmt" + "os" + "os/exec" + "os/signal" + "text/template" + "time" + + "github.com/kardianos/osext" +) + +func isSystemd() bool { + if _, err := os.Stat("/run/systemd/system"); err == nil { + return true + } + return false +} + +type systemd struct { + i Interface + *Config +} + +func newSystemdService(i Interface, c *Config) (Service, error) { + s := &systemd{ + i: i, + Config: c, + } + + return s, nil +} + +func (s *systemd) String() string { + if len(s.DisplayName) > 0 { + return s.DisplayName + } + return s.Name +} + +// Systemd services should be supported, but are not currently. +var errNoUserServiceSystemd = errors.New("User services are not supported on systemd.") + +func (s *systemd) configPath() (cp string, err error) { + if s.Config.UserService { + err = errNoUserServiceSystemd + return + } + cp = "/etc/systemd/system/" + s.Config.Name + ".service" + return +} +func (s *systemd) template() *template.Template { + return template.Must(template.New("").Funcs(tf).Parse(systemdScript)) +} + +func (s *systemd) Install() error { + confPath, err := s.configPath() + if err != nil { + return err + } + _, err = os.Stat(confPath) + if err == nil { + return fmt.Errorf("Init already exists: %s", confPath) + } + + f, err := os.Create(confPath) + if err != nil { + return err + } + defer f.Close() + + path, err := osext.Executable() + if err != nil { + return err + } + + var to = &struct { + *Config + Path string + }{ + s.Config, + path, + } + + err = s.template().Execute(f, to) + if err != nil { + return err + } + + err = exec.Command("systemctl", "enable", s.Name+".service").Run() + if err != nil { + return err + } + return exec.Command("systemctl", "daemon-reload").Run() + + return nil +} + +func (s *systemd) Uninstall() error { + exec.Command("systemctl", "disable", s.Name+".service").Run() + cp, err := s.configPath() + if err != nil { + return err + } + if err := os.Remove(cp); err != nil { + return err + } + return nil +} + +func (s *systemd) Logger(errs chan<- error) (Logger, error) { + if system.Interactive() { + return ConsoleLogger, nil + } + return s.SystemLogger(errs) +} +func (s *systemd) SystemLogger(errs chan<- error) (Logger, error) { + return newSysLogger(s.Name, errs) +} + +func (s *systemd) Run() (err error) { + err = s.i.Start(s) + if err != nil { + return err + } + + sigChan := make(chan os.Signal, 3) + + signal.Notify(sigChan, os.Interrupt, os.Kill) + + <-sigChan + + return s.i.Stop(s) +} + +func (s *systemd) Start() error { + return exec.Command("systemctl", "start", s.Name+".service").Run() +} + +func (s *systemd) Stop() error { + return exec.Command("systemctl", "stop", s.Name+".service").Run() +} + +func (s *systemd) Restart() error { + err := s.Stop() + if err != nil { + return err + } + time.Sleep(50 * time.Millisecond) + return s.Start() +} + +const systemdScript = `[Unit] +Description={{.Description}} +ConditionFileIsExecutable={{.Path|cmd}} + +[Service] +StartLimitInterval=5 +StartLimitBurst=10 +ExecStart={{.Path|cmd}}{{range .Arguments}} {{.|cmd}}{{end}} +{{if .ChRoot}}RootDirectory={{.ChRoot|cmd}}{{end}} +{{if .WorkingDirectory}}WorkingDirectory={{.WorkingDirectory|cmd}}{{end}} +{{if .UserName}}User={{.UserName}}{{end}} +Restart=always +RestartSec=120 + +[Install] +WantedBy=multi-user.target +` diff --git a/service_sysv_linux.go b/service_sysv_linux.go new file mode 100644 index 0000000..5872ec3 --- /dev/null +++ b/service_sysv_linux.go @@ -0,0 +1,251 @@ +// Copyright 2015 Daniel Theophanes. +// Use of this source code is governed by a zlib-style +// license that can be found in the LICENSE file.package service + +package service + +import ( + "errors" + "fmt" + "os" + "os/exec" + "os/signal" + "text/template" + "time" + + "github.com/kardianos/osext" +) + +type sysv struct { + i Interface + *Config +} + +func newSystemVService(i Interface, c *Config) (Service, error) { + s := &sysv{ + i: i, + Config: c, + } + + return s, nil +} + +func (s *sysv) String() string { + if len(s.DisplayName) > 0 { + return s.DisplayName + } + return s.Name +} + +var errNoUserServiceSystemV = errors.New("User services are not supported on SystemV.") + +func (s *sysv) configPath() (cp string, err error) { + if s.Config.UserService { + err = errNoUserServiceSystemV + return + } + cp = "/etc/init.d/" + s.Config.Name + return +} +func (s *sysv) template() *template.Template { + return template.Must(template.New("").Funcs(tf).Parse(sysvScript)) +} + +func (s *sysv) Install() error { + confPath, err := s.configPath() + if err != nil { + return err + } + _, err = os.Stat(confPath) + if err == nil { + return fmt.Errorf("Init already exists: %s", confPath) + } + + f, err := os.Create(confPath) + if err != nil { + return err + } + defer f.Close() + + path, err := osext.Executable() + if err != nil { + return err + } + + var to = &struct { + *Config + Path string + }{ + s.Config, + path, + } + + err = s.template().Execute(f, to) + if err != nil { + return err + } + + if err = os.Chmod(confPath, 0755); err != nil { + return err + } + for _, i := range [...]string{"2", "3", "4", "5"} { + if err = os.Symlink(confPath, "/etc/rc"+i+".d/S50"+s.Name); err != nil { + continue + } + } + for _, i := range [...]string{"0", "1", "6"} { + if err = os.Symlink(confPath, "/etc/rc"+i+".d/K02"+s.Name); err != nil { + continue + } + } + + return nil +} + +func (s *sysv) Uninstall() error { + cp, err := s.configPath() + if err != nil { + return err + } + if err := os.Remove(cp); err != nil { + return err + } + return nil +} + +func (s *sysv) Logger(errs chan<- error) (Logger, error) { + if system.Interactive() { + return ConsoleLogger, nil + } + return s.SystemLogger(errs) +} +func (s *sysv) SystemLogger(errs chan<- error) (Logger, error) { + return newSysLogger(s.Name, errs) +} + +func (s *sysv) Run() (err error) { + err = s.i.Start(s) + if err != nil { + return err + } + + sigChan := make(chan os.Signal, 3) + + signal.Notify(sigChan, os.Interrupt, os.Kill) + + <-sigChan + + return s.i.Stop(s) +} + +func (s *sysv) Start() error { + return exec.Command("service", s.Name, "start").Run() +} + +func (s *sysv) Stop() error { + return exec.Command("service", s.Name, "stop").Run() +} + +func (s *sysv) Restart() error { + err := s.Stop() + if err != nil { + return err + } + time.Sleep(50 * time.Millisecond) + return s.Start() +} + +const sysvScript = `#!/bin/sh +# For RedHat and cousins: +# chkconfig: - 99 01 +# description: {{.Description}} +# processname: {{.Path}} + +### BEGIN INIT INFO +# Provides: {{.Path}} +# Required-Start: +# Required-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: {{.DisplayName}} +# Description: {{.Description}} +### END INIT INFO + +cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}" + +name=$(basename $0) +pid_file="/var/run/$name.pid" +stdout_log="/var/log/$name.log" +stderr_log="/var/log/$name.err" + +get_pid() { + cat "$pid_file" +} + +is_running() { + [ -f "$pid_file" ] && ps $(get_pid) > /dev/null 2>&1 +} + +case "$1" in + start) + if is_running; then + echo "Already started" + else + echo "Starting $name" + $cmd >> "$stdout_log" 2>> "$stderr_log" & + echo $! > "$pid_file" + if ! is_running; then + echo "Unable to start, see $stdout_log and $stderr_log" + exit 1 + fi + fi + ;; + stop) + if is_running; then + echo -n "Stopping $name.." + kill $(get_pid) + for i in {1..10} + do + if ! is_running; then + break + fi + echo -n "." + sleep 1 + done + echo + if is_running; then + echo "Not stopped; may still be shutting down or shutdown may have failed" + exit 1 + else + echo "Stopped" + if [ -f "$pid_file" ]; then + rm "$pid_file" + fi + fi + else + echo "Not running" + fi + ;; + restart) + $0 stop + if is_running; then + echo "Unable to stop, will not attempt to start" + exit 1 + fi + $0 start + ;; + status) + if is_running; then + echo "Running" + else + echo "Stopped" + exit 1 + fi + ;; + *) + echo "Usage: $0 {start|stop|restart|status}" + exit 1 + ;; +esac +exit 0 +` diff --git a/service_upstart_linux.go b/service_upstart_linux.go new file mode 100644 index 0000000..e6ec161 --- /dev/null +++ b/service_upstart_linux.go @@ -0,0 +1,173 @@ +// Copyright 2015 Daniel Theophanes. +// Use of this source code is governed by a zlib-style +// license that can be found in the LICENSE file.package service + +package service + +import ( + "errors" + "fmt" + "os" + "os/exec" + "os/signal" + "text/template" + "time" + + "github.com/kardianos/osext" +) + +func isUpstart() bool { + if _, err := os.Stat("/sbin/upstart-udev-bridge"); err == nil { + return true + } + return false +} + +type upstart struct { + i Interface + *Config +} + +func newUpstartService(i Interface, c *Config) (Service, error) { + s := &upstart{ + i: i, + Config: c, + } + + return s, nil +} + +func (s *upstart) String() string { + if len(s.DisplayName) > 0 { + return s.DisplayName + } + return s.Name +} + +// Upstart has some support for user services in graphical sessions. +// Due to the mix of actual support for user services over versions, just don't bother. +// Upstart will be replaced by systemd in most cases anyway. +var errNoUserServiceUpstart = errors.New("User services are not supported on Upstart.") + +func (s *upstart) configPath() (cp string, err error) { + if s.Config.UserService { + err = errNoUserServiceUpstart + return + } + cp = "/etc/init/" + s.Config.Name + ".conf" + return +} +func (s *upstart) template() *template.Template { + return template.Must(template.New("").Funcs(tf).Parse(upstartScript)) +} + +func (s *upstart) Install() error { + confPath, err := s.configPath() + if err != nil { + return err + } + _, err = os.Stat(confPath) + if err == nil { + return fmt.Errorf("Init already exists: %s", confPath) + } + + f, err := os.Create(confPath) + if err != nil { + return err + } + defer f.Close() + + path, err := osext.Executable() + if err != nil { + return err + } + + var to = &struct { + *Config + Path string + }{ + s.Config, + path, + } + + return s.template().Execute(f, to) +} + +func (s *upstart) Uninstall() error { + cp, err := s.configPath() + if err != nil { + return err + } + if err := os.Remove(cp); err != nil { + return err + } + return nil +} + +func (s *upstart) Logger(errs chan<- error) (Logger, error) { + if system.Interactive() { + return ConsoleLogger, nil + } + return s.SystemLogger(errs) +} +func (s *upstart) SystemLogger(errs chan<- error) (Logger, error) { + return newSysLogger(s.Name, errs) +} + +func (s *upstart) Run() (err error) { + err = s.i.Start(s) + if err != nil { + return err + } + + sigChan := make(chan os.Signal, 3) + + signal.Notify(sigChan, os.Interrupt, os.Kill) + + <-sigChan + + return s.i.Stop(s) +} + +func (s *upstart) Start() error { + return exec.Command("initctl", "start", s.Name).Run() +} + +func (s *upstart) Stop() error { + return exec.Command("initctl", "stop", s.Name).Run() +} + +func (s *upstart) Restart() error { + err := s.Stop() + if err != nil { + return err + } + time.Sleep(50 * time.Millisecond) + return s.Start() +} + +// The upstart script should stop with an INT or the Go runtime will terminate +// the program before the Stop handler can run. +const upstartScript = `# {{.Description}} + + {{if .DisplayName}}description "{{.DisplayName}}"{{end}} + +kill signal INT +start on filesystem or runlevel [2345] +stop on runlevel [!2345] + +#setuid username + +respawn +respawn limit 10 5 +umask 022 + +console none + +pre-start script + test -x {{.Path}} || { stop; exit 0; } +end script + +# Start +exec {{.Path}}{{range .Arguments}} {{.|cmd}}{{end}} +`