434 lines
11 KiB
Go
434 lines
11 KiB
Go
//go:build openbsd
|
|
|
|
package home
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"text/template"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/AdguardTeam/golibs/log"
|
|
"github.com/kardianos/service"
|
|
)
|
|
|
|
// OpenBSD Service Implementation
|
|
//
|
|
// The file contains OpenBSD implementations for service.System and
|
|
// service.Service interfaces. It uses the default approach for RunCom-based
|
|
// services systems, e.g. rc.d script. It's written as if it was in a separate
|
|
// package and has only one internal dependency.
|
|
//
|
|
// TODO(e.burkov): Perhaps, file a PR to github.com/kardianos/service.
|
|
|
|
// sysVersion is the version of local service.System interface implementation.
|
|
const sysVersion = "openbsd-runcom"
|
|
|
|
// chooseSystem checks the current system detected and substitutes it with local
|
|
// implementation if needed.
|
|
func chooseSystem() {
|
|
service.ChooseSystem(openbsdSystem{})
|
|
}
|
|
|
|
// openbsdSystem is the service.System to be used on the OpenBSD.
|
|
type openbsdSystem struct{}
|
|
|
|
// String implements service.System interface for openbsdSystem.
|
|
func (openbsdSystem) String() string {
|
|
return sysVersion
|
|
}
|
|
|
|
// Detect implements service.System interface for openbsdSystem.
|
|
func (openbsdSystem) Detect() (ok bool) {
|
|
return true
|
|
}
|
|
|
|
// Interactive implements service.System interface for openbsdSystem.
|
|
func (openbsdSystem) Interactive() (ok bool) {
|
|
return os.Getppid() != 1
|
|
}
|
|
|
|
// New implements service.System interface for openbsdSystem.
|
|
func (openbsdSystem) New(i service.Interface, c *service.Config) (s service.Service, err error) {
|
|
return &openbsdRunComService{
|
|
i: i,
|
|
cfg: c,
|
|
}, nil
|
|
}
|
|
|
|
// openbsdRunComService is the RunCom-based service.Service to be used on the
|
|
// OpenBSD.
|
|
type openbsdRunComService struct {
|
|
i service.Interface
|
|
cfg *service.Config
|
|
}
|
|
|
|
// Platform implements service.Service interface for *openbsdRunComService.
|
|
func (*openbsdRunComService) Platform() (p string) {
|
|
return "openbsd"
|
|
}
|
|
|
|
// String implements service.Service interface for *openbsdRunComService.
|
|
func (s *openbsdRunComService) String() string {
|
|
return cmp.Or(s.cfg.DisplayName, s.cfg.Name)
|
|
}
|
|
|
|
// getBool returns the value of the given name from kv, assuming the value is a
|
|
// boolean. If the value isn't found or is not of the type, the defaultValue is
|
|
// returned.
|
|
func getBool(kv service.KeyValue, name string, defaultValue bool) (val bool) {
|
|
var ok bool
|
|
if val, ok = kv[name].(bool); ok {
|
|
return val
|
|
}
|
|
|
|
return defaultValue
|
|
}
|
|
|
|
// getString returns the value of the given name from kv, assuming the value is
|
|
// a string. If the value isn't found or is not of the type, the defaultValue
|
|
// is returned.
|
|
func getString(kv service.KeyValue, name, defaultValue string) (val string) {
|
|
var ok bool
|
|
if val, ok = kv[name].(string); ok {
|
|
return val
|
|
}
|
|
|
|
return defaultValue
|
|
}
|
|
|
|
// getFuncNiladic returns the value of the given name from kv, assuming the
|
|
// value is a func(). If the value isn't found or is not of the type, the
|
|
// defaultValue is returned.
|
|
func getFuncNiladic(kv service.KeyValue, name string, defaultValue func()) (val func()) {
|
|
var ok bool
|
|
if val, ok = kv[name].(func()); ok {
|
|
return val
|
|
}
|
|
|
|
return defaultValue
|
|
}
|
|
|
|
const (
|
|
// optionUserService is the UserService option name.
|
|
optionUserService = "UserService"
|
|
|
|
// optionUserServiceDefault is the UserService option default value.
|
|
optionUserServiceDefault = false
|
|
|
|
// errNoUserServiceRunCom is returned when the service uses some custom
|
|
// path to script.
|
|
errNoUserServiceRunCom errors.Error = "user services are not supported on " + sysVersion
|
|
)
|
|
|
|
// scriptPath returns the absolute path to the script. It's commonly used to
|
|
// send commands to the service.
|
|
func (s *openbsdRunComService) scriptPath() (cp string, err error) {
|
|
if getBool(s.cfg.Option, optionUserService, optionUserServiceDefault) {
|
|
return "", errNoUserServiceRunCom
|
|
}
|
|
|
|
const scriptPathPref = "/etc/rc.d"
|
|
|
|
return filepath.Join(scriptPathPref, s.cfg.Name), nil
|
|
}
|
|
|
|
const (
|
|
// optionRunComScript is the RunCom script option name.
|
|
optionRunComScript = "RunComScript"
|
|
|
|
// runComScript is the default RunCom script.
|
|
runComScript = `#!/bin/sh
|
|
#
|
|
# $OpenBSD: {{ .SvcInfo }}
|
|
|
|
daemon="{{.Path}}"
|
|
daemon_flags={{ .Arguments | args }}
|
|
|
|
. /etc/rc.d/rc.subr
|
|
|
|
rc_bg=YES
|
|
|
|
rc_cmd $1
|
|
`
|
|
)
|
|
|
|
// template returns the script template to put into rc.d.
|
|
func (s *openbsdRunComService) template() (t *template.Template) {
|
|
tf := map[string]any{
|
|
"args": func(sl []string) string {
|
|
return `"` + strings.Join(sl, " ") + `"`
|
|
},
|
|
}
|
|
|
|
return template.Must(template.New("").Funcs(tf).Parse(getString(
|
|
s.cfg.Option,
|
|
optionRunComScript,
|
|
runComScript,
|
|
)))
|
|
}
|
|
|
|
// execPath returns the absolute path to the executable to be run as a service.
|
|
func (s *openbsdRunComService) execPath() (path string, err error) {
|
|
if c := s.cfg; c != nil && len(c.Executable) != 0 {
|
|
return filepath.Abs(c.Executable)
|
|
}
|
|
|
|
if path, err = os.Executable(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Abs(path)
|
|
}
|
|
|
|
// annotate wraps errors.Annotate applying a common error format.
|
|
func (s *openbsdRunComService) annotate(action string, err error) (annotated error) {
|
|
return errors.Annotate(err, "%s %s %s service: %w", action, sysVersion, s.cfg.Name)
|
|
}
|
|
|
|
// Install implements service.Service interface for *openbsdRunComService.
|
|
func (s *openbsdRunComService) Install() (err error) {
|
|
defer func() { err = s.annotate("installing", err) }()
|
|
|
|
if err = s.writeScript(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.configureSysStartup(true)
|
|
}
|
|
|
|
// configureSysStartup adds s into the group of packages started with system.
|
|
func (s *openbsdRunComService) configureSysStartup(enable bool) (err error) {
|
|
cmd := "enable"
|
|
if !enable {
|
|
cmd = "disable"
|
|
}
|
|
|
|
var code int
|
|
code, _, err = aghos.RunCommand("rcctl", cmd, s.cfg.Name)
|
|
if err != nil {
|
|
return err
|
|
} else if code != 0 {
|
|
return fmt.Errorf("rcctl finished with code %d", code)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeScript tries to write the script for the service.
|
|
func (s *openbsdRunComService) writeScript() (err error) {
|
|
var scriptPath string
|
|
if scriptPath, err = s.scriptPath(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = os.Stat(scriptPath); !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("script already exists at %s", scriptPath)
|
|
}
|
|
|
|
var execPath string
|
|
if execPath, err = s.execPath(); err != nil {
|
|
return err
|
|
}
|
|
|
|
t := s.template()
|
|
f, err := os.Create(scriptPath)
|
|
if err != nil {
|
|
return fmt.Errorf("creating rc.d script file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
err = t.Execute(f, &struct {
|
|
*service.Config
|
|
Path string
|
|
SvcInfo string
|
|
}{
|
|
Config: s.cfg,
|
|
Path: execPath,
|
|
SvcInfo: getString(s.cfg.Option, "SvcInfo", s.String()),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return errors.Annotate(
|
|
os.Chmod(scriptPath, 0o755),
|
|
"changing rc.d script file permissions: %w",
|
|
)
|
|
}
|
|
|
|
// Uninstall implements service.Service interface for *openbsdRunComService.
|
|
func (s *openbsdRunComService) Uninstall() (err error) {
|
|
defer func() { err = s.annotate("uninstalling", err) }()
|
|
|
|
if err = s.configureSysStartup(false); err != nil {
|
|
return err
|
|
}
|
|
|
|
var scriptPath string
|
|
if scriptPath, err = s.scriptPath(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = os.Remove(scriptPath); errors.Is(err, os.ErrNotExist) {
|
|
return service.ErrNotInstalled
|
|
}
|
|
|
|
return errors.Annotate(err, "removing rc.d script: %w")
|
|
}
|
|
|
|
// optionRunWait is the name of the option associated with function which waits
|
|
// for the service to be stopped.
|
|
const optionRunWait = "RunWait"
|
|
|
|
// runWait is the default function to wait for service to be stopped.
|
|
func runWait() {
|
|
sigChan := make(chan os.Signal, 3)
|
|
signal.Notify(sigChan, syscall.SIGTERM, os.Interrupt)
|
|
<-sigChan
|
|
}
|
|
|
|
// Run implements service.Service interface for *openbsdRunComService.
|
|
func (s *openbsdRunComService) Run() (err error) {
|
|
if err = s.i.Start(s); err != nil {
|
|
return err
|
|
}
|
|
|
|
getFuncNiladic(s.cfg.Option, optionRunWait, runWait)()
|
|
|
|
return s.i.Stop(s)
|
|
}
|
|
|
|
// runCom calls the script with the specified cmd.
|
|
func (s *openbsdRunComService) runCom(cmd string) (out string, err error) {
|
|
var scriptPath string
|
|
if scriptPath, err = s.scriptPath(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// TODO(e.burkov): It's possible that os.ErrNotExist is caused by
|
|
// something different than the service script's non-existence. Keep it
|
|
// in mind, when replace the aghos.RunCommand.
|
|
var outData []byte
|
|
_, outData, err = aghos.RunCommand(scriptPath, cmd)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return "", service.ErrNotInstalled
|
|
}
|
|
|
|
return string(outData), err
|
|
}
|
|
|
|
// Status implements service.Service interface for *openbsdRunComService.
|
|
func (s *openbsdRunComService) Status() (status service.Status, err error) {
|
|
defer func() { err = s.annotate("getting status of", err) }()
|
|
|
|
var out string
|
|
if out, err = s.runCom("check"); err != nil {
|
|
return service.StatusUnknown, err
|
|
}
|
|
|
|
name := s.cfg.Name
|
|
switch out {
|
|
case fmt.Sprintf("%s(ok)\n", name):
|
|
return service.StatusRunning, nil
|
|
case fmt.Sprintf("%s(failed)\n", name):
|
|
return service.StatusStopped, nil
|
|
default:
|
|
return service.StatusUnknown, service.ErrNotInstalled
|
|
}
|
|
}
|
|
|
|
// Start implements service.Service interface for *openbsdRunComService.
|
|
func (s *openbsdRunComService) Start() (err error) {
|
|
_, err = s.runCom("start")
|
|
|
|
return s.annotate("starting", err)
|
|
}
|
|
|
|
// Stop implements service.Service interface for *openbsdRunComService.
|
|
func (s *openbsdRunComService) Stop() (err error) {
|
|
_, err = s.runCom("stop")
|
|
|
|
return s.annotate("stopping", err)
|
|
}
|
|
|
|
// Restart implements service.Service interface for *openbsdRunComService.
|
|
func (s *openbsdRunComService) Restart() (err error) {
|
|
if err = s.Stop(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.Start()
|
|
}
|
|
|
|
// Logger implements service.Service interface for *openbsdRunComService.
|
|
func (s *openbsdRunComService) Logger(errs chan<- error) (l service.Logger, err error) {
|
|
if service.ChosenSystem().Interactive() {
|
|
return service.ConsoleLogger, nil
|
|
}
|
|
|
|
return s.SystemLogger(errs)
|
|
}
|
|
|
|
// SystemLogger implements service.Service interface for *openbsdRunComService.
|
|
func (s *openbsdRunComService) SystemLogger(errs chan<- error) (l service.Logger, err error) {
|
|
return newSysLogger(s.cfg.Name, errs)
|
|
}
|
|
|
|
// newSysLogger returns a stub service.Logger implementation.
|
|
func newSysLogger(_ string, _ chan<- error) (service.Logger, error) {
|
|
return sysLogger{}, nil
|
|
}
|
|
|
|
// sysLogger wraps calls of the logging functions understandable for service
|
|
// interfaces.
|
|
type sysLogger struct{}
|
|
|
|
// Error implements service.Logger interface for sysLogger.
|
|
func (sysLogger) Error(v ...any) error {
|
|
log.Error(fmt.Sprint(v...))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Warning implements service.Logger interface for sysLogger.
|
|
func (sysLogger) Warning(v ...any) error {
|
|
log.Info("warning: %s", fmt.Sprint(v...))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Info implements service.Logger interface for sysLogger.
|
|
func (sysLogger) Info(v ...any) error {
|
|
log.Info(fmt.Sprint(v...))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Errorf implements service.Logger interface for sysLogger.
|
|
func (sysLogger) Errorf(format string, a ...any) error {
|
|
log.Error(format, a...)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Warningf implements service.Logger interface for sysLogger.
|
|
func (sysLogger) Warningf(format string, a ...any) error {
|
|
log.Info("warning: %s", fmt.Sprintf(format, a...))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Infof implements service.Logger interface for sysLogger.
|
|
func (sysLogger) Infof(format string, a ...any) error {
|
|
log.Info(format, a...)
|
|
|
|
return nil
|
|
}
|