Pull request 2419: AGDNS-2929 Systemd service status

Merge in DNS/adguard-home from AGDNS-2929-systemd-service-status to master

Squashed commit of the following:

commit 89ec40c2267b94e354795e0ced40b5a929e4a7b1
Merge: 8fe6f2fc8 21834ee3c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Jun 2 13:55:34 2025 +0300

    Merge branch 'master' into AGDNS-2929-systemd-service-status

commit 8fe6f2fc88d9354e27ddd9bc5e7bb15015de5bea
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri May 30 16:59:11 2025 +0300

    home: fix doc

commit cb947ed4042dbcb3cf88b7c082f0d8f81d560bed
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu May 29 22:29:54 2025 +0300

    home: imp docs

commit 3edfffdbc77c2b0fc54050b8884cf2b1dfe9026a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu May 29 15:21:26 2025 +0300

    home: reimpl status on systemd
This commit is contained in:
Eugene Burkov
2025-06-02 15:35:00 +03:00
parent 21834ee3c5
commit 7cf1600f1b
3 changed files with 195 additions and 25 deletions

View File

@@ -17,6 +17,11 @@ See also the [v0.107.63 GitHub milestone][ms-v0.107.63].
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Fixed
- Status reported by the systemd service implementation in cases of auto-restart after a failed start.
<!-- <!--
NOTE: Add new changes ABOVE THIS COMMENT. NOTE: Add new changes ABOVE THIS COMMENT.
--> -->

View File

@@ -292,6 +292,10 @@ func handleServiceCommand(s service.Service, action string, opts options) (err e
return nil 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
// handleServiceStatusCommand handles service "status" command. // handleServiceStatusCommand handles service "status" command.
func handleServiceStatusCommand(s service.Service) { func handleServiceStatusCommand(s service.Service) {
status, errSt := svcStatus(s) status, errSt := svcStatus(s)
@@ -306,6 +310,8 @@ func handleServiceStatusCommand(s service.Service) {
log.Printf("service: stopped") log.Printf("service: stopped")
case service.StatusRunning: case service.StatusRunning:
log.Printf("service: running") log.Printf("service: running")
case statusRestartOnFail:
log.Printf("service: restarting after failed start")
} }
} }

View File

@@ -3,6 +3,12 @@
package home package home
import ( import (
"bufio"
"fmt"
"io"
"os/exec"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/aghos" "github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/kardianos/service" "github.com/kardianos/service"
) )
@@ -11,54 +17,71 @@ import (
// implementation if needed. // implementation if needed.
func chooseSystem() { func chooseSystem() {
sys := service.ChosenSystem() sys := service.ChosenSystem()
// By default, package service uses the SysV system if it cannot detect switch sys.String() {
// anything other, but the update-rc.d fix should not be applied on OpenWrt, case "unix-systemv":
// so exclude it explicitly. // By default, package service uses the SysV system if it cannot detect
// // anything other, but the update-rc.d fix should not be applied on
// See https://github.com/AdguardTeam/AdGuardHome/issues/4480 and // OpenWrt, so exclude it explicitly.
// https://github.com/AdguardTeam/AdGuardHome/issues/4677. //
if sys.String() == "unix-systemv" && !aghos.IsOpenWrt() { // See https://github.com/AdguardTeam/AdGuardHome/issues/4480 and
service.ChooseSystem(sysvSystem{System: sys}) // https://github.com/AdguardTeam/AdGuardHome/issues/4677.
if !aghos.IsOpenWrt() {
service.ChooseSystem(&sysvSystem{System: sys})
}
case "linux-systemd":
service.ChooseSystem(&systemdSystem{System: sys})
default:
// Do nothing.
} }
} }
// sysvSystem is a wrapper for service.System that wraps the service.Service // sysvSystem is a wrapper for a [service.System] that returns the custom
// while creating a new one. // implementation of the [service.Service] interface.
// //
// TODO(e.burkov): File a PR to github.com/kardianos/service. // TODO(e.burkov): File a PR to github.com/kardianos/service.
type sysvSystem struct { type sysvSystem struct {
// System is expected to have an unexported type // System must have an unexported type *service.linuxSystemService.
// *service.linuxSystemService.
service.System service.System
} }
// New returns a wrapped service.Service. // type check
func (sys sysvSystem) New(i service.Interface, c *service.Config) (s service.Service, err error) { var _ service.System = (*sysvSystem)(nil)
// New implements the [service.System] interface for *sysvSystem. i and c must
// not be nil.
func (sys *sysvSystem) New(i service.Interface, c *service.Config) (s service.Service, err error) {
s, err = sys.System.New(i, c) s, err = sys.System.New(i, c)
if err != nil { if err != nil {
// Don't wrap the error to keep it as close to the original one as
// possible.
return s, err return s, err
} }
return sysvService{ return &sysvService{
Service: s, Service: s,
name: c.Name, name: c.Name,
}, nil }, nil
} }
// sysvService is a wrapper for a service.Service that also calls update-rc.d in // sysvService is a wrapper for a SysV [service.Service] that supplements the
// a proper way on installing and uninstalling. // installation and uninstallation.
type sysvService struct { type sysvService struct {
// Service is expected to have an unexported type *service.sysv. // Service must have an unexported type *service.sysv.
service.Service service.Service
// name stores the name of the service to call updating script with it. // name stores the name of the service to call updating script with it.
name string name string
} }
// Install wraps service.Service.Install call with calling the updating script. // type check
func (svc sysvService) Install() (err error) { var _ service.Service = (*sysvService)(nil)
// Install implements the [service.Service] interface for *sysvService.
func (svc *sysvService) Install() (err error) {
err = svc.Service.Install() err = svc.Service.Install()
if err != nil { if err != nil {
// Don't wrap an error since it's informative enough as is. // Don't wrap the error to keep it as close to the original one as
// possible.
return err return err
} }
@@ -68,12 +91,12 @@ func (svc sysvService) Install() (err error) {
return err return err
} }
// Uninstall wraps service.Service.Uninstall call with calling the updating // Uninstall implements the [service.Service] interface for *sysvService.
// script. func (svc *sysvService) Uninstall() (err error) {
func (svc sysvService) Uninstall() (err error) {
err = svc.Service.Uninstall() err = svc.Service.Uninstall()
if err != nil { if err != nil {
// Don't wrap an error since it's informative enough as is. // Don't wrap the error to keep it as close to the original one as
// possible.
return err return err
} }
@@ -82,3 +105,139 @@ func (svc sysvService) Uninstall() (err error) {
// Don't wrap an error since it's informative enough as is. // Don't wrap an error since it's informative enough as is.
return err return err
} }
// systemdSystem is a wrapper for a [service.System] that returns the custom
// implementation of the [service.Service] interface.
type systemdSystem struct {
// System must have an unexported type *service.linuxSystemService.
service.System
}
// type check
var _ service.System = (*systemdSystem)(nil)
// New implements the [service.System] interface for *systemdSystem. i and c
// must not be nil.
func (sys *systemdSystem) New(i service.Interface, c *service.Config) (s service.Service, err error) {
s, err = sys.System.New(i, c)
if err != nil {
// Don't wrap the error to keep it as close to the original one as
// possible.
return s, err
}
return &systemdService{
Service: s,
unitName: fmt.Sprintf("%s.service", c.Name),
}, nil
}
// type check
var _ service.Service = (*systemdService)(nil)
// systemdService is a wrapper for a systemd [service.Service] that enriches the
// service status information.
type systemdService struct {
// Service is expected to have an unexported type *service.systemd.
service.Service
// unitName stores the name of the systemd daemon.
unitName string
}
// type check
var _ service.Service = (*systemdService)(nil)
// Status implements the [service.Service] interface for *systemdService.
func (s *systemdService) Status() (status service.Status, err error) {
cmd := exec.Command("systemctl", "show", s.unitName)
stdout, err := cmd.StdoutPipe()
if err != nil {
return service.StatusUnknown, fmt.Errorf("connecting to command stdout: %w", err)
}
if err = cmd.Start(); err != nil {
return service.StatusUnknown, fmt.Errorf("start command executing: %w", err)
}
status, err = parseSystemctlShow(stdout)
if err != nil {
return service.StatusUnknown, fmt.Errorf("parsing command output: %w", err)
}
err = cmd.Wait()
if err != nil {
return service.StatusUnknown, fmt.Errorf("executing command: %w", err)
}
return status, nil
}
// Searched property names. See man systemctl(1).
const (
propNameLoadState = "LoadState"
propNameActiveState = "ActiveState"
propNameSubState = "SubState"
)
// parseSystemctlShow parses the output of the systemctl show command. It
// expects the key=value pairs separated by newlines.
func parseSystemctlShow(output io.Reader) (status service.Status, err error) {
var loadState, activeState, subState string
scanner := bufio.NewScanner(output)
for scanner.Scan() {
line := scanner.Text()
propName, propValue, ok := strings.Cut(line, "=")
if !ok {
return service.StatusUnknown, fmt.Errorf("unexpected line format: %q", line)
}
switch propName {
case propNameLoadState:
loadState = propValue
case propNameActiveState:
activeState = propValue
case propNameSubState:
subState = propValue
default:
// Go on.
}
}
if err = scanner.Err(); err != nil {
return service.StatusUnknown, err
}
return statusFromState(loadState, activeState, subState)
}
// statusFromState returns the service status based on the systemctl state
// property values.
func statusFromState(loadState, activeState, subState string) (status service.Status, err error) {
// Desired property values. See man systemctl(1).
const (
propValueLoadStateNotFound = "not-found"
propValueActiveStateActive = "active"
propValueActiveStateInactive = "inactive"
propValueSubStateAutoRestart = "auto-restart"
)
switch {
case loadState == propValueLoadStateNotFound:
return service.StatusUnknown, service.ErrNotInstalled
case activeState == propValueActiveStateActive:
return service.StatusRunning, nil
case activeState == propValueActiveStateInactive:
return service.StatusStopped, nil
case subState == propValueSubStateAutoRestart:
return statusRestartOnFail, nil
default:
return service.StatusUnknown, fmt.Errorf(
"unexpected state: %s=%q, %s=%q, %s=%q",
propNameLoadState, loadState,
propNameActiveState, activeState,
propNameSubState, subState,
)
}
}