AdGuardHome/internal/home/service_openbsd.go

434 lines
11 KiB
Go

//go:build openbsd
package home
import (
"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/AdguardTeam/golibs/stringutil"
"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 stringutil.Coalesce(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
}