Skip to content

Commit 4619e08

Browse files
committed
fix(console): stop the dashboard before the shutdown message is drawn
Since #366 the console shows a branded "Rebooting..." or "Shutting down..." message as soon as a shutdown is chosen. The dashboard is a separate supervised process writing to the same console on its own refresh tick, and nothing told it to stop, so a tick landing after the message was written could redraw the ordinary status frame over it. Wait now stops the console dashboard, and waits for it to have actually exited, before announcing the shutdown message, so no dashboard frame can land after it. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
1 parent 7db83ee commit 4619e08

3 files changed

Lines changed: 54 additions & 1 deletion

File tree

‎internal/init/run.go‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1016,7 +1016,21 @@ func boot(ctx context.Context, shutdown *shutdownRequests) (err error) {
10161016
// polls the local socket and redraws the node status frame. A console crash
10171017
// is non-critical: superviseConsole restarts it and never returns an error
10181018
// that would reboot a serving CA.
1019-
go superviseConsole(ctx)
1019+
//
1020+
// It runs under its own cancelable context rather than ctx directly, so a
1021+
// shutdown can stop it for good -- and confirm it has actually exited --
1022+
// before the shutdown message is written to the same console (#370).
1023+
consoleCtx, cancelConsole := context.WithCancel(ctx)
1024+
defer cancelConsole()
1025+
consoleDone := make(chan struct{})
1026+
go func() {
1027+
defer close(consoleDone)
1028+
superviseConsole(consoleCtx)
1029+
}()
1030+
shutdown.stopConsole = func() {
1031+
cancelConsole()
1032+
<-consoleDone
1033+
}
10201034

10211035
// 14. Park until a shutdown is requested, then return so the deferred
10221036
// teardown runs before PID 1 reboots or powers off. SIGINT must be handled

‎internal/init/shutdown.go‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ type shutdownRequests struct {
102102
// is happening instead of freezing on its last frame.
103103
announce func(ShutdownAction)
104104

105+
// stopConsole stops the console dashboard for good and blocks until it has
106+
// actually exited. Wait calls it before announce, so the dashboard's own
107+
// refresh loop can never redraw a frame over the shutdown message (#370).
108+
stopConsole func()
109+
105110
mu sync.Mutex
106111
chosen ShutdownAction
107112
}
@@ -145,6 +150,11 @@ func (r *shutdownRequests) Request(a ShutdownAction) {
145150
// for Action, and arms the teardown watchdog before returning, so the
146151
// teardown that follows cannot hang the node. An ended context counts as a
147152
// reboot.
153+
//
154+
// The console is stopped, and waited for, before the shutdown message is
155+
// announced: announcing first and stopping the console after would leave a
156+
// window where the dashboard's own refresh loop redraws its ordinary status
157+
// frame over the message that was just written (#370).
148158
func (r *shutdownRequests) Wait(ctx context.Context) ShutdownAction {
149159
a := ShutdownReboot
150160
select {
@@ -154,6 +164,9 @@ func (r *shutdownRequests) Wait(ctx context.Context) ShutdownAction {
154164
r.mu.Lock()
155165
r.chosen = a
156166
r.mu.Unlock()
167+
if r.stopConsole != nil {
168+
r.stopConsole()
169+
}
157170
if r.announce != nil {
158171
r.announce(a)
159172
}

‎internal/init/shutdown_test.go‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,32 @@ func TestShutdownRequests_WaitAnnouncesTheAction(t *testing.T) {
108108
}
109109
}
110110

111+
// Wait stops the console dashboard for good, and waits for it to have
112+
// actually stopped, before announcing the shutdown message. Otherwise the
113+
// dashboard's own refresh loop can redraw its ordinary status frame over the
114+
// shutdown message after it is written (#370).
115+
func TestShutdownRequests_WaitStopsTheConsoleBeforeAnnouncing(t *testing.T) {
116+
sd := testShutdownRequests(nil)
117+
var order []string
118+
sd.stopConsole = func() { order = append(order, "stop") }
119+
sd.announce = func(ShutdownAction) { order = append(order, "announce") }
120+
sd.Request(ShutdownReboot)
121+
122+
sd.Wait(context.Background())
123+
if !slices.Equal(order, []string{"stop", "announce"}) {
124+
t.Fatalf("order = %v, want [stop announce]", order)
125+
}
126+
}
127+
128+
// A nil stopConsole (the zero value, before run.go wires one up) must not
129+
// panic Wait.
130+
func TestShutdownRequests_WaitToleratesNoStopConsole(t *testing.T) {
131+
sd := testShutdownRequests(nil)
132+
sd.Request(ShutdownReboot)
133+
134+
sd.Wait(context.Background())
135+
}
136+
111137
// The console message names the action in the same words an admin sees
112138
// elsewhere in the UI, not the log's terse "reboot"/"power-off".
113139
func TestShutdownAction_ConsoleMessage(t *testing.T) {

0 commit comments

Comments
 (0)