diff --git a/linux_test/Makefile b/linux_test/Makefile index dd3d0cf1..f82f9ee7 100644 --- a/linux_test/Makefile +++ b/linux_test/Makefile @@ -1,8 +1,9 @@ -all: sysv systemd upstart clean +all: sysv systemd upstart openrc clean +# compile `go test` binary statically test: - @go test -c .. + @CGO_ENABLED=0 go test -installsuffix netgo -a -c .. clean: -rm service.test @@ -33,3 +34,12 @@ upstart: test @-docker rm $(shell docker ps -l -q) @-docker rmi -f service.test.upstart @-rm upstart/service.test + +openrc: test + @echo openrc + @cp service.test openrc/ + @docker build -q --tag="service.test.openrc" openrc + @-docker run service.test.openrc + @-docker rm $(shell docker ps -l -q) + @-docker rmi -f service.test.openrc + @-rm openrc/service.test \ No newline at end of file diff --git a/linux_test/openrc/Dockerfile b/linux_test/openrc/Dockerfile new file mode 100644 index 00000000..c333545a --- /dev/null +++ b/linux_test/openrc/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:latest +ADD service.test /tmp/ +CMD /tmp/service.test -test.v=true diff --git a/service.go b/service.go index 91a69944..a8d08ed8 100644 --- a/service.go +++ b/service.go @@ -93,6 +93,7 @@ const ( optionSysvScript = "SysvScript" optionUpstartScript = "UpstartScript" optionLaunchdConfig = "LaunchdConfig" + optionOpenRCScript = "OpenRCScript" ) // Status represents service status as an byte value diff --git a/service_linux.go b/service_linux.go index d0345466..49d3faee 100644 --- a/service_linux.go +++ b/service_linux.go @@ -53,6 +53,15 @@ func init() { }, new: newUpstartService, }, + linuxSystemService{ + name: "linux-openrc", + detect: isOpenRC, + interactive: func() bool { + is, _ := isInteractive() + return is + }, + new: newOpenRCService, + }, linuxSystemService{ name: "unix-systemv", detect: func() bool { return true }, diff --git a/service_openrc_linux.go b/service_openrc_linux.go new file mode 100644 index 00000000..dd339d55 --- /dev/null +++ b/service_openrc_linux.go @@ -0,0 +1,226 @@ +package service + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "os/signal" + "regexp" + "strings" + "syscall" + "text/template" + "time" +) + +func isOpenRC() bool { + if _, err := exec.LookPath("openrc-init"); err == nil { + return true + } + if _, err := os.Stat("/etc/inittab"); err == nil { + filerc, err := os.Open("/etc/inittab") + if err != nil { + return false + } + defer filerc.Close() + + buf := new(bytes.Buffer) + buf.ReadFrom(filerc) + contents := buf.String() + + re := regexp.MustCompile(`::sysinit:.*openrc.*sysinit`) + matches := re.FindStringSubmatch(contents) + if len(matches) > 0 { + return true + } + return false + } + return false +} + +type openrc struct { + i Interface + platform string + *Config +} + +func (s *openrc) String() string { + if len(s.DisplayName) > 0 { + return s.DisplayName + } + return s.Name +} + +func (s *openrc) Platform() string { + return s.platform +} + +func (s *openrc) template() *template.Template { + customScript := s.Option.string(optionOpenRCScript, "") + + if customScript != "" { + return template.Must(template.New("").Funcs(tf).Parse(customScript)) + } else { + return template.Must(template.New("").Funcs(tf).Parse(openRCScript)) + } +} + +func newOpenRCService(i Interface, platform string, c *Config) (Service, error) { + s := &openrc{ + i: i, + platform: platform, + Config: c, + } + return s, nil +} + +var errNoUserServiceOpenRC = errors.New("user services are not supported on OpenRC") + +func (s *openrc) configPath() (cp string, err error) { + if s.Option.bool(optionUserService, optionUserServiceDefault) { + err = errNoUserServiceOpenRC + return + } + cp = "/etc/init.d/" + s.Config.Name + return +} + +func (s *openrc) 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() + + err = os.Chmod(confPath, 0755) + if err != nil { + return err + } + + path, err := s.execPath() + 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 + } + // run rc-update + return s.runAction("add") +} + +func (s *openrc) Uninstall() error { + confPath, err := s.configPath() + if err != nil { + return err + } + if err := os.Remove(confPath); err != nil { + return err + } + return s.runAction("delete") +} + +func (s *openrc) Logger(errs chan<- error) (Logger, error) { + if system.Interactive() { + return ConsoleLogger, nil + } + return s.SystemLogger(errs) +} + +func (s *openrc) SystemLogger(errs chan<- error) (Logger, error) { + return newSysLogger(s.Name, errs) +} + +func (s *openrc) Run() (err error) { + err = s.i.Start(s) + if err != nil { + return err + } + + s.Option.funcSingle(optionRunWait, func() { + var sigChan = make(chan os.Signal, 3) + signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt) + <-sigChan + })() + + return s.i.Stop(s) +} + +func (s *openrc) Status() (Status, error) { + _, out, err := runWithOutput("rc-service", s.Name, "status") + if err != nil { + return StatusUnknown, err + } + + switch { + case strings.HasPrefix(out, "Running"): + return StatusRunning, nil + case strings.HasPrefix(out, "Stopped"): + return StatusStopped, nil + default: + return StatusUnknown, ErrNotInstalled + } +} + +func (s *openrc) Start() error { + return run("rc-service", s.Name, "start") +} + +func (s *openrc) Stop() error { + return run("rc-service", s.Name, "stop") +} + +func (s *openrc) Restart() error { + err := s.Stop() + if err != nil { + return err + } + time.Sleep(50 * time.Millisecond) + return s.Start() +} + +func (s *openrc) runAction(action string) error { + return s.run(action, s.Name) +} + +func (s *openrc) run(action string, args ...string) error { + return run("rc-update", append([]string{action}, args...)...) +} + +const openRCScript = `#!/sbin/openrc-run +supervisor=supervise-daemon +name="{{.DisplayName}}" +description="{{.Description}}" +command={{.Path|cmdEscape}} +{{- if .Arguments }} +command_args="{{range .Arguments}}{{.}} {{end}}" +{{- end }} +name=$(basename $(readlink -f $command)) +supervise_daemon_args="--stdout /var/log/${name}.log --stderr /var/log/${name}.err" + +{{- if .Dependencies }} +depend() { +{{- range $i, $dep := .Dependencies}} +{{"\t"}}{{$dep}}{{end}} +} +{{- end}} +`