mirror of
https://git.vectorsigma.ru/public/AdGuardHome.git
synced 2026-08-03 22:29:30 +00:00
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:
@@ -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.
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user