Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion internal/init/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,21 @@ func boot(ctx context.Context, shutdown *shutdownRequests) (err error) {
// polls the local socket and redraws the node status frame. A console crash
// is non-critical: superviseConsole restarts it and never returns an error
// that would reboot a serving CA.
go superviseConsole(ctx)
//
// It runs under its own cancelable context rather than ctx directly, so a
// shutdown can stop it for good -- and confirm it has actually exited --
// before the shutdown message is written to the same console (#370).
consoleCtx, cancelConsole := context.WithCancel(ctx)
defer cancelConsole()
consoleDone := make(chan struct{})
go func() {
defer close(consoleDone)
superviseConsole(consoleCtx)
}()
shutdown.stopConsole = func() {
cancelConsole()
<-consoleDone
}

// 14. Park until a shutdown is requested, then return so the deferred
// teardown runs before PID 1 reboots or powers off. SIGINT must be handled
Expand Down
13 changes: 13 additions & 0 deletions internal/init/shutdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ type shutdownRequests struct {
// is happening instead of freezing on its last frame.
announce func(ShutdownAction)

// stopConsole stops the console dashboard for good and blocks until it has
// actually exited. Wait calls it before announce, so the dashboard's own
// refresh loop can never redraw a frame over the shutdown message (#370).
stopConsole func()

mu sync.Mutex
chosen ShutdownAction
}
Expand Down Expand Up @@ -145,6 +150,11 @@ func (r *shutdownRequests) Request(a ShutdownAction) {
// for Action, and arms the teardown watchdog before returning, so the
// teardown that follows cannot hang the node. An ended context counts as a
// reboot.
//
// The console is stopped, and waited for, before the shutdown message is
// announced: announcing first and stopping the console after would leave a
// window where the dashboard's own refresh loop redraws its ordinary status
// frame over the message that was just written (#370).
func (r *shutdownRequests) Wait(ctx context.Context) ShutdownAction {
a := ShutdownReboot
select {
Expand All @@ -154,6 +164,9 @@ func (r *shutdownRequests) Wait(ctx context.Context) ShutdownAction {
r.mu.Lock()
r.chosen = a
r.mu.Unlock()
if r.stopConsole != nil {
r.stopConsole()
}
if r.announce != nil {
r.announce(a)
}
Expand Down
26 changes: 26 additions & 0 deletions internal/init/shutdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,32 @@ func TestShutdownRequests_WaitAnnouncesTheAction(t *testing.T) {
}
}

// Wait stops the console dashboard for good, and waits for it to have
// actually stopped, before announcing the shutdown message. Otherwise the
// dashboard's own refresh loop can redraw its ordinary status frame over the
// shutdown message after it is written (#370).
func TestShutdownRequests_WaitStopsTheConsoleBeforeAnnouncing(t *testing.T) {
sd := testShutdownRequests(nil)
var order []string
sd.stopConsole = func() { order = append(order, "stop") }
sd.announce = func(ShutdownAction) { order = append(order, "announce") }
sd.Request(ShutdownReboot)

sd.Wait(context.Background())
if !slices.Equal(order, []string{"stop", "announce"}) {
t.Fatalf("order = %v, want [stop announce]", order)
}
}

// A nil stopConsole (the zero value, before run.go wires one up) must not
// panic Wait.
func TestShutdownRequests_WaitToleratesNoStopConsole(t *testing.T) {
sd := testShutdownRequests(nil)
sd.Request(ShutdownReboot)

sd.Wait(context.Background())
}

// The console message names the action in the same words an admin sees
// elsewhere in the UI, not the log's terse "reboot"/"power-off".
func TestShutdownAction_ConsoleMessage(t *testing.T) {
Expand Down
Loading