Skip to content

fix: release write lock before showing blocking error dialog#1637

Open
chindris-mihai-alexandru wants to merge 5 commits intoCapSoftware:mainfrom
chindris-mihai-alexandru:fix/recording-error-deadlock
Open

fix: release write lock before showing blocking error dialog#1637
chindris-mihai-alexandru wants to merge 5 commits intoCapSoftware:mainfrom
chindris-mihai-alexandru:fix/recording-error-deadlock

Conversation

@chindris-mihai-alexandru
Copy link

@chindris-mihai-alexandru chindris-mihai-alexandru commented Feb 28, 2026

Summary

  • Fixes a deadlock in the recording error handler that prevents users from starting a new recording after an error occurs
  • The root cause: state_mtx.write().await is acquired, then dialog.blocking_show() is called while still holding the lock — blocking any new recording attempt at state_mtx.read().await indefinitely

Problem

When a recording fails (e.g. due to WriterFailed / AVAssetWriter errors as reported in #1535), the error handler in recording.rs does:

  1. Acquires state_mtx.write().await
  2. Emits the Failed event
  3. Shows a blocking error dialog (dialog.blocking_show())
  4. Only after the user dismisses the dialog does it call handle_recording_end() and release the lock

Since the write lock is held for the entire duration the user is looking at the error popup, any attempt to start a new recording blocks at state_mtx.read().await — the app appears frozen and the user must force-quit and relaunch.

Fix

Restructure the error handler to:

  1. Emit the Failed event
  2. Acquire the write lock in a scoped block, call handle_recording_end() to clean up state, then drop the lock
  3. Show the blocking error dialog after the lock is released

This allows users to immediately start a new recording after dismissing the error dialog.

Testing

  • cargo check -p cap-desktop passes
  • Minimal, single-file change — no new dependencies or behavioral changes beyond fixing the lock ordering

Related: #1535 (this fixes the secondary deadlock that compounds the WriterFailed issue)

Greptile Summary

Fixed a critical deadlock in the recording error handler by restructuring lock acquisition and dialog display order.

Key Changes:

  • Moved state_mtx.write().await acquisition into a scoped block after event emission
  • Cleanup (handle_recording_end) now executes within the scoped block, ensuring lock is dropped before the blocking dialog
  • Blocking error dialog now displays after lock release, allowing new recordings to start immediately

Technical Details:
The previous code held a write lock during dialog.blocking_show(), which blocked any attempt to start a new recording at state_mtx.read().await. The fix ensures the lock is held only during cleanup, not during user interaction with the dialog. This allows the app to remain responsive after an error occurs.

Confidence Score: 5/5

  • This PR is safe to merge - it fixes a critical deadlock with a minimal, well-scoped change
  • The fix correctly addresses the deadlock by using a scoped block to ensure the write lock is released immediately after state cleanup and before showing the blocking dialog. The change is minimal (10 lines moved), preserves all functionality, and follows Rust best practices for lock management. No new dependencies or behavioral changes beyond the lock ordering fix.
  • No files require special attention

Important Files Changed

Filename Overview
apps/desktop/src-tauri/src/recording.rs Fixed deadlock by releasing write lock before blocking dialog - uses scoped block to ensure cleanup completes and lock is dropped before showing error UI

Last reviewed commit: 163e51c

When a recording fails, the error handler acquires state_mtx.write()
and then calls dialog.blocking_show(), holding the write lock for the
entire duration the user is looking at the error popup. Any attempt to
start a new recording blocks at state_mtx.read() indefinitely, forcing
the user to quit and relaunch the app.

Fix: run handle_recording_end() inside a scoped block so the write lock
is dropped before the blocking dialog is shown. This allows users to
immediately start a new recording after dismissing the error dialog.

Closes CapSoftware#1535 (partial — fixes the secondary deadlock, not the root
AVAssetWriter/WriterFailed error)
Err(e) => {
let mut state = state_mtx.write().await;

let _ = RecordingEvent::Failed {
Copy link

Choose a reason for hiding this comment

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

Minor cleanup: e.to_string() is computed 3x here; consider computing it once and reusing it (also keeps the error message consistent across event/state cleanup/dialog).

Suggested change
let _ = RecordingEvent::Failed {
Err(e) => {
let error = e.to_string();
let _ = RecordingEvent::Failed {
error: error.clone(),
}
.emit(&app);
{
let mut state = state_mtx.write().await;
let _ = handle_recording_end(
app.clone(),
Err(error.clone()),
&mut state,
project_file_path,
)
.await;
}
let mut dialog = MessageDialogBuilder::new(
app.dialog().clone(),
"An error occurred".to_string(),
error,
)
.kind(tauri_plugin_dialog::MessageDialogKind::Error);

Copy link
Author

@chindris-mihai-alexandru chindris-mihai-alexandru Feb 28, 2026

Choose a reason for hiding this comment

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

Applied in 7beab87. I now compute the error string once and reuse it for the failed event payload, cleanup path, and dialog message.

chindris-mihai-alexandru

This comment was marked as duplicate.

Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
chindris-mihai-alexandru and others added 2 commits March 1, 2026 00:20
Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
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.

1 participant