feat(systemd): Add support for watchdog timer

Signed-off-by: Steffen Vogel <post@steffenvogel.de>
This commit is contained in:
Steffen Vogel
2024-12-24 12:10:38 +01:00
parent a73ee65427
commit c3a6b6e2bb
4 changed files with 177 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ import (
"cunicu.li/cunicu/pkg/device"
"cunicu.li/cunicu/pkg/log"
osx "cunicu.li/cunicu/pkg/os"
"cunicu.li/cunicu/pkg/os/systemd"
"cunicu.li/cunicu/pkg/signaling"
"cunicu.li/cunicu/pkg/wg"
)
@@ -109,6 +110,11 @@ func (d *Daemon) Start() error {
signals := osx.SetupSignals(osx.SigUpdate)
wdt, err := d.watchdogTicker()
if err != nil && !errors.Is(err, errNotSupported) {
return fmt.Errorf("failed to get watchdog interval: %w", err)
}
if err := d.setState(StateReady); err != nil {
return fmt.Errorf("failed transition state: %w", err)
}
@@ -127,6 +133,12 @@ out:
break out
}
case <-wdt:
if err := d.notify(systemd.NotifyWatchdog); err != nil {
return fmt.Errorf("failed to notify systemd watchdog: %w", err)
}
d.logger.DebugV(20, "Watchdog tick")
case <-d.stop:
break out
}
@@ -252,6 +264,18 @@ func (d *Daemon) CreateDevices() error {
return nil
}
func (d *Daemon) watchdogTicker() (<-chan time.Time, error) {
wdInterval, err := systemd.WatchdogEnabled(true)
if err != nil {
return nil, err
} else if wdInterval == 0 {
d.logger.DebugV(5, "Not started via systemd. Disabling watchdog")
return nil, errNotSupported
}
return time.NewTicker(wdInterval / 2).C, nil
}
func (d *Daemon) setState(s State) error {
d.state = s

View File

@@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2015 CoreOS, Inc.
// SPDX-FileCopyrightText: 2023-2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
//go:build linux
package systemd
import (
"errors"
"fmt"
"os"
"strconv"
"time"
)
var ErrNegativeWatchdogInterval = errors.New("WATCHDOG_USEC must be a positive number")
// WatchdogEnabled returns watchdog information for a service.
// Processes should call daemon.SdNotify(false, daemon.SdNotifyWatchdog) every
// time / 2.
// If `unsetEnv` is true, the environment variables `WATCHDOG_USEC` and
// `WATCHDOG_PID` will be unconditionally unset.
//
// It returns one of the following:
// (0, nil) - watchdog isn't enabled or we aren't the watched PID.
// (0, err) - an error happened (e.g. error converting time).
// (time, nil) - watchdog is enabled and we can send ping. time is delay
// before inactive service will be killed.
func WatchdogEnabled(unsetEnv bool) (interval time.Duration, err error) {
wUSec := os.Getenv("WATCHDOG_USEC")
wPID := os.Getenv("WATCHDOG_PID")
if unsetEnv {
wUSecErr := os.Unsetenv("WATCHDOG_USEC")
wPIDErr := os.Unsetenv("WATCHDOG_PID")
if wUSecErr != nil {
return 0, wUSecErr
}
if wPIDErr != nil {
return 0, wPIDErr
}
}
if wUSec == "" {
return 0, nil
}
s, err := strconv.Atoi(wUSec)
if err != nil {
return 0, fmt.Errorf("failed to convert WATCHDOG_USEC: %w", err)
} else if s <= 0 {
return 0, ErrNegativeWatchdogInterval
}
interval = time.Duration(s) * time.Microsecond
if wPID == "" {
return interval, nil
}
if p, err := strconv.Atoi(wPID); err != nil {
return 0, fmt.Errorf("failed to convert WATCHDOG_PID: %w", err)
} else if os.Getpid() != p {
return 0, nil
}
return interval, nil
}

View File

@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2023-2024 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
//go:build !linux
package systemd
import "time"
func WatchdogEnabled(_ bool) (interval time.Duration, err error) {
return 0, nil
}

View File

@@ -0,0 +1,72 @@
// 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 (
"os"
"strconv"
"time"
"cunicu.li/cunicu/pkg/os/systemd"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Context("Watchdog", func() {
myPID := strconv.Itoa(os.Getpid())
DescribeTable("enabled", func(usec string, pid string, unsetEnv bool, expectedErr error, expectedDelay time.Duration) {
if usec != "" {
err := os.Setenv("WATCHDOG_USEC", usec)
Expect(err).To(Succeed())
} else {
err := os.Unsetenv("WATCHDOG_USEC")
Expect(err).To(Succeed())
}
if pid != "" {
err := os.Setenv("WATCHDOG_PID", pid)
Expect(err).To(Succeed())
} else {
err := os.Unsetenv("WATCHDOG_PID")
Expect(err).To(Succeed())
}
delay, err := systemd.WatchdogEnabled(unsetEnv)
Expect(delay).To(Equal(expectedDelay))
if expectedErr != nil {
Expect(err).To(MatchError(expectedErr))
} else {
Expect(err).To(Succeed())
}
if unsetEnv {
Expect(os.Getenv("WATCHDOG_PID")).To(BeEmpty())
Expect(os.Getenv("WATCHDOG_USEC")).To(BeEmpty())
}
},
// Success cases
Entry(nil, "100", myPID, true, nil, 100*time.Microsecond),
Entry(nil, "50", myPID, true, nil, 50*time.Microsecond),
Entry(nil, "1", myPID, false, nil, 1*time.Microsecond),
Entry(nil, "1", "", true, nil, 1*time.Microsecond),
// No-op cases
Entry("WATCHDOG_USEC not set", "", myPID, true, nil, time.Duration(0)),
Entry("WATCHDOG_PID doesn't match", "1", "0", false, nil, time.Duration(0)),
Entry("Both not set", "", "", true, nil, time.Duration(0)),
// Failure cases
Entry("Negative USEC", "-1", myPID, true, systemd.ErrNegativeWatchdogInterval, time.Duration(0)),
Entry("Non-integer USEC value", "string", "1", false, strconv.ErrSyntax, time.Duration(0)),
Entry("Non-integer PID value", "1", "string", true, strconv.ErrSyntax, time.Duration(0)),
Entry("Everything wrong", "stringa", "stringb", false, strconv.ErrSyntax, time.Duration(0)),
Entry("Everything wrong", "-10239", "-eleventythree", true, systemd.ErrNegativeWatchdogInterval, time.Duration(0)),
)
})