Skip to content

fix: prevent abort on quit by handling poisoned mutexes in Drop impls#1354

Open
marcel-dias wants to merge 1 commit into
cjpais:mainfrom
marcel-dias:fix/panic-in-drop-during-shutdown
Open

fix: prevent abort on quit by handling poisoned mutexes in Drop impls#1354
marcel-dias wants to merge 1 commit into
cjpais:mainfrom
marcel-dias:fix/panic-in-drop-during-shutdown

Conversation

@marcel-dias
Copy link
Copy Markdown

Summary

  • Replace .lock().unwrap() with match + into_inner() in TranscriptionManager::drop() and LoadingGuard::drop() to prevent abort() on app quit

Problem

When macOS sends a quit event (Cmd+Q, system shutdown, etc.), the app terminates via [NSApplication terminate:]exit()__cxa_finalize_ranges, which runs Rust Drop handlers. If a mutex was poisoned by a prior panic anywhere in the transcription pipeline, calling .lock().unwrap() inside a Drop impl triggers a second panic. Rust treats panic-during-Drop as unrecoverable and calls abort(), producing a SIGABRT crash instead of a clean exit.

The two affected locations:

  • TranscriptionManager::drop() (transcription.rs:846) — .lock().unwrap() on watcher_handle
  • LoadingGuard::drop() (transcription.rs:59) — .lock().unwrap() on is_loading

Note: HandyKeysState::drop() already uses if let Ok(...) which correctly handles poisoned locks.

Fix

Use PoisonError::into_inner() to recover the underlying MutexGuard even when the mutex is poisoned. This is the standard Rust pattern for "I need this data during cleanup regardless of prior panics."

Test plan

  • cargo check / cargo clippy pass
  • App starts and records normally
  • Cmd+Q exits cleanly without crash
  • Force-quit and re-launch works correctly

🤖 Generated with Claude Code

During app termination, macOS sends a quit event which triggers
`exit()` → `__cxa_finalize_ranges` → Rust Drop handlers. If a mutex
was poisoned by a prior panic anywhere in the transcription pipeline,
calling `.lock().unwrap()` inside a Drop impl triggers a second panic.
Rust treats panic-during-Drop as unrecoverable and calls `abort()`,
crashing the app instead of exiting cleanly.

Replace `.lock().unwrap()` with `match` + `into_inner()` in both
`TranscriptionManager::drop()` and `LoadingGuard::drop()` so they
gracefully recover poisoned mutexes during cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cjpais
Copy link
Copy Markdown
Owner

cjpais commented May 3, 2026

Thanks we will pick this up after review

@marcel-dias
Copy link
Copy Markdown
Author

I believe I was able to map the steps that cause the issue.

I'm using Whisper Turbo model and the unload model = after 1 hour. With these settings, around the 5th recording Handy got stuck, like it opens the overlay, listen but do not record or transcribe the audio. After closing Handy and opening again MacOS shows this error.

I changed the unload model to after 5 min and it stopped failing.
using version 0.8.3

@cjpais
Copy link
Copy Markdown
Owner

cjpais commented May 7, 2026

Interesting @marcel-dias thanks for the info

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.

2 participants