Skip to content

fix(linux): fix race conditions and deadlocks in event handling#5192

Merged
leaanthony merged 4 commits into
masterfrom
fix/4424-event-blocks-menu
May 17, 2026
Merged

fix(linux): fix race conditions and deadlocks in event handling#5192
leaanthony merged 4 commits into
masterfrom
fix/4424-event-blocks-menu

Conversation

@leaanthony

@leaanthony leaanthony commented Apr 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Fix RLock used for map deletion in mainthread_linux.go, mainthread_darwin.go, and mainthread_ios.go — map writes under RLock cause undefined behavior with concurrent readers
  • Fix DispatchWailsEvent holding windowsLock during potentially blocking ExecJS dispatch — release lock before dispatching to prevent deadlocks when the main thread also needs the lock
  • Add early unlock before Fatal in nil callback path to avoid lock leak
  • Add unit tests for lock correctness and concurrent access patterns

Fixes #4424

Test plan

  • TestMainThreadFunctionStoreLockCorrectness — concurrent callback store access with Lock (not RLock)
  • TestMainThreadFunctionStoreConcurrentReadWrite — concurrent readers and writers
  • TestDispatchWailsEventReleasesLockAfterCompletion — lock is released after dispatch
  • Integration test at v3/test/4424-event-block/main.go — manual test with event emitter + menu clicks

Summary by CodeRabbit

  • New Features

    • Added XDG_SESSION_TYPE to Linux diagnostic output.
  • Bug Fixes

    • Fixed Wayland window menu crashes.
    • Fixed GTK crashes from invalid app names.
    • Fixed Windows drag-and-drop memory error during startup.
    • Resolved race conditions in main-thread callback handling.
    • Improved CLI task variable parsing for KEY=VALUE pairs.
    • Fixed macOS window zoom button state conflict.
  • Tests

    • Added concurrency test and a sample app + assets to reproduce and validate event/callback behavior.

Review Change Stack

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 726aab48-e43b-4599-8180-fd33a07a9e34

📥 Commits

Reviewing files that changed from the base of the PR and between 7ee25c5 and 6902fbd.

📒 Files selected for processing (1)
  • v3/UNRELEASED_CHANGELOG.md

Walkthrough

Switches mainthread callback dispatch from read locks to exclusive locks across platforms, adds a test verifying DispatchWailsEvent releases the app lock before per-window dispatch, and includes a regression app, assets, Taskfile, and changelog entry documenting the fixes.

Changes

Mainthread Event Emission Race Condition Fix

Layer / File(s) Summary
Mainthread synchronization fix across platforms
v3/pkg/application/mainthread_darwin.go, v3/pkg/application/mainthread_ios.go, v3/pkg/application/mainthread_linux.go, v3/pkg/application/mainthread_android.go
dispatchOnMainThreadCallback / executeOnMainThread switch from RLock/RUnlock to Lock/Unlock when looking up and deleting callbacks in mainThreadFunctionStore. Explicit Unlock is added on invalid-id error paths before calling Fatal.
Lock-release concurrency test
v3/pkg/application/transport_event_ipc_test.go
Adds lockProbeWindow and TestDispatchWailsEventReleasesLockBeforePerWindowDispatch, which probes app.windowsLock via TryLock() during per-window dispatch and verifies the lock is released after the overall dispatch completes.
Regression test app, assets, Taskfile, and changelog
v3/test/4424-event-block/*, v3/UNRELEASED_CHANGELOG.md
Adds a reproducible app that emits periodic backendmessage events (main.go, assets/index.html), a Taskfile.yml to run it, and a changelog entry documenting the Linux XDG_SESSION_TYPE doctor output and multiple Fixed items (Wayland appmenu crash, GTK invalid app name crash, Windows drag-and-drop memory errors, mainthread callback race, CLI KEY=VALUE handling, macOS NSWindowZoomButton state conflict).

Sequence Diagram(s)

sequenceDiagram
  participant EventEmitter
  participant MainThreadDispatcher
  participant CallbackStore
  participant MenuHandler
  EventEmitter->>MainThreadDispatcher: Emit event
  MainThreadDispatcher->>CallbackStore: Lock (exclusive) and lookup callbackID
  CallbackStore-->>MainThreadDispatcher: return callback fn / nil
  MainThreadDispatcher->>CallbackStore: Delete callbackID
  MainThreadDispatcher->>CallbackStore: Unlock
  MainThreadDispatcher->>MainThreadDispatcher: Invoke callback (fn())
  MenuHandler->>MainThreadDispatcher: Menu click handling (may acquire windowsLock)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #5106 — Adds a test ensuring DispatchWailsEvent releases the windowsLock; addresses the same windowsLock/DispatchWailsEvent deadlock behavior.

Possibly related PRs

  • wailsapp/wails#5107 — Adds a regression test for EventIPCTransport.DispatchWailsEvent lock-release behavior and modifies dispatch locking (directly related).

Suggested Labels

Bug, go, v3, Linux, size:M

Poem

🐰
I bounced where locks once held the day,
Events and menus found their way.
Read to strong, the race undone,
Clicks return and events can run.
🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'fix(linux): fix race conditions and deadlocks in event handling' accurately describes the main changes—addressing RLock/Lock issues and deadlock problems in event handling mechanisms.
Description check ✅ Passed The PR description includes a clear summary of changes, issue reference (Fixes #4424), test plan with specific test names, and addresses the template requirements adequately despite missing some optional checklist items.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #4424: fixing RLock usage in mainthread files for safe map writes, releasing windowsLock before blocking ExecJS dispatch, adding early unlocks to prevent lock leaks, and providing unit and integration tests.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the event/menu blocking issue: mainthread lock fixes, event dispatch refactoring, lock-correctness tests, and the reproduction test harness are all within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/4424-event-blocks-menu

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@leaanthony leaanthony changed the base branch from v3-alpha to master April 29, 2026 13:07
Addresses issues that could cause menu buttons to stop responding when
events are emitted frequently:

Fix race condition in mainthread callback store (Linux, macOS, iOS):
- Change RLock() to Lock() when deleting from mainThreadFunctionStore.
  Using RLock for the read but then calling delete() violated the
  RWMutex contract and could cause undefined behaviour under concurrent
  access.

The related EventIPCTransport.DispatchWailsEvent deadlock identified
during this investigation was independently fixed on master in #5107.

Adds regression tests and a manual reproduction app under
v3/test/4424-event-block/.
@leaanthony leaanthony force-pushed the fix/4424-event-blocks-menu branch from 6b5193b to 185b0b2 Compare May 17, 2026 02:14
@leaanthony leaanthony marked this pull request as ready for review May 17, 2026 02:16
Copilot AI review requested due to automatic review settings May 17, 2026 02:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
v3/pkg/application/transport_event_ipc_test.go (1)

80-103: ⚡ Quick win

Broaden this regression test to exercise the actual window dispatch path.

With windows initialized empty, this can pass without touching the potentially blocking ExecJS branch tied to #4424. Add a stub window and assert lock behavior while dispatch is in-flight.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/transport_event_ipc_test.go` around lines 80 - 103, The
test currently never exercises the window dispatch path because app.windows is
empty; modify TestDispatchWailsEventReleasesLockAfterCompletion to insert a stub
Window into app.windows (e.g., id 1) that implements the ExecJS method and
blocks on a channel so DispatchWailsEvent goes through the ExecJS branch. Start
DispatchWailsEvent in a goroutine, assert that while the stub's ExecJS is
blocked the windowsLock is still held (try to acquire it and expect to block),
then unblock the stub ExecJS channel and finally assert windowsLock is released
(acquire succeeds within timeout). Reference symbols:
TestDispatchWailsEventReleasesLockAfterCompletion, App.windows, App.windowsLock,
EventIPCTransport.DispatchWailsEvent, CustomEvent, and the window stub's ExecJS.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@v3/pkg/application/transport_event_ipc_test.go`:
- Around line 80-103: The test currently never exercises the window dispatch
path because app.windows is empty; modify
TestDispatchWailsEventReleasesLockAfterCompletion to insert a stub Window into
app.windows (e.g., id 1) that implements the ExecJS method and blocks on a
channel so DispatchWailsEvent goes through the ExecJS branch. Start
DispatchWailsEvent in a goroutine, assert that while the stub's ExecJS is
blocked the windowsLock is still held (try to acquire it and expect to block),
then unblock the stub ExecJS channel and finally assert windowsLock is released
(acquire succeeds within timeout). Reference symbols:
TestDispatchWailsEventReleasesLockAfterCompletion, App.windows, App.windowsLock,
EventIPCTransport.DispatchWailsEvent, CustomEvent, and the window stub's ExecJS.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ba5ea097-d2c3-4289-a128-98a0760bf85d

📥 Commits

Reviewing files that changed from the base of the PR and between a2da1df and 185b0b2.

📒 Files selected for processing (7)
  • v3/UNRELEASED_CHANGELOG.md
  • v3/pkg/application/mainthread_darwin.go
  • v3/pkg/application/mainthread_ios.go
  • v3/pkg/application/mainthread_linux.go
  • v3/pkg/application/transport_event_ipc_test.go
  • v3/test/4424-event-block/Taskfile.yml
  • v3/test/4424-event-block/main.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes concurrency issues in v3 event/main-thread handling, especially around callback-store mutation and event dispatch locks for #4424.

Changes:

  • Switches main-thread callback deletion on Linux, macOS, and iOS from RLock to Lock.
  • Updates event IPC dispatch to avoid holding windowsLock while dispatching to windows.
  • Adds unit/manual tests and changelog entries for the race/deadlock fixes.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
v3/UNRELEASED_CHANGELOG.md Adds unreleased notes for #4424 and related fixes.
v3/pkg/application/transport_event_ipc.go Snapshots windows before dispatch to avoid lock retention during ExecJS.
v3/pkg/application/mainthread_linux.go Uses write locking when reading/deleting callback-store entries.
v3/pkg/application/mainthread_darwin.go Uses write locking when reading/deleting callback-store entries.
v3/pkg/application/mainthread_ios.go Uses write locking when reading/deleting callback-store entries.
v3/pkg/application/transport_event_ipc_test.go Adds concurrency-focused tests for callback-store and dispatch locking.
v3/test/4424-event-block/Taskfile.yml Adds a task to run the manual repro app.
v3/test/4424-event-block/main.go Adds a manual app intended to reproduce event/menu blocking.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread v3/pkg/application/transport_event_ipc_test.go Outdated
Comment on lines +80 to +102
func TestDispatchWailsEventReleasesLockAfterCompletion(t *testing.T) {
app := &App{
windows: make(map[uint]Window),
windowsLock: sync.RWMutex{},
}

transport := &EventIPCTransport{app: app}
event := &CustomEvent{Name: "test"}

transport.DispatchWailsEvent(event)

acquired := make(chan struct{})
go func() {
app.windowsLock.Lock()
close(acquired)
app.windowsLock.Unlock()
}()

select {
case <-acquired:
case <-time.After(time.Second):
t.Fatal("windowsLock should be released after DispatchWailsEvent completes")
}
Comment on lines +58 to +61
app.Event.Emit("backendmessage", map[string]interface{}{
"counter": counter,
"time": time.Now().Format(time.RFC3339),
})
Comment on lines +46 to +77
var lock sync.RWMutex
store := make(map[uint]func())
var wg sync.WaitGroup

const writers = 50
const readers = 50

for i := 0; i < writers; i++ {
wg.Add(1)
go func(id uint) {
defer wg.Done()
lock.Lock()
store[id] = func() {}
lock.Unlock()

lock.Lock()
delete(store, id)
lock.Unlock()
}(uint(i))
}

for i := 0; i < readers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
lock.RLock()
_ = len(store)
lock.RUnlock()
}()
}

wg.Wait()
- Fix window menu crash on Wayland caused by appmenu-gtk-module accessing unrealized window (#4769) by @leaanthony
- Fix GTK application crash when app name contains invalid characters (spaces, parentheses, etc.) by @leaanthony
- Fix "not enough memory" error when initializing drag and drop on Windows (#4701) by @overlordtm
- Fix race condition in mainthread callback store using incorrect RLock for map deletion (Linux, macOS, iOS) (#4424) by @leaanthony
Comment thread v3/pkg/application/mainthread_linux.go
leaanthony and others added 3 commits May 17, 2026 14:44
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- mainthread_android.go: apply the same RLock -> Lock change made to
  the linux/macOS/iOS callback paths. Android uses the same shared
  mainThreadFunctionStore, so it had the identical map-write under
  RLock pattern.

- transport_event_ipc_test: the previous lock-release test used an
  empty windows map and so would also have passed against the broken
  (lock-held-across-dispatch) implementation. Replace it with a probe
  Window whose DispatchWailsEvent attempts windowsLock.TryLock(); the
  fixed snapshot-and-release path lets the TryLock succeed, a
  regression to holding RLock across per-window dispatch fails it.
  Verified by mutation-test against the regressed implementation.

- test/4424-event-block: the manual repro had no webview window and
  so EventIPCTransport.DispatchWailsEvent's per-window loop was a
  no-op, making the harness unable to reach the ExecJS path involved
  in #4424. Add a small webview window that subscribes to the emitted
  event, bump the emit cadence to 100ms so menu clicks have a high
  probability of landing during a dispatch, and document the manual
  pass/fail criteria in the file header.
@leaanthony leaanthony merged commit d6e9ad1 into master May 17, 2026
13 of 15 checks passed
@leaanthony leaanthony deleted the fix/4424-event-blocks-menu branch May 17, 2026 12:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v3] Event emitter blocks menu button press

2 participants