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

fix: gracefully quit panicking Cmd #846

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions tea.go
Expand Up @@ -290,6 +290,10 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
// possible to cancel them so we'll have to leak the goroutine
// until Cmd returns.
go func() {
// Recover from panics.
if !p.startupOptions.has(withoutCatchPanics) {
defer p.recoverFromPanic()
}
msg := cmd() // this can be long.
p.Send(msg)
}()
Expand All @@ -300,9 +304,22 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
return ch
}

func (p *Program) recoverFromPanic() {
if r := recover(); r != nil {
fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
debug.PrintStack()
p.errs <- fmt.Errorf("%v", r)
return
}
}

// eventLoop is the central message loop. It receives and handles the default
// Bubble Tea messages, update the model and triggers redraws.
func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
// Recover from panics.
if !p.startupOptions.has(withoutCatchPanics) {
defer p.recoverFromPanic()
}
for {
select {
case <-p.ctx.Done():
Expand Down
50 changes: 50 additions & 0 deletions tea_test.go
Expand Up @@ -263,3 +263,53 @@ func TestTeaNoRun(t *testing.T) {
m := &testModel{}
NewProgram(m, WithInput(&in), WithOutput(&buf))
}

type panicModel struct {
panicOnInit, panicOnUpdate, panicOnView bool
}

func (m panicModel) Init() Cmd {
if m.panicOnInit {
panic("init panic")
}
return nil
}

func (m panicModel) Update(msg Msg) (Model, Cmd) {
return m, func() Msg {
if m.panicOnUpdate {
panic("update panic")
}
return Quit
}
}
func (m panicModel) View() string {
if m.panicOnView {
panic("view panic")
}
return ""
}

func TestPanicRecovery(t *testing.T) {
t.Run("recover from panics from model.Update()", func(t *testing.T) {
var buf bytes.Buffer
var in bytes.Buffer
in.Write([]byte("q"))

NewProgram(panicModel{panicOnUpdate: true}, WithInput(&in), WithOutput(&buf)).Run()
})
t.Run("recover from panics from model.View()", func(t *testing.T) {
var buf bytes.Buffer
var in bytes.Buffer
in.Write([]byte("q"))

NewProgram(panicModel{panicOnView: true}, WithInput(&in), WithOutput(&buf)).Run()
})
t.Run("recover from panics from model.View()", func(t *testing.T) {
var buf bytes.Buffer
var in bytes.Buffer
in.Write([]byte("q"))

NewProgram(panicModel{panicOnView: true}, WithInput(&in), WithOutput(&buf)).Run()
})
}