2018-01-17 11:28:43 +01:00
|
|
|
// 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
|
|
|
|
|
|
|
|
import (
|
2019-11-17 22:54:56 +01:00
|
|
|
"bytes"
|
2018-01-17 11:28:43 +01:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"os/signal"
|
2020-06-08 19:22:36 +02:00
|
|
|
"path/filepath"
|
2018-08-23 00:23:59 +02:00
|
|
|
"regexp"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2018-01-17 11:28:43 +01:00
|
|
|
"syscall"
|
|
|
|
"text/template"
|
|
|
|
)
|
|
|
|
|
|
|
|
func isSystemd() bool {
|
|
|
|
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
|
|
|
return true
|
|
|
|
}
|
2019-11-17 22:54:56 +01:00
|
|
|
if _, err := os.Stat("/proc/1/comm"); err == nil {
|
|
|
|
filerc, err := os.Open("/proc/1/comm")
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
defer filerc.Close()
|
|
|
|
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
buf.ReadFrom(filerc)
|
|
|
|
contents := buf.String()
|
|
|
|
|
|
|
|
if strings.Trim(contents, " \r\n") == "systemd" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2018-01-17 11:28:43 +01:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
type systemd struct {
|
2018-10-02 18:06:43 +02:00
|
|
|
i Interface
|
|
|
|
platform string
|
2018-01-17 11:28:43 +01:00
|
|
|
*Config
|
|
|
|
}
|
|
|
|
|
2018-10-02 18:06:43 +02:00
|
|
|
func newSystemdService(i Interface, platform string, c *Config) (Service, error) {
|
2018-01-17 11:28:43 +01:00
|
|
|
s := &systemd{
|
2018-10-02 18:06:43 +02:00
|
|
|
i: i,
|
|
|
|
platform: platform,
|
|
|
|
Config: c,
|
2018-01-17 11:28:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *systemd) String() string {
|
|
|
|
if len(s.DisplayName) > 0 {
|
|
|
|
return s.DisplayName
|
|
|
|
}
|
|
|
|
return s.Name
|
|
|
|
}
|
|
|
|
|
2018-10-02 18:06:43 +02:00
|
|
|
func (s *systemd) Platform() string {
|
|
|
|
return s.platform
|
|
|
|
}
|
|
|
|
|
2018-01-17 11:28:43 +01:00
|
|
|
func (s *systemd) configPath() (cp string, err error) {
|
2020-11-18 10:19:58 +01:00
|
|
|
if !s.isUserService() {
|
2022-01-11 16:05:08 +01:00
|
|
|
cp = "/etc/systemd/system/" + s.unitName()
|
2020-06-08 19:22:36 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
if err != nil {
|
2018-01-17 11:28:43 +01:00
|
|
|
return
|
|
|
|
}
|
2020-06-08 19:22:36 +02:00
|
|
|
systemdUserDir := filepath.Join(homeDir, ".config/systemd/user")
|
|
|
|
err = os.MkdirAll(systemdUserDir, os.ModePerm)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
2022-01-11 16:05:08 +01:00
|
|
|
cp = filepath.Join(systemdUserDir, s.unitName())
|
2018-01-17 11:28:43 +01:00
|
|
|
return
|
|
|
|
}
|
2018-08-23 00:23:59 +02:00
|
|
|
|
2022-01-11 16:05:08 +01:00
|
|
|
func (s *systemd) unitName() string {
|
|
|
|
return s.Config.Name + ".service"
|
|
|
|
}
|
|
|
|
|
2018-08-23 00:23:59 +02:00
|
|
|
func (s *systemd) getSystemdVersion() int64 {
|
2022-10-15 10:37:07 +02:00
|
|
|
_, out, err := s.runWithOutput("systemctl", "--version")
|
2018-08-23 00:23:59 +02:00
|
|
|
if err != nil {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
re := regexp.MustCompile(`systemd ([0-9]+)`)
|
|
|
|
matches := re.FindStringSubmatch(out)
|
|
|
|
if len(matches) != 2 {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
v, err := strconv.ParseInt(matches[1], 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *systemd) hasOutputFileSupport() bool {
|
|
|
|
defaultValue := true
|
|
|
|
version := s.getSystemdVersion()
|
|
|
|
if version == -1 {
|
|
|
|
return defaultValue
|
|
|
|
}
|
|
|
|
|
|
|
|
if version < 236 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return defaultValue
|
|
|
|
}
|
|
|
|
|
2018-01-17 11:28:43 +01:00
|
|
|
func (s *systemd) template() *template.Template {
|
2018-08-23 00:23:59 +02:00
|
|
|
customScript := s.Option.string(optionSystemdScript, "")
|
|
|
|
|
|
|
|
if customScript != "" {
|
|
|
|
return template.Must(template.New("").Funcs(tf).Parse(customScript))
|
|
|
|
}
|
2022-10-15 10:37:07 +02:00
|
|
|
return template.Must(template.New("").Funcs(tf).Parse(systemdScript))
|
2018-01-17 11:28:43 +01:00
|
|
|
}
|
|
|
|
|
2020-11-18 10:19:58 +01:00
|
|
|
func (s *systemd) isUserService() bool {
|
|
|
|
return s.Option.bool(optionUserService, optionUserServiceDefault)
|
|
|
|
}
|
|
|
|
|
2018-01-17 11:28:43 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-06-08 19:22:36 +02:00
|
|
|
f, err := os.OpenFile(confPath, os.O_WRONLY|os.O_CREATE, 0644)
|
2018-01-17 11:28:43 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
path, err := s.execPath()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var to = &struct {
|
|
|
|
*Config
|
2018-08-23 00:23:59 +02:00
|
|
|
Path string
|
|
|
|
HasOutputFileSupport bool
|
|
|
|
ReloadSignal string
|
|
|
|
PIDFile string
|
2020-06-08 19:22:36 +02:00
|
|
|
LimitNOFILE int
|
2019-11-17 22:54:56 +01:00
|
|
|
Restart string
|
|
|
|
SuccessExitStatus string
|
2018-08-23 00:23:59 +02:00
|
|
|
LogOutput bool
|
2022-10-15 10:37:07 +02:00
|
|
|
LogDirectory string
|
2018-01-17 11:28:43 +01:00
|
|
|
}{
|
|
|
|
s.Config,
|
|
|
|
path,
|
2018-08-23 00:23:59 +02:00
|
|
|
s.hasOutputFileSupport(),
|
2018-01-17 11:28:43 +01:00
|
|
|
s.Option.string(optionReloadSignal, ""),
|
|
|
|
s.Option.string(optionPIDFile, ""),
|
2020-06-08 19:22:36 +02:00
|
|
|
s.Option.int(optionLimitNOFILE, optionLimitNOFILEDefault),
|
2019-11-17 22:54:56 +01:00
|
|
|
s.Option.string(optionRestart, "always"),
|
|
|
|
s.Option.string(optionSuccessExitStatus, ""),
|
2018-08-23 00:23:59 +02:00
|
|
|
s.Option.bool(optionLogOutput, optionLogOutputDefault),
|
2022-10-15 10:37:07 +02:00
|
|
|
s.Option.string(optionLogDirectory, defaultLogDirectory),
|
2018-01-17 11:28:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
err = s.template().Execute(f, to)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-11-18 10:19:58 +01:00
|
|
|
err = s.runAction("enable")
|
2018-01-17 11:28:43 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-06-08 19:22:36 +02:00
|
|
|
|
2020-11-18 10:19:58 +01:00
|
|
|
return s.run("daemon-reload")
|
2018-01-17 11:28:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *systemd) Uninstall() error {
|
2020-11-18 10:19:58 +01:00
|
|
|
err := s.runAction("disable")
|
2018-01-17 11:28:43 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2018-08-23 00:23:59 +02:00
|
|
|
func (s *systemd) Status() (Status, error) {
|
2022-10-15 10:37:07 +02:00
|
|
|
exitCode, out, err := s.runWithOutput("systemctl", "is-active", s.unitName())
|
2018-08-23 00:23:59 +02:00
|
|
|
if exitCode == 0 && err != nil {
|
|
|
|
return StatusUnknown, err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case strings.HasPrefix(out, "active"):
|
|
|
|
return StatusRunning, nil
|
|
|
|
case strings.HasPrefix(out, "inactive"):
|
2020-11-18 10:19:58 +01:00
|
|
|
// inactive can also mean its not installed, check unit files
|
2022-10-15 10:37:07 +02:00
|
|
|
exitCode, out, err := s.runWithOutput("systemctl", "list-unit-files", "-t", "service", s.unitName())
|
2020-11-18 10:19:58 +01:00
|
|
|
if exitCode == 0 && err != nil {
|
|
|
|
return StatusUnknown, err
|
|
|
|
}
|
|
|
|
if strings.Contains(out, s.Name) {
|
|
|
|
// unit file exists, installed but not running
|
|
|
|
return StatusStopped, nil
|
|
|
|
}
|
|
|
|
// no unit file
|
|
|
|
return StatusUnknown, ErrNotInstalled
|
2020-06-08 19:22:36 +02:00
|
|
|
case strings.HasPrefix(out, "activating"):
|
|
|
|
return StatusRunning, nil
|
2018-08-23 00:23:59 +02:00
|
|
|
case strings.HasPrefix(out, "failed"):
|
|
|
|
return StatusUnknown, errors.New("service in failed state")
|
|
|
|
default:
|
|
|
|
return StatusUnknown, ErrNotInstalled
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-17 11:28:43 +01:00
|
|
|
func (s *systemd) Start() error {
|
2020-11-18 10:19:58 +01:00
|
|
|
return s.runAction("start")
|
2018-01-17 11:28:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *systemd) Stop() error {
|
2020-11-18 10:19:58 +01:00
|
|
|
return s.runAction("stop")
|
2018-01-17 11:28:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *systemd) Restart() error {
|
2020-11-18 10:19:58 +01:00
|
|
|
return s.runAction("restart")
|
|
|
|
}
|
|
|
|
|
2022-10-15 10:37:07 +02:00
|
|
|
func (s *systemd) runWithOutput(command string, arguments ...string) (int, string, error) {
|
|
|
|
if s.isUserService() {
|
|
|
|
arguments = append(arguments, "--user")
|
|
|
|
}
|
|
|
|
return runWithOutput(command, arguments...)
|
|
|
|
}
|
|
|
|
|
2020-11-18 10:19:58 +01:00
|
|
|
func (s *systemd) run(action string, args ...string) error {
|
|
|
|
if s.isUserService() {
|
|
|
|
return run("systemctl", append([]string{action, "--user"}, args...)...)
|
|
|
|
}
|
|
|
|
return run("systemctl", append([]string{action}, args...)...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *systemd) runAction(action string) error {
|
2022-01-11 16:05:08 +01:00
|
|
|
return s.run(action, s.unitName())
|
2018-01-17 11:28:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const systemdScript = `[Unit]
|
|
|
|
Description={{.Description}}
|
|
|
|
ConditionFileIsExecutable={{.Path|cmdEscape}}
|
2019-11-17 22:54:56 +01:00
|
|
|
{{range $i, $dep := .Dependencies}}
|
|
|
|
{{$dep}} {{end}}
|
2018-01-17 11:28:43 +01:00
|
|
|
|
|
|
|
[Service]
|
|
|
|
StartLimitInterval=5
|
|
|
|
StartLimitBurst=10
|
|
|
|
ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}}
|
|
|
|
{{if .ChRoot}}RootDirectory={{.ChRoot|cmd}}{{end}}
|
|
|
|
{{if .WorkingDirectory}}WorkingDirectory={{.WorkingDirectory|cmdEscape}}{{end}}
|
|
|
|
{{if .UserName}}User={{.UserName}}{{end}}
|
|
|
|
{{if .ReloadSignal}}ExecReload=/bin/kill -{{.ReloadSignal}} "$MAINPID"{{end}}
|
|
|
|
{{if .PIDFile}}PIDFile={{.PIDFile|cmd}}{{end}}
|
2018-08-23 00:23:59 +02:00
|
|
|
{{if and .LogOutput .HasOutputFileSupport -}}
|
2022-10-15 10:37:07 +02:00
|
|
|
StandardOutput=file:{{.LogDirectory}}/{{.Name}}.out
|
|
|
|
StandardError=file:{{.LogDirectory}}/{{.Name}}.err
|
2018-08-23 00:23:59 +02:00
|
|
|
{{- end}}
|
2020-06-08 19:22:36 +02:00
|
|
|
{{if gt .LimitNOFILE -1 }}LimitNOFILE={{.LimitNOFILE}}{{end}}
|
2019-11-17 22:54:56 +01:00
|
|
|
{{if .Restart}}Restart={{.Restart}}{{end}}
|
|
|
|
{{if .SuccessExitStatus}}SuccessExitStatus={{.SuccessExitStatus}}{{end}}
|
2018-01-17 11:28:43 +01:00
|
|
|
RestartSec=120
|
|
|
|
EnvironmentFile=-/etc/sysconfig/{{.Name}}
|
|
|
|
|
2022-10-15 10:37:07 +02:00
|
|
|
{{range $k, $v := .EnvVars -}}
|
|
|
|
Environment={{$k}}={{$v}}
|
|
|
|
{{end -}}
|
|
|
|
|
2018-01-17 11:28:43 +01:00
|
|
|
[Install]
|
|
|
|
WantedBy=multi-user.target
|
|
|
|
`
|