fix(bonfire): end a live view when its context is cancelled and print later lines plainly (WR-09)
This commit is contained in:
@@ -325,6 +325,9 @@ func (m processModel) View() tea.View {
|
||||
type liveView struct {
|
||||
owner *console
|
||||
prog *tea.Program
|
||||
// done is closed when the program's Run has returned; printAbove never
|
||||
// sends to a program whose Run has ended, whatever ended it.
|
||||
done <-chan struct{}
|
||||
}
|
||||
|
||||
// The running live view, for PrintAbove and the owning console's output.
|
||||
@@ -348,6 +351,13 @@ var errLivePanic = errors.New("bonfire: live view work panicked")
|
||||
// work returns or panics, the view is unregistered (so PrintAbove stops
|
||||
// sending to it), told to finish and waited for, all under liveMu; it
|
||||
// never receives lines after that, and a panic goes on after the cleanup.
|
||||
//
|
||||
// The program does not take ctx: when ctx is cancelled, a watcher ends
|
||||
// the view with its last frame and unregisters it in the same step under
|
||||
// liveMu, without waiting for work, so lines printed after the
|
||||
// cancellation print plainly below the view instead of reaching a stopped
|
||||
// program. A program whose Run ended on its own is unregistered as well
|
||||
// and is never sent lines.
|
||||
func runLive(ctx context.Context, out io.Writer, c *console, model tea.Model, work func(send func(tea.Msg)) error) (started bool, err error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
@@ -355,6 +365,7 @@ func runLive(ctx context.Context, out io.Writer, c *console, model tea.Model, wo
|
||||
// Claim the slot before building the program: a widget that loses it
|
||||
// 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{})
|
||||
liveMu.Lock()
|
||||
if activeView != nil {
|
||||
liveMu.Unlock()
|
||||
@@ -364,18 +375,17 @@ func runLive(ctx context.Context, out io.Writer, c *console, model tea.Model, wo
|
||||
tea.WithInput(nil),
|
||||
tea.WithOutput(out),
|
||||
tea.WithoutSignalHandler(),
|
||||
tea.WithContext(ctx),
|
||||
tea.WithColorProfile(c.profile()),
|
||||
)
|
||||
view := &liveView{owner: c, prog: prog}
|
||||
view := &liveView{owner: c, prog: prog, done: finished}
|
||||
activeView = view
|
||||
liveMu.Unlock()
|
||||
|
||||
finished := make(chan struct{})
|
||||
go func() {
|
||||
defer close(finished)
|
||||
_, _ = prog.Run()
|
||||
}()
|
||||
stopWatch := watchContext(ctx, view, finished)
|
||||
stopSignals := watchSignals(view, finished)
|
||||
|
||||
completed := false
|
||||
@@ -392,9 +402,12 @@ func runLive(ctx context.Context, out io.Writer, c *console, model tea.Model, wo
|
||||
if activeView == view {
|
||||
activeView = nil
|
||||
}
|
||||
// When the context watcher already finished the program, its Run
|
||||
// has returned: Send returns at once and finished is closed.
|
||||
prog.Send(finishMsg{err: result})
|
||||
<-finished
|
||||
liveMu.Unlock()
|
||||
stopWatch()
|
||||
stopSignals()
|
||||
}()
|
||||
err = work(prog.Send)
|
||||
@@ -402,6 +415,38 @@ func runLive(ctx context.Context, out io.Writer, c *console, model tea.Model, wo
|
||||
return true, err
|
||||
}
|
||||
|
||||
// watchContext ends view when ctx is cancelled before the program
|
||||
// finishes: under liveMu it unregisters the view, sends the finish message
|
||||
// with ctx's error and waits for the program, the same single step as
|
||||
// runLive's cleanup, so a printer either reaches the running program or
|
||||
// waits and prints plainly after the last frame. When the program finishes
|
||||
// first (normal end, a signal, Run failing), the view is only unregistered.
|
||||
// The returned function waits for the watcher; call it after finished is
|
||||
// closed and without holding liveMu.
|
||||
func watchContext(ctx context.Context, view *liveView, finished <-chan struct{}) (wait func()) {
|
||||
exited := make(chan struct{})
|
||||
go func() {
|
||||
defer close(exited)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
liveMu.Lock()
|
||||
if activeView == view {
|
||||
activeView = nil
|
||||
}
|
||||
view.prog.Send(finishMsg{err: ctx.Err()})
|
||||
<-finished
|
||||
liveMu.Unlock()
|
||||
case <-finished:
|
||||
liveMu.Lock()
|
||||
if activeView == view {
|
||||
activeView = nil
|
||||
}
|
||||
liveMu.Unlock()
|
||||
}
|
||||
}()
|
||||
return func() { <-exited }
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -477,15 +522,25 @@ func printAboveLines(s string) bool {
|
||||
}
|
||||
|
||||
// printAbove prints s above the running live view when owner is nil or is
|
||||
// the console that started the view, and reports whether it did.
|
||||
// the console that started the view, and reports whether it did. A view
|
||||
// whose program has ended (its context was cancelled, or Run returned on
|
||||
// its own) is not running: printAbove unregisters it if it is still
|
||||
// registered and reports false, so the caller prints s plainly.
|
||||
func printAbove(owner *console, s string) bool {
|
||||
liveMu.Lock()
|
||||
defer liveMu.Unlock()
|
||||
if activeView == nil || (owner != nil && activeView.owner != owner) {
|
||||
v := activeView
|
||||
if v == nil || (owner != nil && v.owner != owner) {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-v.done:
|
||||
activeView = nil
|
||||
return false
|
||||
default:
|
||||
}
|
||||
// One message for the whole block, sent while holding liveMu: blocks
|
||||
// from different goroutines keep the order of their calls.
|
||||
activeView.prog.Send(printLine(strings.TrimSuffix(s, "\n")))
|
||||
v.prog.Send(printLine(strings.TrimSuffix(s, "\n")))
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user