mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-06-28 11:51:22 +00:00
Pull request 2607: AGDNS-3661-launchd-svc-status
Squashed commit of the following: commit 6912e3a609f82457d1bca7121a309a3b9be84ed8 Merge: 9892a8258d7b6dc0cfAuthor: f.setrakov <f.setrakov@adguard.com> Date: Fri Mar 20 11:33:34 2026 +0300 Merge branch 'master' into AGDNS-3661-launchd-svc-status commit 9892a825801b3793956b9f501481e1b7dd4d64d3 Author: f.setrakov <f.setrakov@adguard.com> Date: Thu Mar 19 14:47:40 2026 +0300 ossvc: imp tests commit 962fdc9f1c7bd8615ef4f8a2e4879369678580d8 Author: f.setrakov <f.setrakov@adguard.com> Date: Thu Mar 19 13:59:43 2026 +0300 ossvc: add logs, tests commit 3f8b678888c5d09314eb993b06abb162767b4ef2 Author: f.setrakov <f.setrakov@adguard.com> Date: Mon Mar 16 18:48:58 2026 +0300 ossvc: add warn, imp style commit 115696220d0a5bb6ef620a4a5b1dc30fecdf866c Author: f.setrakov <f.setrakov@adguard.com> Date: Fri Mar 13 18:18:18 2026 +0300 ossvc: use launchctl list commit c1ae51859de33133d8bd461e131538e4e7f8b682 Merge: 02c752fc60078b8430Author: f.setrakov <f.setrakov@adguard.com> Date: Fri Mar 13 18:12:33 2026 +0300 Merge branch 'master' into AGDNS-3661-launchd-svc-status commit 02c752fc6625e283a9c422469d9da1ebf308c80c Author: f.setrakov <f.setrakov@adguard.com> Date: Fri Mar 13 15:53:37 2026 +0300 all: add darwin service
This commit is contained in:
parent
d7b6dc0cf7
commit
1415014085
7 changed files with 312 additions and 11 deletions
|
|
@ -26,6 +26,8 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
|||
|
||||
### Fixed
|
||||
|
||||
- Status reported by the launchd service implementation in cases of scheduled service restart.
|
||||
|
||||
- Fixed clients block/unblock when moving clients between allowed and disallowed lists.
|
||||
|
||||
<!--
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ type manager struct {
|
|||
// newManager creates a new [Manager] that uses [service.Service].
|
||||
//
|
||||
// TODO(e.burkov): Return error.
|
||||
func newManager(_ context.Context, conf *ManagerConfig) (mgr *manager) {
|
||||
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()
|
||||
chooseSystem(ctx, conf.Logger)
|
||||
|
||||
return &manager{
|
||||
logger: conf.Logger,
|
||||
|
|
@ -74,8 +74,8 @@ func (m *manager) Perform(ctx context.Context, action Action) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// statusRestartOnFail is a custom status value used to indicate the service's
|
||||
// state of restarting after failed start.
|
||||
// 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.
|
||||
|
|
|
|||
173
internal/ossvc/service_darwin.go
Normal file
173
internal/ossvc/service_darwin.go
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
//go:build darwin
|
||||
|
||||
package ossvc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||
"github.com/kardianos/service"
|
||||
)
|
||||
|
||||
// chooseSystem replaces the currently selected system with a wrapper. l must
|
||||
// not be nil.
|
||||
func chooseSystem(_ context.Context, l *slog.Logger) {
|
||||
sys := service.ChosenSystem()
|
||||
service.ChooseSystem(&darwinSystem{
|
||||
System: sys,
|
||||
logger: l,
|
||||
})
|
||||
}
|
||||
|
||||
// darwinSystem is the wrapper for [service.System] that returns the custom
|
||||
// implementation of the [service.Service] interface.
|
||||
type darwinSystem struct {
|
||||
service.System
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ service.System = (*darwinSystem)(nil)
|
||||
|
||||
// New implements the [service.System] interface for *darwinSystem. i and c
|
||||
// must not be nil.
|
||||
func (d *darwinSystem) New(i service.Interface, c *service.Config) (s service.Service, err error) {
|
||||
s, err = d.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 newDarwinService(&darwinServiceConfig{
|
||||
svc: s,
|
||||
logger: d.logger,
|
||||
cmdCons: executil.SystemCommandConstructor{},
|
||||
name: c.Name,
|
||||
plistDir: "/Library/LaunchDaemons",
|
||||
}), nil
|
||||
}
|
||||
|
||||
// darwinServiceConfig is the configuration structure for [*darwinService].
|
||||
type darwinServiceConfig struct {
|
||||
// svc is the base service to extend. It must not be nil.
|
||||
svc service.Service
|
||||
|
||||
// logger is used for logging service operations. It must not be nil.
|
||||
logger *slog.Logger
|
||||
|
||||
// cmdCons is used to create system commands. It must not be nil.
|
||||
cmdCons executil.CommandConstructor
|
||||
|
||||
// name is the launchd service name.
|
||||
name string
|
||||
|
||||
// plistDir is the path to the directory that contains launchd plist files.
|
||||
plistDir string
|
||||
}
|
||||
|
||||
// darwinService is a wrapper for a darwin [service.Service] that enriches the
|
||||
// service status information.
|
||||
type darwinService struct {
|
||||
service.Service
|
||||
logger *slog.Logger
|
||||
cmdCons executil.CommandConstructor
|
||||
name string
|
||||
plistDir string
|
||||
}
|
||||
|
||||
// newDarwinService returns properly initialized *darwinService. c must be
|
||||
// non-nil and valid.
|
||||
func newDarwinService(c *darwinServiceConfig) (d *darwinService) {
|
||||
return &darwinService{
|
||||
Service: c.svc,
|
||||
logger: c.logger,
|
||||
cmdCons: c.cmdCons,
|
||||
name: c.name,
|
||||
plistDir: c.plistDir,
|
||||
}
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ service.Service = (*darwinService)(nil)
|
||||
|
||||
// Status implements the [service.Service] interface for *darwinService.
|
||||
func (d *darwinService) Status() (status service.Status, err error) {
|
||||
// TODO(f.setrakov): Pass context.
|
||||
ctx := context.TODO()
|
||||
|
||||
if !d.isInstalled(ctx) {
|
||||
return service.StatusUnknown, service.ErrNotInstalled
|
||||
}
|
||||
|
||||
const launchctlCmd = "launchctl"
|
||||
var (
|
||||
launchctlArgs = []string{"list", d.name}
|
||||
launchctlStdout bytes.Buffer
|
||||
)
|
||||
|
||||
err = executil.Run(ctx, d.cmdCons, &executil.CommandConfig{
|
||||
Path: launchctlCmd,
|
||||
Args: launchctlArgs,
|
||||
Stdout: &launchctlStdout,
|
||||
})
|
||||
if err != nil {
|
||||
return service.StatusStopped, nil
|
||||
}
|
||||
|
||||
return parseLaunchctlList(&launchctlStdout)
|
||||
}
|
||||
|
||||
// isInstalled returns true if there is actual service .plist file.
|
||||
func (d *darwinService) isInstalled(ctx context.Context) (ok bool) {
|
||||
plistPath := path.Join(d.plistDir, d.name+".plist")
|
||||
_, err := os.Stat(plistPath)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
d.logger.WarnContext(ctx, "checking plist file", slogutil.KeyError, err)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// propNamePID represents the PID prop name in the launchctl list output.
|
||||
const propNamePID = `"PID"`
|
||||
|
||||
// parseLaunchctlList parses the output of the launchctl list command. It
|
||||
// expects that output contains default launchctl tree-like output with
|
||||
// prop=value pairs.
|
||||
func parseLaunchctlList(output io.Reader) (status service.Status, err error) {
|
||||
scanner := bufio.NewScanner(output)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
propName, propValue, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
propName = strings.TrimSpace(propName)
|
||||
propValue = strings.TrimSpace(propValue)
|
||||
|
||||
if propName == propNamePID && propValue != "" {
|
||||
return service.StatusRunning, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return service.StatusUnknown, err
|
||||
}
|
||||
|
||||
return statusRestartOnFail, nil
|
||||
}
|
||||
113
internal/ossvc/service_darwin_internal_test.go
Normal file
113
internal/ossvc/service_darwin_internal_test.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
//go:build darwin
|
||||
|
||||
package ossvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||
"github.com/AdguardTeam/golibs/osutil/executil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/golibs/testutil/fakeos/fakeexec"
|
||||
"github.com/kardianos/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newTestCmdConstructor is a helper that creates a new command constructor. The
|
||||
// returned constructor creates [fakeexec.Command] instances that print the
|
||||
// given body to the command's standard output and return the error.
|
||||
func newTestCmdConstructor(
|
||||
tb testing.TB,
|
||||
body string,
|
||||
returnErr error,
|
||||
) (c executil.CommandConstructor) {
|
||||
tb.Helper()
|
||||
|
||||
onNew := func(
|
||||
_ context.Context,
|
||||
conf *executil.CommandConfig,
|
||||
) (c executil.Command, err error) {
|
||||
cmd := fakeexec.NewCommand()
|
||||
cmd.OnStart = func(_ context.Context) (err error) {
|
||||
_, err = io.WriteString(conf.Stdout, body)
|
||||
require.NoError(tb, err)
|
||||
|
||||
return returnErr
|
||||
}
|
||||
|
||||
cmd.OnWait = func(_ context.Context) (err error) { return nil }
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
return &fakeexec.CommandConstructor{
|
||||
OnNew: onNew,
|
||||
}
|
||||
}
|
||||
|
||||
func TestDarwinService_Status(t *testing.T) {
|
||||
name := "AdGuardHome"
|
||||
plistDir := t.TempDir()
|
||||
svc := newDarwinService(&darwinServiceConfig{
|
||||
logger: slogutil.NewDiscardLogger(),
|
||||
plistDir: plistDir,
|
||||
name: name,
|
||||
})
|
||||
|
||||
t.Run("not_installed", func(t *testing.T) {
|
||||
status, err := svc.Status()
|
||||
assert.Equal(t, service.StatusUnknown, status)
|
||||
assert.ErrorIs(t, err, service.ErrNotInstalled)
|
||||
})
|
||||
|
||||
plistPath := path.Join(plistDir, name+".plist")
|
||||
file, err := os.Create(plistPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
testutil.CleanupAndRequireSuccess(t, file.Close)
|
||||
|
||||
testCases := []struct {
|
||||
cmdErr error
|
||||
name string
|
||||
body string
|
||||
wantStatus service.Status
|
||||
}{{
|
||||
name: "running",
|
||||
body: `
|
||||
{
|
||||
"PID" = 12345;
|
||||
};`,
|
||||
cmdErr: nil,
|
||||
wantStatus: service.StatusRunning,
|
||||
}, {
|
||||
name: "restarting",
|
||||
body: `
|
||||
{
|
||||
"foo" = "bar";
|
||||
};`,
|
||||
cmdErr: nil,
|
||||
wantStatus: statusRestartOnFail,
|
||||
}, {
|
||||
name: "stopped",
|
||||
body: "",
|
||||
cmdErr: errors.Error("launchctl error"),
|
||||
wantStatus: service.StatusStopped,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
svc.cmdCons = newTestCmdConstructor(t, tc.body, tc.cmdErr)
|
||||
var status service.Status
|
||||
status, err = svc.Status()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantStatus, status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
|
|
@ -16,8 +17,8 @@ import (
|
|||
)
|
||||
|
||||
// chooseSystem checks the current system detected and substitutes it with local
|
||||
// implementation if needed.
|
||||
func chooseSystem() {
|
||||
// implementation if needed. l must not be nil.
|
||||
func chooseSystem(ctx context.Context, l *slog.Logger) {
|
||||
sys := service.ChosenSystem()
|
||||
switch sys.String() {
|
||||
case "unix-systemv":
|
||||
|
|
@ -29,12 +30,18 @@ func chooseSystem() {
|
|||
// https://github.com/AdguardTeam/AdGuardHome/issues/4677.
|
||||
if !aghos.IsOpenWrt() {
|
||||
service.ChooseSystem(&sysvSystem{System: sys})
|
||||
l.DebugContext(ctx, "using custom SysV system")
|
||||
|
||||
return
|
||||
}
|
||||
case "linux-systemd":
|
||||
service.ChooseSystem(&systemdSystem{System: sys})
|
||||
default:
|
||||
// Do nothing.
|
||||
l.DebugContext(ctx, "using custom systemd system")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
l.DebugContext(ctx, "using default system", "system_description", sys.String())
|
||||
}
|
||||
|
||||
// sysvSystem is a wrapper for a [service.System] that returns the custom
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
|
|
@ -34,7 +35,7 @@ const sysVersion = "openbsd-runcom"
|
|||
|
||||
// chooseSystem checks the current system detected and substitutes it with local
|
||||
// implementation if needed.
|
||||
func chooseSystem() {
|
||||
func chooseSystem(_ context.Context, _ *slog.Logger) {
|
||||
service.ChooseSystem(openbsdSystem{})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
//go:build !openbsd && !linux
|
||||
//go:build !openbsd && !linux && !darwin
|
||||
|
||||
package ossvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// chooseSystem checks the current system detected and substitutes it with local
|
||||
// implementation if needed.
|
||||
func chooseSystem() {}
|
||||
func chooseSystem(_ context.Context, _ *slog.Logger) {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue