diff --git a/internal/async/goroutine.go b/internal/async/goroutine.go index 08a42b65e2..297fce5ba5 100644 --- a/internal/async/goroutine.go +++ b/internal/async/goroutine.go @@ -1,5 +1,14 @@ package async +import ( + "log" + "runtime" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/build" +) + // mainGoroutineID stores the main goroutine ID. // This ID must be initialized in main.init because // a main goroutine may not equal to 1 due to the @@ -10,6 +19,55 @@ func init() { mainGoroutineID = goroutineID() } +// IsMainGoroutine returns true if it is called from the main goroutine, false otherwise. func IsMainGoroutine() bool { return goroutineID() == mainGoroutineID } + +// EnsureNotMain is part of our thread transition and makes sure that the passed function runs off main. +// If the context is running on a goroutine or the transition has been disabled this will blindly run. +// Otherwise, an error will be logged and the function will be called on a new goroutine. +// +// This will be removed later and should never be public +func EnsureNotMain(fn func()) { + if build.DisableThreadChecks || !IsMainGoroutine() { + fn() + return + } + + log.Println("*** Error in Fyne call thread, fyne.Do called from main goroutine ***") + + logStackTop(2) + go fn() +} + +// EnsureMain is part of our thread transition and makes sure that the passed function runs on main. +// If the context is main or the transition has been disabled this will blindly run. +// Otherwise, an error will be logged and the function will be called on the main goroutine. +// +// This will be removed later and should never be public +func EnsureMain(fn func()) { + if build.DisableThreadChecks || IsMainGoroutine() { + fn() + return + } + + log.Println("*** Error in Fyne call thread, this should have been called in fyne.Do ***") + + logStackTop(1) + fyne.Do(fn) +} + +func logStackTop(skip int) { + pc := make([]uintptr, 2) + count := runtime.Callers(2+skip, pc) + frames := runtime.CallersFrames(pc) + frame, more := frames.Next() + if more && count > 1 { + nextFrame, _ := frames.Next() // skip an occasional driver call to itself + if !strings.Contains(nextFrame.File, "runtime") { // don't descend into Go + frame = nextFrame + } + } + log.Printf(" From: %s:%d", frame.File, frame.Line) +} diff --git a/internal/build/build.go b/internal/build/build.go index 7db2881d5e..5013808f07 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -1,2 +1,4 @@ // Package build contains information about they type of build currently running. package build + +const DisableThreadChecks = false diff --git a/internal/driver/glfw/driver.go b/internal/driver/glfw/driver.go index d20f2f3d14..93022fb536 100644 --- a/internal/driver/glfw/driver.go +++ b/internal/driver/glfw/driver.go @@ -57,7 +57,7 @@ func toOSIcon(icon []byte) ([]byte, error) { } func (d *gLDriver) DoFromGoroutine(f func()) { - runOnMain(f) + async.EnsureNotMain(f) } func (d *gLDriver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) { diff --git a/internal/driver/glfw/window.go b/internal/driver/glfw/window.go index 44ac97d641..92bea00d63 100644 --- a/internal/driver/glfw/window.go +++ b/internal/driver/glfw/window.go @@ -13,6 +13,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/async" "fyne.io/fyne/v2/internal/build" "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/internal/driver" @@ -129,79 +130,85 @@ func (w *window) detectTextureScale() float32 { } func (w *window) Show() { - if w.view() != nil { - w.doShowAgain() - return - } + async.EnsureMain(func() { + if w.view() != nil { + w.doShowAgain() + return + } - w.createLock.Do(w.create) - if w.view() == nil { - return - } + w.createLock.Do(w.create) + if w.view() == nil { + return + } - w.visible = true - view := w.view() - view.SetTitle(w.title) + w.visible = true + view := w.view() + view.SetTitle(w.title) - if !build.IsWayland && w.centered { - w.doCenterOnScreen() // lastly center if that was requested - } - view.Show() + if !build.IsWayland && w.centered { + w.doCenterOnScreen() // lastly center if that was requested + } + view.Show() - // save coordinates - if !build.IsWayland { - w.xpos, w.ypos = view.GetPos() - } + // save coordinates + if !build.IsWayland { + w.xpos, w.ypos = view.GetPos() + } - if w.fullScreen { // this does not work if called before viewport.Show() - w.SetFullScreen(true) - } + if w.fullScreen { // this does not work if called before viewport.Show() + w.SetFullScreen(true) + } - // show top canvas element - if content := w.canvas.Content(); content != nil { - content.Show() + // show top canvas element + if content := w.canvas.Content(); content != nil { + content.Show() - w.RunWithContext(func() { - w.driver.repaintWindow(w) - }) - } + w.RunWithContext(func() { + w.driver.repaintWindow(w) + }) + } + }) } func (w *window) Hide() { - if w.closing || w.viewport == nil { - return - } + async.EnsureMain(func() { + if w.closing || w.viewport == nil { + return + } - w.visible = false - v := w.viewport + w.visible = false + v := w.viewport - v.Hide() + v.Hide() - // hide top canvas element - if content := w.canvas.Content(); content != nil { - content.Hide() - } + // hide top canvas element + if content := w.canvas.Content(); content != nil { + content.Hide() + } + }) } func (w *window) Close() { - if w.isClosing() { - return - } + async.EnsureMain(func() { + if w.isClosing() { + return + } - // trigger callbacks - early so window still exists - if fn := w.onClosed; fn != nil { - w.onClosed = nil // avoid possibility of calling twice - fn() - } + // trigger callbacks - early so window still exists + if fn := w.onClosed; fn != nil { + w.onClosed = nil // avoid possibility of calling twice + fn() + } - w.closing = true - w.viewport.SetShouldClose(true) + w.closing = true + w.viewport.SetShouldClose(true) - cache.RangeTexturesFor(w.canvas, w.canvas.Painter().Free) - w.canvas.WalkTrees(nil, func(node *common.RenderCacheNode, _ fyne.Position) { - if wid, ok := node.Obj().(fyne.Widget); ok { - cache.DestroyRenderer(wid) - } + cache.RangeTexturesFor(w.canvas, w.canvas.Painter().Free) + w.canvas.WalkTrees(nil, func(node *common.RenderCacheNode, _ fyne.Position) { + if wid, ok := node.Obj().(fyne.Widget); ok { + cache.DestroyRenderer(wid) + } + }) }) } @@ -232,7 +239,7 @@ func (w *window) SetContent(content fyne.CanvasObject) { if content != nil { content.Show() } - w.RescaleContext() + async.EnsureMain(w.RescaleContext) } func (w *window) Canvas() fyne.Canvas { @@ -860,16 +867,19 @@ func (w *window) Context() any { func (w *window) runOnMainWhenCreated(fn func()) { if w.view() != nil { - fn() + async.EnsureMain(fn) return } w.pending = append(w.pending, fn) } -func (d *gLDriver) CreateWindow(title string) fyne.Window { +func (d *gLDriver) CreateWindow(title string) (win fyne.Window) { if runtime.GOOS != "js" { - return d.createWindow(title, true) + async.EnsureMain(func() { + win = d.createWindow(title, true) + }) + return win } // handling multiple windows by overlaying on the root for web diff --git a/internal/driver/mobile/driver.go b/internal/driver/mobile/driver.go index ea87e664de..7448f6030e 100644 --- a/internal/driver/mobile/driver.go +++ b/internal/driver/mobile/driver.go @@ -73,15 +73,17 @@ func init() { } func (d *driver) DoFromGoroutine(fn func()) { - done := common.DonePool.Get() - defer common.DonePool.Put(done) + async.EnsureNotMain(func() { + done := common.DonePool.Get() + defer common.DonePool.Put(done) - d.queuedFuncs.In() <- func() { - fn() - done <- struct{}{} - } + d.queuedFuncs.In() <- func() { + fn() + done <- struct{}{} + } - <-done + <-done + }) } func (d *driver) CreateWindow(title string) fyne.Window { diff --git a/test/driver.go b/test/driver.go index a98beb5a6e..16df8cccef 100644 --- a/test/driver.go +++ b/test/driver.go @@ -6,6 +6,7 @@ import ( "time" "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" intdriver "fyne.io/fyne/v2/internal/driver" "fyne.io/fyne/v2/internal/painter" "fyne.io/fyne/v2/internal/painter/software" @@ -50,7 +51,8 @@ func NewDriverWithPainter(painter SoftwarePainter) fyne.Driver { } func (d *driver) DoFromGoroutine(f func()) { - f() // Tests all run on a single (but potentially different per-test) thread + // Tests all run on a single (but potentially different per-test) thread + async.EnsureNotMain(f) } func (d *driver) AbsolutePositionForObject(co fyne.CanvasObject) fyne.Position {