Pull request 2490: AGDNS-2966-ossvc
Some checks failed
build / test (macOS-latest) (push) Has been cancelled
build / test (ubuntu-latest) (push) Has been cancelled
build / test (windows-latest) (push) Has been cancelled
build / build-release (push) Has been cancelled
build / notify (push) Has been cancelled
lint / go-lint (push) Has been cancelled
lint / eslint (push) Has been cancelled
lint / notify (push) Has been cancelled

Squashed commit of the following:

commit 444c1d8fba64271f0348eaa5d979086b0f383eca
Merge: 79acd1e3e 68fac0147
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Oct 7 16:31:18 2025 +0300

    Merge branch 'master' into AGDNS-2966-ossvc

commit 79acd1e3e7958ab7c697b4726ec9e92d83186170
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Oct 7 16:04:14 2025 +0300

    ossvc: imp log, doc

commit bd030f01a3
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Oct 6 18:31:15 2025 +0300

    ossvc: imp docs

commit ff41521552
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Oct 6 14:03:19 2025 +0300

    ossvc: imp docs, code

commit b097b19d0a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Oct 2 19:52:45 2025 +0300

    ossvc: imp code, docs

commit af5c1b61c5
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Sep 29 15:35:11 2025 +0300

    ossvc: introduce package
This commit is contained in:
Eugene Burkov
2025-10-07 18:24:24 +03:00
parent 68fac0147d
commit 42baae311c
5 changed files with 323 additions and 0 deletions

28
internal/ossvc/action.go Normal file
View File

@@ -0,0 +1,28 @@
package ossvc
// ActionName is the type for actions' names. It has the following valid
// values:
// - [ActionNameInstall]
// - [ActionNameReload]
// - [ActionNameStart]
// - [ActionNameStop]
// - [ActionNameUninstall]
type ActionName string
const (
ActionNameInstall ActionName = "install"
ActionNameReload ActionName = "reload"
ActionNameStart ActionName = "start"
ActionNameStop ActionName = "stop"
ActionNameUninstall ActionName = "uninstall"
)
// Action is the interface for actions that can be performed by [Manager].
type Action interface {
// Name returns the name of the action.
Name() (name ActionName)
// isAction is a marker method to prevent types from other packages from
// implementing this interface.
isAction()
}

View File

@@ -0,0 +1,80 @@
package ossvc
import "github.com/kardianos/service"
// TODO(e.burkov): Declare actions for each OS.
// ActionInstall is the implementation of the [Action] interface.
type ActionInstall struct {
// ServiceConf is the configuration for the service to control.
//
// TODO(e.burkov): Get rid of github.com/kardianos/service dependency and
// replace with the actual configuration.
ServiceConf *service.Config
}
// Name implements the [Action] interface for *ActionInstall.
func (a *ActionInstall) Name() (name ActionName) { return ActionNameInstall }
// isAction implements the [Action] interface for *ActionInstall.
func (a *ActionInstall) isAction() {}
// ActionReload is the implementation of the [Action] interface.
type ActionReload struct {
// ServiceConf is the configuration for the service to control.
//
// TODO(e.burkov): Get rid of github.com/kardianos/service dependency and
// replace with the actual configuration.
ServiceConf *service.Config
}
// Name implements the [Action] interface for *ActionReload.
func (a *ActionReload) Name() (name ActionName) { return ActionNameReload }
// isAction implements the [Action] interface for *ActionReload.
func (a *ActionReload) isAction() {}
// ActionStart is the implementation of the [Action] interface.
type ActionStart struct {
// ServiceConf is the configuration for the service to control.
//
// TODO(e.burkov): Get rid of github.com/kardianos/service dependency and
// replace with the actual configuration.
ServiceConf *service.Config
}
// Name implements the [Action] interface for *ActionStart.
func (a *ActionStart) Name() (name ActionName) { return ActionNameStart }
// isAction implements the [Action] interface for *ActionStart.
func (a *ActionStart) isAction() {}
// ActionStop is the implementation of the [Action] interface.
type ActionStop struct {
// ServiceConf is the configuration for the service to control.
//
// TODO(e.burkov): Get rid of github.com/kardianos/service dependency and
// replace with the actual configuration.
ServiceConf *service.Config
}
// Name implements the [Action] interface for *ActionStop.
func (a *ActionStop) Name() (name ActionName) { return ActionNameStop }
// isAction implements the [Action] interface for *ActionStop.
func (a *ActionStop) isAction() {}
// ActionUninstall is the implementation of the [Action] interface.
type ActionUninstall struct {
// ServiceConf is the configuration for the service to control.
//
// TODO(e.burkov): Get rid of github.com/kardianos/service dependency and
// replace with the actual configuration.
ServiceConf *service.Config
}
// Name implements the [Action] interface for *ActionUninstall.
func (a *ActionUninstall) Name() (name ActionName) { return ActionNameUninstall }
// isAction implements the [Action] interface for *ActionUninstall.
func (a *ActionUninstall) isAction() {}

View File

@@ -0,0 +1,139 @@
package ossvc
import (
"context"
_ "embed"
"fmt"
"log/slog"
"github.com/AdguardTeam/golibs/errors"
"github.com/kardianos/service"
)
// TODO(e.burkov): Declare managers for each OS.
// manager is the implementation of [Manager] that wraps [service.Service].
type manager struct {
logger *slog.Logger
}
// newManager creates a new [Manager] that wraps [service.Service].
//
// TODO(e.burkov): Return error.
func newManager(_ context.Context, conf *ManagerConfig) (mgr *manager) {
return &manager{
logger: conf.Logger,
}
}
// 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:
return m.install(ctx, action)
case *ActionReload:
return m.reload(ctx, action)
case *ActionStart:
return m.start(ctx, action)
case *ActionStop:
return m.stop(ctx, action)
case *ActionUninstall:
return m.uninstall(ctx, action)
default:
return fmt.Errorf("action: %w: %T(%[2]v)", errors.ErrBadEnumValue, action)
}
}
// 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.ServiceConf.Name)
s, err := service.New(nil, action.ServiceConf)
if err != nil {
return fmt.Errorf("creating service: %w", err)
}
return s.Install()
}
// reload stops, if not yet, and starts the configured service in the service
// manager.
func (m *manager) reload(ctx context.Context, action *ActionReload) (err error) {
m.logger.InfoContext(ctx, "reloading service", "name", action.ServiceConf.Name)
s, err := service.New(nil, action.ServiceConf)
if err != nil {
return fmt.Errorf("creating service: %w", err)
}
return s.Restart()
}
// 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.ServiceConf.Name)
s, err := service.New(nil, action.ServiceConf)
if err != nil {
return fmt.Errorf("creating service: %w", err)
}
return s.Start()
}
// 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 {
if errors.Is(err, service.ErrNotInstalled) {
return StatusNotInstalled, nil
}
return "", fmt.Errorf("getting service status: %w", err)
}
switch svcStatus {
case service.StatusRunning:
return StatusRunning, nil
case service.StatusStopped:
return StatusStopped, nil
default:
return "", fmt.Errorf("service status: %w: %v", errors.ErrBadEnumValue, svcStatus)
}
}
// 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.ServiceConf.Name)
s, err := service.New(nil, action.ServiceConf)
if err != nil {
return fmt.Errorf("creating service: %w", err)
}
return s.Stop()
}
// 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.ServiceConf.Name)
s, err := service.New(nil, action.ServiceConf)
if err != nil {
return fmt.Errorf("creating service: %w", err)
}
return s.Uninstall()
}

52
internal/ossvc/manager.go Normal file
View File

@@ -0,0 +1,52 @@
package ossvc
import (
"context"
"log/slog"
"github.com/AdguardTeam/golibs/osutil/executil"
)
// Manager is the interface for communication with the OS service manager.
//
// TODO(e.burkov): Move to golibs.
//
// TODO(e.burkov): Use.
type Manager interface {
// Perform performs the specified action.
Perform(ctx context.Context, action Action) (err error)
// Status returns the status of the service with the given name.
Status(ctx context.Context, name ServiceName) (status Status, err error)
}
// ManagerConfig contains the configuration for [Manager].
type ManagerConfig struct {
// Logger is the logger to use.
Logger *slog.Logger
// CommandConstructor is the constructor to use for creating commands.
CommandConstructor executil.CommandConstructor
}
// NewManager returns a new properly initialized [Manager], appropriate for the
// current platform.
func NewManager(ctx context.Context, conf *ManagerConfig) (mgr Manager, err error) {
return newManager(ctx, conf), nil
}
// EmptyManager is an empty implementation of [Manager] that does nothing.
type EmptyManager struct{}
// type check
var _ Manager = EmptyManager{}
// Perform implements the [Manager] interface for EmptyManager.
func (EmptyManager) Perform(_ context.Context, _ Action) (err error) {
return nil
}
// Status implements the [Manager] interface for EmptyManager.
func (EmptyManager) Status(_ context.Context, _ ServiceName) (status Status, err error) {
return StatusNotInstalled, nil
}

24
internal/ossvc/ossvc.go Normal file
View File

@@ -0,0 +1,24 @@
// Package ossvc contains abstractions and utilities for platform-independent
// service management.
//
// TODO(e.burkov): Add tests.
package ossvc
// ServiceName is the name of a service.
//
// TODO(e.burkov): Validate for each platform.
type ServiceName string
// Status represents the status of a service.
type Status string
const (
// StatusNotInstalled means that the service is not installed.
StatusNotInstalled Status = "not installed"
// StatusStopped means that the service is stopped.
StatusStopped Status = "stopped"
// StatusRunning means that the service is running.
StatusRunning Status = "running"
)