Files
Maksim Kazantsev 483f39bb03 Pull request 2748: ADG-12585-imp-code
Squashed commit of the following:

commit 9804118f8c8abb684dc5fcfc55509e72d7cdbbd8
Merge: 5b4a1e42b c5c862331
Author: Maksim Kazantsev <m.kazantsev@adguard.com>
Date:   Tue Aug 18 15:19:35 2026 +0300

    Merge branch 'master' into ADG-12585-imp-code

commit 5b4a1e42bee8cb70118594af36966a226aab1799
Author: Maksim Kazantsev <m.kazantsev@adguard.com>
Date:   Thu Aug 13 11:19:35 2026 +0300

    ossvc: fix contract;

commit 48fa81b7e8fce5a1f1d0d71fc6d888819cdaad33
Author: Maksim Kazantsev <m.kazantsev@adguard.com>
Date:   Thu Aug 13 11:02:21 2026 +0300

    all: imp code; imp docs;

commit 0cf4bb20fa6356aa97788d9215c14423159f7ac0
Author: Maksim Kazantsev <m.kazantsev@adguard.com>
Date:   Wed Aug 12 15:02:56 2026 +0300

    all: imp code;

commit c6ea4a1e505e60483a55a66629c51e577aa10b78
Author: Maksim Kazantsev <m.kazantsev@adguard.com>
Date:   Tue Aug 11 19:39:17 2026 +0300

    all: imp code; imp docs;

commit 056388b4b165b585099449496826fac01e6d9840
Author: Maksim Kazantsev <m.kazantsev@adguard.com>
Date:   Tue Aug 11 18:13:43 2026 +0300

    all: complete todos;
2026-08-18 12:28:32 +00:00

342 lines
9.3 KiB
Go

package ossvc
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"runtime"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/logutil/slogutil"
"github.com/AdguardTeam/golibs/osutil/executil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/kardianos/service"
)
// TODO(e.burkov): Declare managers for each OS.
// manager is the implementation of [Manager] that uses [service.Service].
type manager struct {
logger *slog.Logger
clock timeutil.Clock
cmdCons executil.CommandConstructor
isOpenWrt bool
isUnixSystemV bool
}
// newManager creates a new [Manager] that uses [service.Service].
//
// TODO(e.burkov): Return error.
func newManager(ctx context.Context, conf *ManagerConfig) (mgr *manager) {
// Call chooseSystem explicitly to introduce platform-specific support for
// service package. It's a noop for other GOOS values.
chooseSystem(ctx, conf.Logger, conf.CommandConstructor)
return &manager{
logger: conf.Logger,
clock: conf.Clock,
cmdCons: conf.CommandConstructor,
isOpenWrt: aghos.IsOpenWrt(),
isUnixSystemV: service.Platform() == "unix-systemv",
}
}
// type check
var _ Manager = (*manager)(nil)
// Perform implements the [Manager] interface for *manager.
func (m *manager) Perform(ctx context.Context, action Action) (err error) {
switch action := action.(type) {
case *ActionInstall:
err = m.install(ctx, action)
case *ActionRestart:
err = m.restart(ctx, action)
case *ActionStart:
err = m.start(ctx, action)
case *ActionStop:
err = m.stop(ctx, action)
case *ActionUninstall:
err = m.uninstall(ctx, action)
default:
panic(fmt.Errorf("action: %w: %T(%[2]v)", errors.ErrBadEnumValue, action))
}
if err != nil {
// Don't wrap the error, since it's informative enough as is.
return err
}
m.logger.DebugContext(
ctx,
"performed service action",
"action", action.Name(),
"system", service.ChosenSystem(),
)
return nil
}
// statusRestartOnFail is a custom status value used to indicate the
// service's state of restarting after failed start.
const statusRestartOnFail = service.StatusStopped + 1
// Status implements the [Manager] interface for *manager.
func (m *manager) Status(ctx context.Context, name ServiceName) (status Status, err error) {
m.logger.InfoContext(ctx, "getting service status", "name", name)
s, err := service.New(nil, &service.Config{
Name: string(name),
})
if err != nil {
return "", fmt.Errorf("creating service: %w", err)
}
svcStatus, err := s.Status()
if err != nil && m.isUnixSystemV {
m.logger.DebugContext(ctx, "got error from status request", slogutil.KeyError, err)
err = m.runInitdCommand(ctx, string(name), "status")
if err != nil {
m.logger.DebugContext(ctx, "got error from init.d command", slogutil.KeyError, err)
// Treat an error or non-zero exit code as stopped status on Unix
// System V.
//
// TODO(e.burkov): Investigate if it's a valid assumption, and
// properly handle errors in similar cases.
return StatusStopped, nil
}
return StatusRunning, nil
}
if err != nil {
if errors.Is(err, service.ErrNotInstalled) {
return StatusNotInstalled, nil
}
return "", fmt.Errorf("getting service status: %w", err)
}
return statusToInternal(svcStatus)
}
// type check
var _ ReloadManager = (*manager)(nil)
// Reload implements the [ReloadManager] interface for *manager.
func (m *manager) Reload(ctx context.Context, name ServiceName) (err error) {
return m.reload(ctx, name)
}
// install installs the service in the service manager.
func (m *manager) install(ctx context.Context, action *ActionInstall) (err error) {
m.logger.InfoContext(ctx, "installing service", "name", action.ServiceName)
conf := &service.Config{
Name: string(action.ServiceName),
DisplayName: action.DisplayName,
Description: action.Description,
WorkingDirectory: action.WorkingDirectory,
Arguments: action.Arguments,
}
ConfigureServiceOptions(conf, m.clock.Now(), action.Version)
s, err := service.New(emptyInterface{}, conf)
if err != nil {
return fmt.Errorf("creating service: %w", err)
}
err = s.Install()
if err != nil {
return fmt.Errorf("installing service: %w", err)
}
if m.isOpenWrt {
// On OpenWrt it is important to run enable after the service
// installation. Otherwise, the service won't start on the system
// startup.
err = m.runInitdCommand(ctx, string(action.ServiceName), "enable")
if err != nil {
return fmt.Errorf("enabling service on openwrt: %w", err)
}
}
return nil
}
// restart stops, if not yet, and starts the configured service in the service
// manager.
func (m *manager) restart(ctx context.Context, action *ActionRestart) (err error) {
m.logger.InfoContext(ctx, "restarting service", "name", action.ServiceName)
s, err := service.New(emptyInterface{}, &service.Config{
Name: string(action.ServiceName),
})
if err != nil {
return fmt.Errorf("creating service: %w", err)
}
err = s.Restart()
if err != nil && m.isUnixSystemV {
initdErr := m.runInitdCommand(ctx, string(action.ServiceName), "restart")
if initdErr != nil {
return fmt.Errorf("%w (restarting via init.d: %w)", err, initdErr)
}
}
return err
}
// start starts the configured service in the service manager.
func (m *manager) start(ctx context.Context, action *ActionStart) (err error) {
m.logger.InfoContext(ctx, "starting service", "name", action.ServiceName)
// Perform pre-check before starting service.
if err = aghos.PreCheckActionStart(); err != nil {
m.logger.ErrorContext(ctx, "pre-check failed", "err", err)
}
s, err := service.New(emptyInterface{}, &service.Config{
Name: string(action.ServiceName),
})
if err != nil {
return fmt.Errorf("creating service: %w", err)
}
err = s.Start()
if err != nil && m.isUnixSystemV {
initdErr := m.runInitdCommand(ctx, string(action.ServiceName), "start")
if initdErr != nil {
return fmt.Errorf("%w (starting via init.d: %w)", err, initdErr)
}
}
return err
}
// stop stops the service in the service manager.
func (m *manager) stop(ctx context.Context, action *ActionStop) (err error) {
m.logger.InfoContext(ctx, "stopping service", "name", action.ServiceName)
conf := &service.Config{
Name: string(action.ServiceName),
}
s, err := service.New(emptyInterface{}, conf)
if err != nil {
return fmt.Errorf("creating service: %w", err)
}
err = s.Stop()
if err != nil && m.isUnixSystemV {
initdErr := m.runInitdCommand(ctx, string(action.ServiceName), "stop")
if initdErr != nil {
return fmt.Errorf("%w (stopping via init.d: %w)", err, initdErr)
}
}
return err
}
// uninstall uninstalls the service from the service manager.
func (m *manager) uninstall(ctx context.Context, action *ActionUninstall) (err error) {
m.logger.InfoContext(ctx, "uninstalling service", "name", action.ServiceName)
if m.isOpenWrt {
// On OpenWrt it is important to run disable command first as it will
// remove the symlink.
err = m.runInitdCommand(ctx, string(action.ServiceName), "disable")
if err != nil {
return fmt.Errorf("disabling service on openwrt: %w", err)
}
}
s, err := service.New(emptyInterface{}, &service.Config{
Name: string(action.ServiceName),
})
if err != nil {
return fmt.Errorf("creating service: %w", err)
}
err = s.Stop()
if err != nil {
m.logger.DebugContext(ctx, "stopping service", "err", err)
}
err = s.Uninstall()
if err != nil {
return fmt.Errorf("uninstalling service: %w", err)
}
if runtime.GOOS == "darwin" {
removeLaunchdStdLogs(ctx, m.logger)
}
return nil
}
// Paths to stdout and stderr logs for Darwin service manager.
//
// TODO(e.burkov): Move to config_darwin.go.
const (
launchdStdoutPath = "/var/log/AdGuardHome.stdout.log"
launchdStderrPath = "/var/log/AdGuardHome.stderr.log"
)
// removeLaunchdStdLogs removes launchd stdout and stderr log files, if needed,
// and logs errors at warning level.
//
// TODO(e.burkov): Move to manager_darwin.go.
func removeLaunchdStdLogs(ctx context.Context, logger *slog.Logger) {
if runtime.GOOS != "darwin" {
return
}
err := os.Remove(launchdStdoutPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
logger.WarnContext(ctx, "removing stdout file", "err", err)
}
err = os.Remove(launchdStderrPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
logger.WarnContext(ctx, "removing stderr file", "err", err)
}
}
// runInitdCommand runs init.d service command. Returns an error if any.
//
// TODO(e.burkov): Move to manager_linux.go.
func (m *manager) runInitdCommand(
ctx context.Context,
serviceName string,
action string,
) (err error) {
confPath := filepath.Join("/etc", "init.d", serviceName)
// Pass the script and action as a single string argument.
return executil.RunWithPeek(
ctx,
m.cmdCons,
aghos.MaxCmdOutputSize,
"sh",
"-c",
confPath,
action,
)
}
// emptyInterface is an empty implementation of the [service.Interface], as the
// actual implementation is only needed for the [service.Service.Run] method.
type emptyInterface struct{}
// type check
var _ service.Interface = emptyInterface{}
// Start implements the [service.Interface] interface for emptyInterface.
func (emptyInterface) Start(_ service.Service) (err error) { return nil }
// Stop implements the [service.Interface] interface for emptyInterface.
func (emptyInterface) Stop(_ service.Service) (err error) { return nil }