Pull request: 3226 support service on OpenBSD

Merge in DNS/adguard-home from 3226-openbsd-svc to master

Closes #3226.

Squashed commit of the following:

commit bcf1a31a8343ae4b35c7cadeb45bc7a10863fda2
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Aug 24 17:43:31 2021 +0300

    aghos: imp code

commit 3d4060ce6b5a37cf7af05b117b8bc4a49f69b2e8
Merge: 9e9225ec b92db25e
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Aug 24 17:09:00 2021 +0300

    Merge branch 'master' into 3226-openbsd-svc

commit 9e9225ecb2af30fe46999b43c0683e4b3c946778
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Aug 24 17:02:52 2021 +0300

    home: fix lil bugs

commit 03456f9a09081c6178dca0ac9887590b5d24f333
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Aug 24 16:18:48 2021 +0300

    home: imp code

commit 5cdf4fcbae78c07b663190012228003fe94bfdee
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Aug 20 23:32:15 2021 +0300

    home: imp code, docs

commit d2a95faa0a7d176cdcba304e7226ebe11c1ce341
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Fri Aug 20 14:01:53 2021 +0300

    home: sup service on openbsd
This commit is contained in:
Eugene Burkov 2021-08-24 18:00:30 +03:00
parent b92db25e6a
commit 16092e8ba9
6 changed files with 505 additions and 39 deletions

View File

@ -27,7 +27,8 @@ and this project adheres to
- Settable timeouts for querying the upstream servers ([#2280]).
- Configuration file parameters to change group and user ID on startup on Unix
([#2763]).
- Experimental OpenBSD support for AMD64 and 64-bit ARM CPUs ([#2439], [#3225]).
- Experimental OpenBSD support for AMD64 and 64-bit ARM CPUs ([#2439], [#3225],
[#3226]).
- Support for custom port in DNS-over-HTTPS profiles for Apple's devices
([#3172]).
- `darwin/arm64` support ([#2443]).
@ -118,6 +119,7 @@ and this project adheres to
[#3198]: https://github.com/AdguardTeam/AdGuardHome/issues/3198
[#3217]: https://github.com/AdguardTeam/AdGuardHome/issues/3217
[#3225]: https://github.com/AdguardTeam/AdGuardHome/issues/3225
[#3226]: https://github.com/AdguardTeam/AdGuardHome/issues/3226
[#3256]: https://github.com/AdguardTeam/AdGuardHome/issues/3256
[#3257]: https://github.com/AdguardTeam/AdGuardHome/issues/3257
[#3289]: https://github.com/AdguardTeam/AdGuardHome/issues/3289

View File

@ -13,6 +13,7 @@ import (
"strconv"
"strings"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
)
@ -59,8 +60,11 @@ func RunCommand(command string, arguments ...string) (int, string, error) {
if len(out) > MaxCmdOutputSize {
out = out[:MaxCmdOutputSize]
}
if err != nil {
return 1, "", fmt.Errorf("exec.Command(%s) failed: %v: %s", command, err, string(out))
if errors.As(err, new(*exec.ExitError)) {
return cmd.ProcessState.ExitCode(), string(out), nil
} else if err != nil {
return 1, "", fmt.Errorf("exec.Command(%s) failed: %w: %s", command, err, string(out))
}
return cmd.ProcessState.ExitCode(), string(out), nil

View File

@ -116,13 +116,6 @@ func Main(clientBuildFS fs.FS) {
}()
if args.serviceControlAction != "" {
// TODO(a.garipov): github.com/kardianos/service doesn't seem to
// support OpenBSD currently. Either patch it to do so or make
// our own implementation of the service.System interface.
if runtime.GOOS == "openbsd" {
log.Fatal("service actions are not supported on openbsd, see issue 3226")
}
handleServiceControlAction(args, clientBuildFS)
return

View File

@ -8,8 +8,10 @@ import (
"strconv"
"strings"
"syscall"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/kardianos/service"
@ -26,33 +28,37 @@ const (
serviceDescription = "AdGuard Home: Network-level blocker"
)
// Represents the program that will be launched by a service or daemon
// program represents the program that will be launched by as a service or a
// daemon.
type program struct {
clientBuildFS fs.FS
opts options
}
// Start should quickly start the program
func (p *program) Start(s service.Service) error {
// Start should not block. Do the actual work async.
// Start implements service.Interface interface for *program.
func (p *program) Start(_ service.Service) (err error) {
// Start should not block. Do the actual work async.
args := p.opts
args.runningAsService = true
go run(args, p.clientBuildFS)
return nil
}
// Stop stops the program
func (p *program) Stop(s service.Service) error {
// Stop should not block. Return with a few seconds.
// Stop implements service.Interface interface for *program.
func (p *program) Stop(_ service.Service) error {
// Stop should not block. Return with a few seconds.
if Context.appSignalChannel == nil {
os.Exit(0)
}
Context.appSignalChannel <- syscall.SIGINT
return nil
}
// svcStatus check the service's status.
// svcStatus returns the service's status.
//
// On OpenWrt, the service utility may not exist. We use our service script
// directly in this case.
@ -87,8 +93,8 @@ func svcAction(s service.Service, action string) (err error) {
return err
}
// Send SIGHUP to a process with ID taken from our pid-file
// If pid-file doesn't exist, find our PID using 'ps' command
// Send SIGHUP to a process with PID taken from our .pid file. If it doesn't
// exist, find our PID using 'ps' command.
func sendSigReload() {
if runtime.GOOS == "windows" {
log.Error("not implemented on windows")
@ -152,11 +158,16 @@ func sendSigReload() {
// it is specified when we register a service, and it indicates to the app
// that it is being run as a service/daemon.
func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
// Call chooseSystem expicitly to introduce OpenBSD support for service
// package. It's a noop for other GOOS values.
chooseSystem()
action := opts.serviceControlAction
log.Printf("Service control action: %s", action)
if action == "reload" {
sendSigReload()
return
}
@ -164,6 +175,7 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
if err != nil {
log.Fatal("Unable to find the path to the current directory")
}
runOpts := opts
runOpts.serviceControlAction = "run"
svcConfig := &service.Config{
@ -174,39 +186,39 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
Arguments: serialize(runOpts),
}
configureService(svcConfig)
prg := &program{
clientBuildFS: clientBuildFS,
opts: runOpts,
}
s, err := service.New(prg, svcConfig)
if err != nil {
var s service.Service
if s, err = service.New(prg, svcConfig); err != nil {
log.Fatal(err)
}
if action == "status" {
switch action {
case "status":
handleServiceStatusCommand(s)
} else if action == "run" {
err = s.Run()
if err != nil {
case "run":
if err = s.Run(); err != nil {
log.Fatalf("Failed to run service: %s", err)
}
} else if action == "install" {
case "install":
initConfigFilename(opts)
initWorkingDir(opts)
handleServiceInstallCommand(s)
} else if action == "uninstall" {
case "uninstall":
handleServiceUninstallCommand(s)
} else {
err = svcAction(s, action)
if err != nil {
default:
if err = svcAction(s, action); err != nil {
log.Fatal(err)
}
}
log.Printf("Action %s has been done successfully on %s", action, service.ChosenSystem().String())
log.Printf("action %s has been done successfully on %s", action, service.ChosenSystem())
}
// handleServiceStatusCommand handles service "status" command
// handleServiceStatusCommand handles service "status" command.
func handleServiceStatusCommand(s service.Service) {
status, errSt := svcStatus(s)
if errSt != nil {
@ -231,15 +243,16 @@ func handleServiceInstallCommand(s service.Service) {
}
if aghos.IsOpenWrt() {
// On OpenWrt it is important to run enable after the service installation
// Otherwise, the service won't start on the system startup
// On OpenWrt it is important to run enable after the service
// installation Otherwise, the service won't start on the system
// startup.
_, err = runInitdCommand("enable")
if err != nil {
log.Fatal(err)
}
}
// Start automatically after install
// Start automatically after install.
err = svcAction(s, "start")
if err != nil {
log.Fatalf("Failed to start the service: %s", err)
@ -267,14 +280,13 @@ func handleServiceUninstallCommand(s service.Service) {
}
}
err := svcAction(s, "uninstall")
if err != nil {
if err := svcAction(s, "uninstall"); err != nil {
log.Fatal(err)
}
if runtime.GOOS == "darwin" {
// Remove log files on cleanup and log errors.
err = os.Remove(launchdStdoutPath)
err := os.Remove(launchdStdoutPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("removing stdout file: %s", err)
}
@ -313,6 +325,9 @@ func configureService(c *service.Config) {
} else if runtime.GOOS == "freebsd" {
c.Option["SysvScript"] = freeBSDScript
}
c.Option["RunComScript"] = openBSDScript
c.Option["SvcInfo"] = fmt.Sprintf("%s %s", version.Full(), time.Now())
}
// runInitdCommand runs init.d service command
@ -551,3 +566,17 @@ command="/usr/sbin/daemon"
command_args="-p ${pidfile} -f -r {{.WorkingDirectory}}/{{.Name}}"
run_rc_command "$1"
`
const openBSDScript = `#!/bin/sh
#
# $OpenBSD: {{ .SvcInfo }}
daemon="{{.Path}}"
daemon_flags={{ .Arguments | args }}
. /etc/rc.d/rc.subr
rc_bg=YES
rc_cmd $1
`

View File

@ -0,0 +1,432 @@
//go:build openbsd
// +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"
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]interface{}{
"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 excutable 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.
_, out, err = aghos.RunCommand(scriptPath, cmd)
if errors.Is(err, os.ErrNotExist) {
return "", service.ErrNotInstalled
}
return out, 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 ...interface{}) error {
log.Error(fmt.Sprint(v...))
return nil
}
// Warning implements service.Logger interface for sysLogger.
func (sysLogger) Warning(v ...interface{}) error {
log.Info("warning: %s", fmt.Sprint(v...))
return nil
}
// Info implements service.Logger interface for sysLogger.
func (sysLogger) Info(v ...interface{}) error {
log.Info(fmt.Sprint(v...))
return nil
}
// Errorf implements service.Logger interface for sysLogger.
func (sysLogger) Errorf(format string, a ...interface{}) error {
log.Error(format, a...)
return nil
}
// Warningf implements service.Logger interface for sysLogger.
func (sysLogger) Warningf(format string, a ...interface{}) error {
log.Info("warning: %s", fmt.Sprintf(format, a...))
return nil
}
// Infof implements service.Logger interface for sysLogger.
func (sysLogger) Infof(format string, a ...interface{}) error {
log.Info(format, a...)
return nil
}

View File

@ -0,0 +1,6 @@
//go:build !openbsd
// +build !openbsd
package home
func chooseSystem() {}