diff --git a/bridgev2/matrix/mxmain/main.go b/bridgev2/matrix/mxmain/main.go index f8303237..1467e13f 100644 --- a/bridgev2/matrix/mxmain/main.go +++ b/bridgev2/matrix/mxmain/main.go @@ -115,7 +115,11 @@ func (br *BridgeMain) Run() { br.PreInit() br.Init() br.Start() + sdNotifyReady(br.Log) + stopWatchdog := startSdWatchdog(br.Log) exitCode := br.WaitForInterrupt() + sdNotifyStopping(br.Log) + stopWatchdog() br.Stop() os.Exit(exitCode) } diff --git a/bridgev2/matrix/mxmain/systemd.go b/bridgev2/matrix/mxmain/systemd.go new file mode 100644 index 00000000..82573edf --- /dev/null +++ b/bridgev2/matrix/mxmain/systemd.go @@ -0,0 +1,84 @@ +// Copyright (c) 2026 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mxmain + +import ( + "time" + + "github.com/coreos/go-systemd/v22/daemon" + "github.com/rs/zerolog" +) + +// sdNotifyReady tells the systemd service manager that the bridge has finished +// starting up. The call is a no-op when the bridge is not running under a +// systemd unit with `Type=notify`. +// +// See https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html. +func sdNotifyReady(log *zerolog.Logger) { + if sent, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil { + log.Warn().Err(err).Msg("Failed to send systemd READY=1 notification") + } else if sent { + log.Debug().Msg("Sent systemd READY=1 notification") + } +} + +// sdNotifyStopping tells the systemd service manager that the bridge is +// shutting down. The call is a no-op outside of systemd. +func sdNotifyStopping(log *zerolog.Logger) { + if sent, err := daemon.SdNotify(false, daemon.SdNotifyStopping); err != nil { + log.Warn().Err(err).Msg("Failed to send systemd STOPPING=1 notification") + } else if sent { + log.Debug().Msg("Sent systemd STOPPING=1 notification") + } +} + +// startSdWatchdog pings the systemd watchdog at half the configured interval +// for as long as the returned stop function has not been called. The returned +// stop function blocks until the watchdog goroutine has exited. +// +// Returns a no-op stop function when the process is not running under a +// systemd unit with `WatchdogSec=` set (so the call is always safe). +func startSdWatchdog(log *zerolog.Logger) (stop func()) { + interval, err := daemon.SdWatchdogEnabled(false) + if err != nil { + log.Warn().Err(err).Msg("Failed to query systemd watchdog setting") + return func() {} + } + if interval == 0 { + // No watchdog configured by the unit file. + return func() {} + } + // systemd recommends pinging at half the configured interval to leave + // headroom for scheduling jitter. + ping := interval / 2 + log.Info(). + Stringer("watchdog_interval", interval). + Stringer("ping_interval", ping). + Msg("Starting systemd watchdog pinger") + + done := make(chan struct{}) + stopped := make(chan struct{}) + go func() { + defer close(stopped) + ticker := time.NewTicker(ping) + defer ticker.Stop() + for { + select { + case <-done: + return + case <-ticker.C: + if _, err := daemon.SdNotify(false, daemon.SdNotifyWatchdog); err != nil { + log.Warn().Err(err).Msg("Failed to send systemd WATCHDOG=1 notification") + } + } + } + }() + return func() { + close(done) + <-stopped + } +} diff --git a/bridgev2/matrix/mxmain/systemd_test.go b/bridgev2/matrix/mxmain/systemd_test.go new file mode 100644 index 00000000..e9fa67cc --- /dev/null +++ b/bridgev2/matrix/mxmain/systemd_test.go @@ -0,0 +1,39 @@ +// Copyright (c) 2026 Tulir Asokan +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package mxmain + +import ( + "os" + "testing" + + "github.com/rs/zerolog" +) + +// TestSdNotifyHelpersAreNoOpWithoutSystemd verifies that the systemd helpers +// are safe to call when the process is not running under a systemd unit (i.e. +// NOTIFY_SOCKET is not set). They must not panic, must not block, and the +// watchdog stop function must be idempotent and return promptly. +func TestSdNotifyHelpersAreNoOpWithoutSystemd(t *testing.T) { + t.Setenv("NOTIFY_SOCKET", "") + t.Setenv("WATCHDOG_USEC", "") + t.Setenv("WATCHDOG_PID", "") + // Some test runners inherit a parent NOTIFY_SOCKET; explicitly unset it + // so the helpers really do see a non-systemd environment. + _ = os.Unsetenv("NOTIFY_SOCKET") + _ = os.Unsetenv("WATCHDOG_USEC") + _ = os.Unsetenv("WATCHDOG_PID") + + log := zerolog.Nop() + sdNotifyReady(&log) + sdNotifyStopping(&log) + + stop := startSdWatchdog(&log) + if stop == nil { + t.Fatal("startSdWatchdog returned a nil stop function") + } + stop() +}