1// Copyright 2015 Daniel Theophanes. 2// Use of this source code is governed by a zlib-style 3// license that can be found in the LICENSE file. 4 5package service 6 7import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "os" 12 "os/signal" 13 "path/filepath" 14 "regexp" 15 "strconv" 16 "strings" 17 "syscall" 18 "text/template" 19) 20 21func isSystemd() bool { 22 if _, err := os.Stat("/run/systemd/system"); err == nil { 23 return true 24 } 25 if _, err := os.Stat("/proc/1/comm"); err == nil { 26 filerc, err := os.Open("/proc/1/comm") 27 if err != nil { 28 return false 29 } 30 defer filerc.Close() 31 32 buf := new(bytes.Buffer) 33 buf.ReadFrom(filerc) 34 contents := buf.String() 35 36 if strings.Trim(contents, " \r\n") == "systemd" { 37 return true 38 } 39 } 40 return false 41} 42 43type systemd struct { 44 i Interface 45 platform string 46 *Config 47} 48 49func newSystemdService(i Interface, platform string, c *Config) (Service, error) { 50 s := &systemd{ 51 i: i, 52 platform: platform, 53 Config: c, 54 } 55 56 return s, nil 57} 58 59func (s *systemd) String() string { 60 if len(s.DisplayName) > 0 { 61 return s.DisplayName 62 } 63 return s.Name 64} 65 66func (s *systemd) Platform() string { 67 return s.platform 68} 69 70func (s *systemd) configPath() (cp string, err error) { 71 if !s.isUserService() { 72 cp = "/etc/systemd/system/" + s.Config.Name + ".service" 73 return 74 } 75 homeDir, err := os.UserHomeDir() 76 if err != nil { 77 return 78 } 79 systemdUserDir := filepath.Join(homeDir, ".config/systemd/user") 80 err = os.MkdirAll(systemdUserDir, os.ModePerm) 81 if err != nil { 82 return 83 } 84 cp = filepath.Join(systemdUserDir, s.Config.Name+".service") 85 return 86} 87 88func (s *systemd) getSystemdVersion() int64 { 89 _, out, err := runWithOutput("systemctl", "--version") 90 if err != nil { 91 return -1 92 } 93 94 re := regexp.MustCompile(`systemd ([0-9]+)`) 95 matches := re.FindStringSubmatch(out) 96 if len(matches) != 2 { 97 return -1 98 } 99 100 v, err := strconv.ParseInt(matches[1], 10, 64) 101 if err != nil { 102 return -1 103 } 104 105 return v 106} 107 108func (s *systemd) hasOutputFileSupport() bool { 109 defaultValue := true 110 version := s.getSystemdVersion() 111 if version == -1 { 112 return defaultValue 113 } 114 115 if version < 236 { 116 return false 117 } 118 119 return defaultValue 120} 121 122func (s *systemd) template() *template.Template { 123 customScript := s.Option.string(optionSystemdScript, "") 124 125 if customScript != "" { 126 return template.Must(template.New("").Funcs(tf).Parse(customScript)) 127 } else { 128 return template.Must(template.New("").Funcs(tf).Parse(systemdScript)) 129 } 130} 131 132func (s *systemd) isUserService() bool { 133 return s.Option.bool(optionUserService, optionUserServiceDefault) 134} 135 136func (s *systemd) Install() error { 137 confPath, err := s.configPath() 138 if err != nil { 139 return err 140 } 141 _, err = os.Stat(confPath) 142 if err == nil { 143 return fmt.Errorf("Init already exists: %s", confPath) 144 } 145 146 f, err := os.OpenFile(confPath, os.O_WRONLY|os.O_CREATE, 0644) 147 if err != nil { 148 return err 149 } 150 defer f.Close() 151 152 path, err := s.execPath() 153 if err != nil { 154 return err 155 } 156 157 var to = &struct { 158 *Config 159 Path string 160 HasOutputFileSupport bool 161 ReloadSignal string 162 PIDFile string 163 LimitNOFILE int 164 Restart string 165 SuccessExitStatus string 166 LogOutput bool 167 }{ 168 s.Config, 169 path, 170 s.hasOutputFileSupport(), 171 s.Option.string(optionReloadSignal, ""), 172 s.Option.string(optionPIDFile, ""), 173 s.Option.int(optionLimitNOFILE, optionLimitNOFILEDefault), 174 s.Option.string(optionRestart, "always"), 175 s.Option.string(optionSuccessExitStatus, ""), 176 s.Option.bool(optionLogOutput, optionLogOutputDefault), 177 } 178 179 err = s.template().Execute(f, to) 180 if err != nil { 181 return err 182 } 183 184 err = s.runAction("enable") 185 if err != nil { 186 return err 187 } 188 189 return s.run("daemon-reload") 190} 191 192func (s *systemd) Uninstall() error { 193 err := s.runAction("disable") 194 if err != nil { 195 return err 196 } 197 cp, err := s.configPath() 198 if err != nil { 199 return err 200 } 201 if err := os.Remove(cp); err != nil { 202 return err 203 } 204 return nil 205} 206 207func (s *systemd) Logger(errs chan<- error) (Logger, error) { 208 if system.Interactive() { 209 return ConsoleLogger, nil 210 } 211 return s.SystemLogger(errs) 212} 213func (s *systemd) SystemLogger(errs chan<- error) (Logger, error) { 214 return newSysLogger(s.Name, errs) 215} 216 217func (s *systemd) Run() (err error) { 218 err = s.i.Start(s) 219 if err != nil { 220 return err 221 } 222 223 s.Option.funcSingle(optionRunWait, func() { 224 var sigChan = make(chan os.Signal, 3) 225 signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt) 226 <-sigChan 227 })() 228 229 return s.i.Stop(s) 230} 231 232func (s *systemd) Status() (Status, error) { 233 exitCode, out, err := runWithOutput("systemctl", "is-active", s.Name) 234 if exitCode == 0 && err != nil { 235 return StatusUnknown, err 236 } 237 238 switch { 239 case strings.HasPrefix(out, "active"): 240 return StatusRunning, nil 241 case strings.HasPrefix(out, "inactive"): 242 // inactive can also mean its not installed, check unit files 243 exitCode, out, err := runWithOutput("systemctl", "list-unit-files", "-t", "service", s.Name) 244 if exitCode == 0 && err != nil { 245 return StatusUnknown, err 246 } 247 if strings.Contains(out, s.Name) { 248 // unit file exists, installed but not running 249 return StatusStopped, nil 250 } 251 // no unit file 252 return StatusUnknown, ErrNotInstalled 253 case strings.HasPrefix(out, "activating"): 254 return StatusRunning, nil 255 case strings.HasPrefix(out, "failed"): 256 return StatusUnknown, errors.New("service in failed state") 257 default: 258 return StatusUnknown, ErrNotInstalled 259 } 260} 261 262func (s *systemd) Start() error { 263 return s.runAction("start") 264} 265 266func (s *systemd) Stop() error { 267 return s.runAction("stop") 268} 269 270func (s *systemd) Restart() error { 271 return s.runAction("restart") 272} 273 274func (s *systemd) run(action string, args ...string) error { 275 if s.isUserService() { 276 return run("systemctl", append([]string{action, "--user"}, args...)...) 277 } 278 return run("systemctl", append([]string{action}, args...)...) 279} 280 281func (s *systemd) runAction(action string) error { 282 return s.run(action, s.Name+".service") 283} 284 285const systemdScript = `[Unit] 286Description={{.Description}} 287ConditionFileIsExecutable={{.Path|cmdEscape}} 288{{range $i, $dep := .Dependencies}} 289{{$dep}} {{end}} 290 291[Service] 292StartLimitInterval=5 293StartLimitBurst=10 294ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}} 295{{if .ChRoot}}RootDirectory={{.ChRoot|cmd}}{{end}} 296{{if .WorkingDirectory}}WorkingDirectory={{.WorkingDirectory|cmdEscape}}{{end}} 297{{if .UserName}}User={{.UserName}}{{end}} 298{{if .ReloadSignal}}ExecReload=/bin/kill -{{.ReloadSignal}} "$MAINPID"{{end}} 299{{if .PIDFile}}PIDFile={{.PIDFile|cmd}}{{end}} 300{{if and .LogOutput .HasOutputFileSupport -}} 301StandardOutput=file:/var/log/{{.Name}}.out 302StandardError=file:/var/log/{{.Name}}.err 303{{- end}} 304{{if gt .LimitNOFILE -1 }}LimitNOFILE={{.LimitNOFILE}}{{end}} 305{{if .Restart}}Restart={{.Restart}}{{end}} 306{{if .SuccessExitStatus}}SuccessExitStatus={{.SuccessExitStatus}}{{end}} 307RestartSec=120 308EnvironmentFile=-/etc/sysconfig/{{.Name}} 309 310[Install] 311WantedBy=multi-user.target 312` 313