From ff1f02092c5399998b760d36bce1487302357b01 Mon Sep 17 00:00:00 2001 From: Leorize Date: Thu, 15 May 2025 11:17:56 -0500 Subject: [PATCH] home: add service ready notification support on Linux On systemd-based Linux systems, signal service readiness once the web service is up and running. This allows depending services to accurately rely on AdGuardHome being active and ready to service. This feature is particularly useful with Podman, as the container manager can verify that the service can start correctly after an auto-update operation. This new feature does not do anything on non-systemd Linux or other operating systems. --- internal/home/ready_linux.go | 49 +++++++++++++++++++++++++++++++++++ internal/home/ready_others.go | 14 ++++++++++ internal/home/service.go | 4 +++ internal/home/web.go | 14 +++++++++- 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 internal/home/ready_linux.go create mode 100644 internal/home/ready_others.go diff --git a/internal/home/ready_linux.go b/internal/home/ready_linux.go new file mode 100644 index 00000000000..7ec9311a501 --- /dev/null +++ b/internal/home/ready_linux.go @@ -0,0 +1,49 @@ +//go:build linux + +package home + +import ( + "fmt" + "net" + "os" + "time" +) + +// Notifies the service manager that the program is ready to serve +func notifyReady() error { + return sdNotify("READY=1") +} + +// Notifies the service manager that the program is beginning to reload its +// configuration +func notifyReload() error { + now := time.Now().UnixMicro() + return sdNotify(fmt.Sprintf("RELOADING=1\nMONOTONIC_USEC=%v", now)) +} + +// Implements the sd_notify mechanism +// +// Reference: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html +func sdNotify(message string) error { + socketPath := os.Getenv("NOTIFY_SOCKET") + if socketPath == "" { + return nil + } + socketAddr := net.UnixAddr{ + Name: socketPath, + Net: "unixgram", + } + + conn, err := net.DialUnix("unixgram", nil, &socketAddr) + if err != nil { + return fmt.Errorf("connecting to %q: %w", socketAddr.String(), err) + } + defer conn.Close() + + _, err = conn.Write([]byte(message)) + if err != nil { + return fmt.Errorf("sending %q: %w", message, err) + } + + return nil +} diff --git a/internal/home/ready_others.go b/internal/home/ready_others.go new file mode 100644 index 00000000000..b095abe54fe --- /dev/null +++ b/internal/home/ready_others.go @@ -0,0 +1,14 @@ +//go:build !linux + +package home + +// Notifies the service manager that the program is ready to serve +func notifyReady() error { + return nil +} + +// Notifies the service manager that the program is beginning to reload its +// configuration +func notifyReload() error { + return nil +} diff --git a/internal/home/service.go b/internal/home/service.go index 7075e5d10bc..12b1c5990c8 100644 --- a/internal/home/service.go +++ b/internal/home/service.go @@ -466,6 +466,9 @@ var launchdConfig = ` // 2. The StandardOutput and StandardError settings are set to redirect the // output to the systemd journal, see // https://man7.org/linux/man-pages/man5/systemd.exec.5.html#LOGGING_AND_STANDARD_INPUT/OUTPUT. +// +// 3. The Type setting has been configured to enable service readiness +// notification support. const systemdScript = `[Unit] Description={{.Description}} ConditionFileIsExecutable={{.Path|cmdEscape}} @@ -473,6 +476,7 @@ ConditionFileIsExecutable={{.Path|cmdEscape}} {{$dep}} {{end}} [Service] +Type=notify StartLimitInterval=5 StartLimitBurst=10 ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}} diff --git a/internal/home/web.go b/internal/home/web.go index 9de27782e0f..02985243ec3 100644 --- a/internal/home/web.go +++ b/internal/home/web.go @@ -235,7 +235,13 @@ func (web *webAPI) start(ctx context.Context) { errs <- web.httpServer.ListenAndServe() }() - err := <-errs + // Tell the service manager that we are ready to serve requests + err := notifyReady() + if err != nil { + logger.ErrorContext(ctx, "sending service ready notification: %v", slogutil.KeyError, err) + } + + err = <-errs if !errors.Is(err, http.ErrServerClosed) { cleanupAlways() panic(err) @@ -243,6 +249,12 @@ func (web *webAPI) start(ctx context.Context) { // We use ErrServerClosed as a sign that we need to rebind on a new // address, so go back to the start of the loop. + // + // Let the service manager know that we are reloading + err = notifyReload() + if err != nil { + logger.ErrorContext(ctx, "sending service reload notification: %v", slogutil.KeyError, err) + } } }