Skip to content

Commit

Permalink
daemon: add ability to use syscall based reboot
Browse files Browse the repository at this point in the history
Reboot in the daemon is currently relying on userspace having a
shutdown command. This may not always be the case, as Pebble is
used in a wide variety of environments.

Add a SetSyscallReboot() function to the daemon to allow the daemon
to be initialised with a syscall based reboot implementation.
  • Loading branch information
flotter committed Jun 29, 2023
1 parent a807026 commit a3aad05
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 12 deletions.
52 changes: 43 additions & 9 deletions internals/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ import (
"runtime"
"strings"
"sync"
"syscall"
"time"

"golang.org/x/sys/unix"
"gopkg.in/tomb.v2"

"github.com/gorilla/mux"
Expand Down Expand Up @@ -515,7 +515,7 @@ func (d *Daemon) HandleRestart(t restart.RestartType) {
case restart.RestartSystem:
// try to schedule a fallback slow reboot already here
// in case we get stuck shutting down
if err := reboot(rebootWaitTimeout); err != nil {
if err := rebootHandler(rebootWaitTimeout); err != nil {
logger.Noticef("%s", err)
}

Expand Down Expand Up @@ -700,7 +700,7 @@ func (d *Daemon) doReboot(sigCh chan<- os.Signal, waitTimeout time.Duration) err
}
// ask for shutdown and wait for it to happen.
// if we exit, pebble will be restarted by systemd
if err := reboot(rebootDelay); err != nil {
if err := rebootHandler(rebootDelay); err != nil {
return err
}
// wait for reboot to happen
Expand All @@ -717,21 +717,55 @@ func (d *Daemon) doReboot(sigCh chan<- os.Signal, waitTimeout time.Duration) err
return fmt.Errorf("expected reboot did not happen")
}

var shutdownMsg = "reboot scheduled to update the system"
var (
rebootMsg = "reboot scheduled to update the system"
rebootHandler = commandReboot
)

// SetSyscallReboot replaces the default command-based reboot
// with a direct Linux kernel syscall based implementation.
func SetSyscallReboot() {
rebootHandler = syscallReboot
}

func rebootImpl(rebootDelay time.Duration) error {
// commandReboot assumes a userspace shutdown command exists.
func commandReboot(rebootDelay time.Duration) error {
if rebootDelay < 0 {
rebootDelay = 0
}
mins := int64(rebootDelay / time.Minute)
cmd := exec.Command("shutdown", "-r", fmt.Sprintf("+%d", mins), shutdownMsg)
cmd := exec.Command("shutdown", "-r", fmt.Sprintf("+%d", mins), rebootMsg)
if out, err := cmd.CombinedOutput(); err != nil {
return osutil.OutputErr(out, err)
}
return nil
}

var reboot = rebootImpl
var shutdownSyscall = func() {
// As per the requirements of the reboot syscall, we have to
// first call sync.
unix.Sync()
// This syscall can fail (EINVAL) if invalid arguments are
// supplied, which should not be the case, but let's panic if
// that were ever to be true just to be sure we catch it.
err := unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART)
if err != nil {
panic("internal error: reboot syscall failed")
}
}

// syscallReboot performs a reboot using direct Linux kernel syscalls.
//
// Note: Reboot message not currently supported.
func syscallReboot(rebootDelay time.Duration) error {
if rebootDelay < 0 {
rebootDelay = 0
}
// This has to be non-blocking, and scheduled for a future
// point in time to mimic shutdown.
time.AfterFunc(rebootDelay, shutdownSyscall)
return nil
}

func (d *Daemon) Dying() <-chan struct{} {
return d.tomb.Dying()
Expand Down Expand Up @@ -829,9 +863,9 @@ func getListener(socketPath string, listenerMap map[string]net.Listener) (net.Li
}

runtime.LockOSThread()
oldmask := syscall.Umask(0111)
oldmask := unix.Umask(0111)
listener, err := net.ListenUnix("unix", address)
syscall.Umask(oldmask)
unix.Umask(oldmask)
runtime.UnlockOSThread()
if err != nil {
return nil, err
Expand Down
38 changes: 35 additions & 3 deletions internals/daemon/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -665,15 +665,15 @@ func (s *daemonSuite) TestRestartSystemWiring(c *check.C) {
oldRebootNoticeWait := rebootNoticeWait
oldRebootWaitTimeout := rebootWaitTimeout
defer func() {
reboot = rebootImpl
rebootHandler = commandReboot
rebootNoticeWait = oldRebootNoticeWait
rebootWaitTimeout = oldRebootWaitTimeout
}()
rebootWaitTimeout = 100 * time.Millisecond
rebootNoticeWait = 150 * time.Millisecond

var delays []time.Duration
reboot = func(d time.Duration) error {
rebootHandler = func(d time.Duration) error {
delays = append(delays, d)
return nil
}
Expand Down Expand Up @@ -740,7 +740,7 @@ func (s *daemonSuite) TestRebootHelper(c *check.C) {
}

for _, t := range tests {
err := reboot(t.delay)
err := rebootHandler(t.delay)
c.Assert(err, check.IsNil)
c.Check(cmd.Calls(), check.DeepEquals, [][]string{
{"shutdown", "-r", t.delayArg, "reboot scheduled to update the system"},
Expand Down Expand Up @@ -1148,3 +1148,35 @@ services:
c.Assert(tasks, HasLen, 1)
c.Check(tasks[0].Kind(), Equals, "stop")
}

func (s *daemonSuite) TestSyscallRebootDelay(c *C) {
waitState := 0
old := shutdownSyscall
shutdownSyscall = func() {
waitState = 1
}
defer func() {
shutdownSyscall = old
}()
syscallReboot(time.Millisecond * 25)
c.Assert(waitState, Equals, 0)
time.Sleep(time.Millisecond * 50)
c.Assert(waitState, Equals, 1)
}

func (s *daemonSuite) TestSetSyscall(c *C) {
check := 0
old := shutdownSyscall
shutdownSyscall = func() {
check = 1
}
defer func() {
shutdownSyscall = old
}()
// We know the default is commandReboot otherwise the unit tests
// above will fail. We need to check the switch works.
SetSyscallReboot()
rebootHandler(0)
time.Sleep(time.Millisecond * 50)
c.Assert(check, Equals, 1)
}

0 comments on commit a3aad05

Please sign in to comment.