feat(systemd): Add support for sd_notify protocol

Signed-off-by: Steffen Vogel <post@steffenvogel.de>
This commit is contained in:
Steffen Vogel
2024-12-24 12:07:43 +01:00
parent 3ce39138cc
commit a73ee65427
7 changed files with 240 additions and 0 deletions

View File

@@ -24,6 +24,17 @@ var (
ErrFeatureDeactivated = errors.New("feature deactivated")
)
type State string
const (
StateStarted = "started"
StateInitializing = "initializing"
StateReady = "ready"
StateReloading = "reloading"
StateStopping = "stoppping"
StateSynchronizing = "syncing"
)
type Daemon struct {
*Watcher
@@ -33,6 +44,7 @@ type Daemon struct {
devices []device.Device
state State
stop chan any
reexecOnClose bool
@@ -51,6 +63,7 @@ func NewDaemon(cfg *config.Config) (*Daemon, error) {
Config: cfg,
devices: []device.Device{},
stop: make(chan any),
state: StateStarted,
logger: log.Global.Named("daemon"),
}
@@ -76,6 +89,10 @@ func NewDaemon(cfg *config.Config) (*Daemon, error) {
// Start starts the daemon and blocks until Stop() is called.
func (d *Daemon) Start() error {
if err := d.setState(StateInitializing); err != nil {
return fmt.Errorf("failed transition state: %w", err)
}
if err := wg.CleanupUserSockets(); err != nil {
return fmt.Errorf("failed to cleanup stale user space sockets: %w", err)
}
@@ -92,6 +109,10 @@ func (d *Daemon) Start() error {
signals := osx.SetupSignals(osx.SigUpdate)
if err := d.setState(StateReady); err != nil {
return fmt.Errorf("failed transition state: %w", err)
}
out:
for {
select {
@@ -146,6 +167,10 @@ func (d *Daemon) Sync() error {
}
func (d *Daemon) Close() error {
if err := d.setState(StateStopping); err != nil {
return fmt.Errorf("failed transition state: %w", err)
}
if err := d.Watcher.Close(); err != nil {
return fmt.Errorf("failed to close watcher: %w", err)
}
@@ -226,3 +251,42 @@ func (d *Daemon) CreateDevices() error {
return nil
}
func (d *Daemon) setState(s State) error {
d.state = s
d.logger.DebugV(5, "Daemon state changed", zap.String("state", string(s)))
switch d.state {
case StateStarted:
case StateInitializing:
case StateSynchronizing:
case StateReady:
if err := d.notify(systemd.NotifyReady); err != nil {
return fmt.Errorf("failed to notify systemd: %w", err)
}
case StateReloading:
if err := d.notify(systemd.NotifyReloading); err != nil {
return fmt.Errorf("failed to notify systemd: %w", err)
}
case StateStopping:
if err := d.notify(systemd.NotifyStopping); err != nil {
return fmt.Errorf("failed to notify systemd: %w", err)
}
}
return nil
}
func (d *Daemon) notify(notify string) error {
notifyMessages := []string{notify}
if _, err := systemd.Notify(false, strings.Join(notifyMessages, "\n")); err != nil {
return err
}
return nil
}

25
pkg/os/systemd/notify.go Normal file
View File

@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2014 Docker, Inc.
// SPDX-FileCopyrightText: 2015-2018 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
package systemd
const (
// NotifyReady tells the service manager that service startup is finished
// or the service finished loading its configuration.
NotifyReady = "READY=1"
// NotifyStopping tells the service manager that the service is beginning
// its shutdown.
NotifyStopping = "STOPPING=1"
// NotifyReloading tells the service manager that this service is
// reloading its configuration. Note that you must call SdNotifyReady when
// it completed reloading.
NotifyReloading = "RELOADING=1"
// NotifyWatchdog tells the service manager to update the watchdog
// timestamp for the service.
NotifyWatchdog = "WATCHDOG=1"
)

View File

@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2014 Docker, Inc.
// SPDX-FileCopyrightText: 2015-2018 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
package systemd
import (
"net"
"os"
)
// Notify sends a message to the init daemon. It is common to ignore the error.
// If `unsetEnv` is true, the environment variable `NOTIFY_SOCKET` will be
// unconditionally unset.
//
// It returns one of the following:
// (false, nil) - notification not supported (i.e. NOTIFY_SOCKET is unset)
// (false, err) - notification supported, but failure happened (e.g. error connecting to NOTIFY_SOCKET or while sending data)
// (true, nil) - notification supported, data has been sent
func Notify(unsetEnv bool, state string) (bool, error) {
socketAddr := &net.UnixAddr{
Name: os.Getenv("NOTIFY_SOCKET"),
Net: "unixgram",
}
if socketAddr.Name == "" {
return false, nil
}
if unsetEnv {
if err := os.Unsetenv("NOTIFY_SOCKET"); err != nil {
return false, err
}
}
conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
if err != nil {
return false, err
}
defer conn.Close()
if _, err = conn.Write([]byte(state)); err != nil {
return false, err
}
return true, nil
}

View File

@@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023-2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
//go:build !linux
package systemd
func Notify(_ bool, _ string) (bool, error) {
return false, nil
}

View File

@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2016 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
//go:build linux
package systemd_test
import (
"net"
"os"
"path/filepath"
"cunicu.li/cunicu/pkg/os/systemd"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Context("Notify", func() {
var testDir, notifySocket string
var conn *net.UnixConn
var err error
BeforeEach(func() {
testDir = GinkgoT().TempDir()
notifySocket = filepath.Join(testDir, "notify-socket.sock")
conn, err = net.ListenUnixgram("unixgram", &net.UnixAddr{
Name: notifySocket,
Net: "unixgram",
})
Expect(err).To(Succeed())
})
AfterEach(func() {
err = conn.Close()
Expect(err).To(Succeed())
})
DescribeTable("works", func(unsetEnv bool, envCb func() string, exptectSent bool, expectErr error) {
env := envCb()
err = os.Setenv("NOTIFY_SOCKET", env)
Expect(err).To(Succeed())
sent, err := systemd.Notify(unsetEnv, systemd.NotifyReady)
if expectErr != nil {
Expect(err).To(MatchError(err))
} else {
Expect(err).To(Succeed())
}
Expect(sent).To(Equal(exptectSent))
if unsetEnv && env != "" {
Expect(os.Getenv("NOTIFY_SOCKET")).To(BeEmpty())
}
},
Entry("Notification supported, data has been sent: (true, nil)", false, func() string { return notifySocket }, true, nil),
Entry("Notification supported, but failure happened: (false, err)", true, func() string { return filepath.Join(testDir, "missing.sock") }, false, os.ErrClosed),
Entry("Notification not supported: (false, nil)", true, func() string { return "" }, false, nil),
)
})

11
pkg/os/systemd/systemd.go Normal file
View File

@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2023-2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
// Package systemd provides a Go implementation of systemd related protocols.
//
// Currently, the followwing features are supported:
//
// - sd_notify: It can be used to inform systemd of service start-up completion,
// watchdog events, and other status changes.
// See: https://www.freedesktop.org/software/systemd/man/sd_notify.html#Description
package systemd

View File

@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2018 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
package systemd_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestSuite(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "SystemD Suite")
}