[GH-ISSUE #199] Using app.SetInputCapture() to call app.QueueUpdate() could cause deadlock #154

Closed
opened 2026-03-04 01:02:27 +03:00 by kerem · 3 comments
Owner

Originally created by @3cb on GitHub (Dec 4, 2018).
Original GitHub issue: https://github.com/rivo/tview/issues/199

If Application.QueueUpdate() is called using Application.SetInputCapture() the main event loop will deadlock if the updates channel buffer is already full. The following example replicates this. If you hit Ctrl-Q it will freeze:

package main

import (
	"time"

	"github.com/gdamore/tcell"
	"github.com/rivo/tview"
)

func main() {
	box := tview.NewBox().
		SetBorder(true).
		SetBorderAttributes(tcell.AttrBold).
		SetTitle("A title")

	app := tview.NewApplication()
	app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
		switch event.Key() {
		case tcell.KeyCtrlQ:
			time.Sleep(3 * time.Second) // allow updates from goroutine to fill updates channel buffer
			app.QueueUpdateDraw(func() {
				box.SetBackgroundColor(tcell.ColorRoyalBlue)
			})
		default:
			return event
		}
		return nil
	})

	go func() {
		count := 1
		t := time.NewTicker(10 * time.Millisecond)
		for {
			<-t.C
			if count%2 == 0 {
				app.QueueUpdateDraw(func() {
					box.SetBackgroundColor(tcell.ColorSeaGreen)
				})
			} else {
				app.QueueUpdateDraw(func() {
					box.SetBackgroundColor(tcell.ColorYellowGreen)
				})
			}
			count++
		}
	}()

	if err := app.SetRoot(box, true).Run(); err != nil {
		panic(err)
	}
}

Originally created by @3cb on GitHub (Dec 4, 2018). Original GitHub issue: https://github.com/rivo/tview/issues/199 If `Application.QueueUpdate()` is called using `Application.SetInputCapture()` the main event loop will deadlock if the updates channel buffer is already full. The following example replicates this. If you hit Ctrl-Q it will freeze: ```go package main import ( "time" "github.com/gdamore/tcell" "github.com/rivo/tview" ) func main() { box := tview.NewBox(). SetBorder(true). SetBorderAttributes(tcell.AttrBold). SetTitle("A title") app := tview.NewApplication() app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyCtrlQ: time.Sleep(3 * time.Second) // allow updates from goroutine to fill updates channel buffer app.QueueUpdateDraw(func() { box.SetBackgroundColor(tcell.ColorRoyalBlue) }) default: return event } return nil }) go func() { count := 1 t := time.NewTicker(10 * time.Millisecond) for { <-t.C if count%2 == 0 { app.QueueUpdateDraw(func() { box.SetBackgroundColor(tcell.ColorSeaGreen) }) } else { app.QueueUpdateDraw(func() { box.SetBackgroundColor(tcell.ColorYellowGreen) }) } count++ } }() if err := app.SetRoot(box, true).Run(); err != nil { panic(err) } } ```
kerem closed this issue 2026-03-04 01:02:27 +03:00
Author
Owner

@rivo commented on GitHub (Dec 14, 2018):

I see. Well, the invocation of the SetInputCapture() callback is already part of the main event loop. Calling QueueUpdateDraw() is not required here. In fact, as you demonstrated, it may even lead to a deadlock. It's kind of already described in the package documentation:

If your code makes changes in response to key events, it will execute in the main goroutine and thus will not cause any race conditions.

I'll add some more clarification there but I don't think this requires any changes in the code. (Besides, I wouldn't even know how to avoid such a deadlock.)

<!-- gh-comment-id:447385931 --> @rivo commented on GitHub (Dec 14, 2018): I see. Well, the invocation of the `SetInputCapture()` callback is already part of the main event loop. Calling `QueueUpdateDraw()` is not required here. In fact, as you demonstrated, it may even lead to a deadlock. It's kind of already described in the package documentation: > If your code makes changes in response to key events, it will execute in the main goroutine and thus will not cause any race conditions. I'll add some more clarification there but I don't think this requires any changes in the code. (Besides, I wouldn't even know how to avoid such a deadlock.)
Author
Owner

@ricochet1k commented on GitHub (Dec 14, 2018):

To avoid the deadlock you can check if you are already running in the main loop and just run the update instead of queueing it.

<!-- gh-comment-id:447401143 --> @ricochet1k commented on GitHub (Dec 14, 2018): To avoid the deadlock you can check if you are already running in the main loop and just run the update instead of queueing it.
Author
Owner

@rivo commented on GitHub (Dec 15, 2018):

Yeah, I guess I could. I'd like to avoid having people call QueueUpdateDraw() everywhere, though. It's meant to help you avoid race conditions in some situations. Most programs will not require it. So unless I'm getting lots of issues here where people have trouble understanding the distinction, I'll leave it the way it is. (And if they have trouble understanding it, there may be a different problem to solve.)

<!-- gh-comment-id:447558783 --> @rivo commented on GitHub (Dec 15, 2018): Yeah, I guess I could. I'd like to avoid having people call `QueueUpdateDraw()` everywhere, though. It's meant to help you avoid race conditions in some situations. Most programs will not require it. So unless I'm getting lots of issues here where people have trouble understanding the distinction, I'll leave it the way it is. (And if they have trouble understanding it, there may be a different problem to solve.)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/tview#154
No description provided.