fix(bonfire): kill a live view on a signal only after it has started (IN-12, quick-261028-rqd)
This commit is contained in:
@@ -366,12 +366,13 @@ func runLive(ctx context.Context, out io.Writer, c *console, model tea.Model, wo
|
||||
// builds none, so it registers no child context on ctx and opens no
|
||||
// TEA_TRACE file. NewProgram takes no bonfire lock and prints nothing.
|
||||
finished := make(chan struct{})
|
||||
ready := make(chan struct{})
|
||||
liveMu.Lock()
|
||||
if activeView != nil {
|
||||
liveMu.Unlock()
|
||||
return false, nil
|
||||
}
|
||||
prog := tea.NewProgram(model,
|
||||
prog := tea.NewProgram(startedModel{Model: model, once: &sync.Once{}, ready: ready},
|
||||
tea.WithInput(nil),
|
||||
tea.WithOutput(out),
|
||||
tea.WithoutSignalHandler(),
|
||||
@@ -386,7 +387,7 @@ func runLive(ctx context.Context, out io.Writer, c *console, model tea.Model, wo
|
||||
_, _ = prog.Run()
|
||||
}()
|
||||
stopWatch := watchContext(ctx, view, finished)
|
||||
stopSignals := watchSignals(view, finished)
|
||||
stopSignals := watchSignals(view, ready, finished)
|
||||
|
||||
completed := false
|
||||
defer func() {
|
||||
@@ -447,16 +448,37 @@ func watchContext(ctx context.Context, view *liveView, finished <-chan struct{})
|
||||
return func() { <-exited }
|
||||
}
|
||||
|
||||
// startedModel wraps a live view's model so the signal relay knows when
|
||||
// the program has started: bubbletea v2.1.0 calls Init only after it has
|
||||
// started the renderer, so once ready is closed a Kill restores the cursor
|
||||
// and the terminal.
|
||||
type startedModel struct {
|
||||
tea.Model
|
||||
once *sync.Once
|
||||
ready chan struct{}
|
||||
}
|
||||
|
||||
func (m startedModel) Init() tea.Cmd {
|
||||
m.once.Do(func() { close(m.ready) })
|
||||
return m.Model.Init()
|
||||
}
|
||||
|
||||
func (m startedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
inner, cmd := m.Model.Update(msg)
|
||||
m.Model = inner
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// watchSignals relays SIGINT and SIGTERM (those not ignored) while view
|
||||
// runs. The views run without bubbletea's own signal handling, so without
|
||||
// this a Ctrl+C would end the process with the cursor still hidden. On a
|
||||
// signal the program is killed (bubbletea then restores the cursor and the
|
||||
// terminal), the view is unregistered and the signal is raised again on
|
||||
// the process with the relay stopped, so its default action, or the
|
||||
// command's own handler, still applies. The returned function stops the
|
||||
// relay when the view ends normally; a signal that arrived meanwhile is
|
||||
// raised again too.
|
||||
func watchSignals(view *liveView, finished <-chan struct{}) (stop func()) {
|
||||
// signal the program is killed once it has started (ready is closed) or
|
||||
// its Run has ended (bubbletea then restores the cursor and the terminal),
|
||||
// the view is unregistered and the signal is raised again on the process
|
||||
// with the relay stopped, so its default action, or the command's own
|
||||
// handler, still applies. The returned function stops the relay when the
|
||||
// view ends normally; a signal that arrived meanwhile is raised again too.
|
||||
func watchSignals(view *liveView, ready, finished <-chan struct{}) (stop func()) {
|
||||
var relay []os.Signal
|
||||
for _, sig := range []os.Signal{os.Interrupt, syscall.SIGTERM} {
|
||||
if !signal.Ignored(sig) {
|
||||
@@ -472,19 +494,14 @@ func watchSignals(view *liveView, finished <-chan struct{}) (stop func()) {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
select {
|
||||
case sig := <-sigs:
|
||||
view.prog.Kill()
|
||||
<-finished
|
||||
relaySignals(sigs, quit, ready, finished, view.prog.Kill, func() {
|
||||
liveMu.Lock()
|
||||
if activeView == view {
|
||||
activeView = nil
|
||||
}
|
||||
liveMu.Unlock()
|
||||
signal.Stop(sigs)
|
||||
raise(sig)
|
||||
case <-quit:
|
||||
}
|
||||
}, raise)
|
||||
}()
|
||||
return func() {
|
||||
close(quit)
|
||||
@@ -498,8 +515,35 @@ func watchSignals(view *liveView, finished <-chan struct{}) (stop func()) {
|
||||
}
|
||||
}
|
||||
|
||||
// relaySignals is the relay of watchSignals. On a signal from sigs it
|
||||
// waits until the program has started (ready) or its Run has ended
|
||||
// (finished), kills it, waits for finished, unregisters the view and the
|
||||
// relay (unregister) and raises the signal. Killing before the program
|
||||
// has started would leave the cursor hidden: bubbletea restores the
|
||||
// terminal only when its renderer is running. quit ends the relay without
|
||||
// a signal.
|
||||
func relaySignals(sigs <-chan os.Signal, quit, ready, finished <-chan struct{}, kill, unregister func(), raise func(os.Signal)) {
|
||||
select {
|
||||
case sig := <-sigs:
|
||||
select {
|
||||
case <-ready:
|
||||
case <-finished:
|
||||
}
|
||||
kill()
|
||||
<-finished
|
||||
unregister()
|
||||
raise(sig)
|
||||
case <-quit:
|
||||
}
|
||||
}
|
||||
|
||||
// raise sends sig to the own process; when that fails, it exits with 128
|
||||
// plus the signal number, as a shell reports a death by that signal.
|
||||
// plus the signal number, as a shell reports a death by that signal. That
|
||||
// is the path on platforms where a process cannot signal itself (Windows):
|
||||
// the relay exits after restoring the terminal, so a command's own signal
|
||||
// handler may not run. Go cannot tell whether another handler is
|
||||
// registered, and returning without exiting would swallow Ctrl+C for
|
||||
// commands that have none.
|
||||
func raise(sig os.Signal) {
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
if err == nil {
|
||||
|
||||
Reference in New Issue
Block a user