Add deadlock guard for Queue{Update,Event}

This commit adds an atomic bool into Application that ensures that the
user can never call QueueUpdate or QueueEvent in the main loop.

Previously, when the user does this, the application would deadlock and
freeze forever. The user would often have to kill the process manually
to get out of this state. Now, the application will panic, indicating
the bug immediately.
pull/985/head
diamondburned 1 month ago
parent ed116790de
commit 986849f093
No known key found for this signature in database
GPG Key ID: D78C4471CE776659

@ -3,6 +3,7 @@ package tview
import (
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gdamore/tcell/v2"
@ -122,6 +123,10 @@ type Application struct {
mouseDownX, mouseDownY int // The position of the mouse when its button was last pressed.
lastMouseClick time.Time // The time when a mouse button was last clicked.
lastMouseButtons tcell.ButtonMask // The last mouse button state.
// updating is true if the event loop is handling update callbacks.
// It helps prevent deadlocks when calling QueueUpdate() from an update callback.
updating atomic.Bool
}
// NewApplication creates and returns a new application.
@ -470,7 +475,9 @@ EventLoop:
// If we have updates, now is the time to execute them.
case update := <-a.updates:
a.updating.Store(true)
update.f()
a.updating.Store(false)
if update.done != nil {
update.done <- struct{}{}
}
@ -837,6 +844,9 @@ func (a *Application) GetFocus() Primitive {
//
// This function returns after f has executed.
func (a *Application) QueueUpdate(f func()) *Application {
if a.updating.Load() {
panic("tview: QueueUpdate() must not be called from an update callback.")
}
ch := make(chan struct{})
a.updates <- queuedUpdate{f: f, done: ch}
<-ch
@ -857,6 +867,9 @@ func (a *Application) QueueUpdateDraw(f func()) *Application {
//
// It is not recommended for event to be nil.
func (a *Application) QueueEvent(event tcell.Event) *Application {
if a.updating.Load() {
panic("tview: QueueEvent() must not be called from an update callback.")
}
a.events <- event
return a
}

@ -0,0 +1,48 @@
package tview
import (
"testing"
"time"
"github.com/gdamore/tcell/v2"
)
func TestApplication_deadlock_check(t *testing.T) {
screen := tcell.NewSimulationScreen("UTF-8")
if err := screen.Init(); err != nil {
t.Errorf("screen.Init() error = %v, want nil", err)
}
app := NewApplication()
app.SetScreen(screen)
app.SetRoot(NewBox().SetTitle("Hello, world!"), true)
panicCh := make(chan bool)
go func() {
defer func() {
panicCh <- recover() != nil
}()
if err := app.Run(); err != nil {
t.Errorf("Application.Run() error = %v, want nil", err)
}
}()
go func() {
app.QueueUpdate(func() {
app.QueueUpdate(func() {
t.Errorf("impossible case")
})
})
}()
select {
case <-time.After(2 * time.Second):
t.Fatal("deadlock detected")
case panicked := <-panicCh:
if panicked {
t.Log("panic detected, deadlock avoided")
} else {
t.Log("impossible case where deadlock did not occur, but things are working fine :)")
}
}
}
Loading…
Cancel
Save