Skip to content

fix(core): dispatch menu commands asynchronously to avoid main-thread deadlock - #15898

Open
tachsin wants to merge 1 commit into
tauri-apps:devfrom
tachsin:fix/menu-command-deadlock
Open

fix(core): dispatch menu commands asynchronously to avoid main-thread deadlock#15898
tachsin wants to merge 1 commit into
tauri-apps:devfrom
tachsin:fix/menu-command-deadlock

Conversation

@tachsin

@tachsin tachsin commented Aug 20, 2026

Copy link
Copy Markdown

Summary

Fixes #15888.

AppManager::extend_api holds the Mutex<PluginStore> lock for the entire synchronous dispatch of a command. Every menu plugin command (aside from popup, set_as_app_menu, set_as_window_menu, which were already async) hops to the main thread via run_main_thread!/run_item_main_thread! and blocks on a channel recv() while still holding that lock.

If a background thread dispatches a menu command while holding the plugin store lock, and the main thread later needs that same lock to process a queued window/run event before it drains the queued main-thread closure, both sides wait on each other forever — the exact lock cycle described in the issue.

Making these commands async moves execution onto crate::async_runtime::spawn. This releases the plugin store lock as soon as the command is spawned, before it reaches the main-thread hop, which breaks the deadlock cycle. This is transparent to the JS API since invoke() already returns a promise regardless of whether the Rust command is sync or async.

I reproduced the underlying lock-ordering issue with a minimal standalone harness that mirrors the real PluginStore/run_main_thread!/on_event call pattern 1:1 — the blocking-dispatch version reliably hangs, and the async-dispatch version completes cleanly.

Test plan

  • cargo check -p tauri --features wry
  • cargo clippy -p tauri --features wry — no new warnings
  • Minimal repro harness confirming the lock-ordering deadlock with blocking dispatch, and that it's resolved with async dispatch
  • Manual verification on macOS with tauri-runtime-cef (not available in this environment)

… deadlock

AppManager::extend_api holds the PluginStore mutex for the duration of a
synchronous command dispatch. Every menu plugin command hops to the main
thread via run_main_thread!/run_item_main_thread! and blocks on a channel
recv() while still holding that lock. If the main thread needs the same
PluginStore lock to process a queued event before it drains the queued
main-thread closure, both sides wait on each other forever.

Making these commands async releases the lock as soon as the command is
spawned, before it reaches the main-thread hop, breaking the cycle.
@tachsin
tachsin requested a review from a team as a code owner August 20, 2026 06:57
@Legend-Master Legend-Master added the ai-slop Low effort content, see https://github.com/tauri-apps/tauri?tab=contributing-ov-file#ai-tool-policy label Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-slop Low effort content, see https://github.com/tauri-apps/tauri?tab=contributing-ov-file#ai-tool-policy

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CEF: frontend Menu.new() can deadlock on macOS

2 participants