Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

daemon: add ability to use syscall based reboot #250

Merged
merged 11 commits into from
Aug 11, 2023
Merged
48 changes: 41 additions & 7 deletions internals/daemon/daemon.go
Original file line number Diff line number Diff line change
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,11 +700,11 @@ 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
logger.Noticef("Waiting for system reboot")
logger.Noticef("Waiting for system reboot...")
if sigCh != nil {
signal.Stop(sigCh)
if len(sigCh) > 0 {
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"
const rebootMsg = "reboot scheduled to update the system"

func rebootImpl(rebootDelay time.Duration) error {
var rebootHandler = commandReboot

// SetSyscallReboot replaces the default command-based reboot
// with a direct Linux kernel syscall based implementation.
func SetSyscallReboot() {
flotter marked this conversation as resolved.
Show resolved Hide resolved
rebootHandler = syscallReboot
}

// 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 (
syncSyscall = syscall.Sync
rebootSyscall = syscall.Reboot
flotter marked this conversation as resolved.
Show resolved Hide resolved
)

// syscallReboot performs a delayed async reboot using direct Linux
// kernel syscalls.
//
// Note: Reboot message not currently supported.
func syscallReboot(rebootDelay time.Duration) error {
flotter marked this conversation as resolved.
Show resolved Hide resolved
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, func() {
// As per the requirements of the reboot syscall, we
// have to first call sync.
syncSyscall()
err := rebootSyscall(syscall.LINUX_REBOOT_CMD_RESTART)
if err != nil {
logger.Noticef("Failed on reboot syscall: %v", err)
}
})
return nil
}

func (d *Daemon) Dying() <-chan struct{} {
return d.tomb.Dying()
Expand Down
141 changes: 137 additions & 4 deletions internals/daemon/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ import (
"time"

"github.com/gorilla/mux"

"gopkg.in/check.v1"

// XXX Delete import above and make this file like the other ones.
. "gopkg.in/check.v1"

"github.com/canonical/pebble/internals/logger"
"github.com/canonical/pebble/internals/osutil"
"github.com/canonical/pebble/internals/overlord/patch"
"github.com/canonical/pebble/internals/overlord/restart"
Expand Down 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,136 @@ services:
c.Assert(tasks, HasLen, 1)
c.Check(tasks[0].Kind(), Equals, "stop")
}

type rebootSuite struct{}

var _ = check.Suite(&rebootSuite{})

func mockSyncSyscall(f func()) (restore func()) {
flotter marked this conversation as resolved.
Show resolved Hide resolved
old := syncSyscall
syncSyscall = f
return func() {
syncSyscall = old
}
}

func mockRebootSyscall(f func(cmd int) error) (restore func()) {
old := rebootSyscall
rebootSyscall = f
return func() {
rebootSyscall = old
}
}

func (s *rebootSuite) TestSyscallPosRebootDelay(c *C) {
wait := make(chan int)
defer mockSyncSyscall(func() {})()
defer mockRebootSyscall(func(cmd int) error {
if cmd == syscall.LINUX_REBOOT_CMD_RESTART {
wait <- 1
}
return nil
})()

period := 25 * time.Millisecond
syscallReboot(period)
start := time.Now()
select {
case <-wait:
case <-time.After(10 * time.Second):
c.Fatal("syscall did not take place and we timed out")
}
elapsed := time.Now().Sub(start)
c.Assert(elapsed >= period, Equals, true)
}

func (s *rebootSuite) TestSyscallNegRebootDelay(c *C) {
wait := make(chan int)
defer mockSyncSyscall(func() {})()
defer mockRebootSyscall(func(cmd int) error {
if cmd == syscall.LINUX_REBOOT_CMD_RESTART {
wait <- 1
}
return nil
})()

// Negative periods will be zeroed, so do not fear the huge negative.
// We do supply a rather big value here because this test is
// effectively a race, but given the huge timeout, it is not going
// to be a problem (c).
period := 10 * time.Second
syscallReboot(-period)
start := time.Now()
select {
case <-wait:
case <-time.After(10 * time.Second):
c.Fatal("syscall did not take place and we timed out")
}
elapsed := time.Now().Sub(start)
c.Assert(elapsed < period, Equals, true)
}

func (s *rebootSuite) TestSetSyscall(c *C) {
wait := make(chan int)
defer mockSyncSyscall(func() {})()
defer mockRebootSyscall(func(cmd int) error {
if cmd == syscall.LINUX_REBOOT_CMD_RESTART {
wait <- 1
}
return nil
})()

// We know the default is commandReboot otherwise the unit tests
// above will fail. We need to check the switch works.
SetSyscallReboot()
defer func() {
rebootHandler = commandReboot
}()

err := rebootHandler(0)
c.Assert(err, IsNil)
select {
case <-wait:
case <-time.After(10 * time.Second):
c.Fatal("syscall did not take place and we timed out")
}
}

type fakeLogger struct {
msg string
noticeCh chan int
}

func (f *fakeLogger) Notice(msg string) {
f.msg = msg
f.noticeCh <- 1
}

func (f *fakeLogger) Debug(msg string) {}

func (s *rebootSuite) TestSyscallRebootError(c *C) {
defer mockSyncSyscall(func() {})()
defer mockRebootSyscall(func(cmd int) error {
return fmt.Errorf("-EPERM")
})()

// We know the default is commandReboot otherwise the unit tests
// above will fail. We need to check the switch works.
SetSyscallReboot()
defer func() {
rebootHandler = commandReboot
}()
complete := make(chan int)
l := fakeLogger{noticeCh: complete}
old := logger.SetLogger(&l)
defer logger.SetLogger(old)

err := rebootHandler(0)
c.Assert(err, IsNil)
select {
case <-complete:
case <-time.After(10 * time.Second):
c.Fatal("syscall did not take place and we timed out")
}
c.Assert(l.msg, Matches, "*-EPERM")
}