mirror of
https://codeberg.org/cunicu/cunicu.git
synced 2025-09-26 21:01:14 +08:00
feat(systemd): Add support for sd_notify protocol
Signed-off-by: Steffen Vogel <post@steffenvogel.de>
This commit is contained in:
@@ -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
25
pkg/os/systemd/notify.go
Normal 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"
|
||||
)
|
48
pkg/os/systemd/notify_linux.go
Normal file
48
pkg/os/systemd/notify_linux.go
Normal 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
|
||||
}
|
10
pkg/os/systemd/notify_others.go
Normal file
10
pkg/os/systemd/notify_others.go
Normal 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
|
||||
}
|
65
pkg/os/systemd/notify_test.go
Normal file
65
pkg/os/systemd/notify_test.go
Normal 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
11
pkg/os/systemd/systemd.go
Normal 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
|
17
pkg/os/systemd/systemd_test.go
Normal file
17
pkg/os/systemd/systemd_test.go
Normal 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")
|
||||
}
|
Reference in New Issue
Block a user