Skip to content

Commit 62fd685

Browse files
committed
Recover from poisoned mutexes instead of crashing
Replace 56 .lock().unwrap() calls across 12 production files with .lock().unwrap_or_else(|e| e.into_inner()). Previously, if any thread panicked while holding a mutex, all subsequent lock attempts would cascade-panic and crash the app. The protected state (menu items, network discovery cache, file viewer sessions, search results, conflict resolution) is always safe to recover. Test files left unchanged.
1 parent 09ba6d4 commit 62fd685

33 files changed

Lines changed: 106 additions & 91 deletions

apps/desktop/src-tauri/src/commands/settings.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ pub fn update_menu_accelerator(app: AppHandle, command_id: &str, shortcut: &str)
6666
let is_checked = menu_state
6767
.view_mode_full
6868
.lock()
69-
.unwrap()
69+
.unwrap_or_else(|e| e.into_inner())
7070
.as_ref()
7171
.and_then(|item| item.is_checked().ok())
7272
.unwrap_or(false);
@@ -75,15 +75,15 @@ pub fn update_menu_accelerator(app: AppHandle, command_id: &str, shortcut: &str)
7575
.map_err(|e| format!("Failed to update Full view accelerator: {e}"))?;
7676

7777
// Update the reference in MenuState
78-
*menu_state.view_mode_full.lock().unwrap() = Some(new_item);
78+
*menu_state.view_mode_full.lock().unwrap_or_else(|e| e.into_inner()) = Some(new_item);
7979
Ok(())
8080
}
8181
"view.briefMode" => {
8282
// Get current checked state before updating
8383
let is_checked = menu_state
8484
.view_mode_brief
8585
.lock()
86-
.unwrap()
86+
.unwrap_or_else(|e| e.into_inner())
8787
.as_ref()
8888
.and_then(|item| item.is_checked().ok())
8989
.unwrap_or(true);
@@ -92,7 +92,7 @@ pub fn update_menu_accelerator(app: AppHandle, command_id: &str, shortcut: &str)
9292
.map_err(|e| format!("Failed to update Brief view accelerator: {e}"))?;
9393

9494
// Update the reference in MenuState
95-
*menu_state.view_mode_brief.lock().unwrap() = Some(new_item);
95+
*menu_state.view_mode_brief.lock().unwrap_or_else(|e| e.into_inner()) = Some(new_item);
9696
Ok(())
9797
}
9898
_ => {

apps/desktop/src-tauri/src/commands/ui.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use tauri_plugin_opener::OpenerExt;
99
#[tauri::command]
1010
pub fn update_menu_context<R: Runtime>(app: AppHandle<R>, path: String, filename: String) {
1111
let state = app.state::<MenuState<R>>();
12-
let mut context = state.context.lock().unwrap();
12+
let mut context = state.context.lock().unwrap_or_else(|e| e.into_inner());
1313
context.path = path;
1414
context.filename = filename;
1515
}
@@ -42,7 +42,7 @@ pub fn show_main_window<R: Runtime>(window: Window<R>) -> Result<(), String> {
4242
#[tauri::command]
4343
pub fn toggle_hidden_files<R: Runtime>(app: AppHandle<R>) -> Result<bool, String> {
4444
let menu_state = app.state::<MenuState<R>>();
45-
let guard = menu_state.show_hidden_files.lock().unwrap();
45+
let guard = menu_state.show_hidden_files.lock().unwrap_or_else(|e| e.into_inner());
4646
let Some(check_item) = guard.as_ref() else {
4747
return Err("Menu not initialized".to_string());
4848
};
@@ -64,8 +64,8 @@ pub fn toggle_hidden_files<R: Runtime>(app: AppHandle<R>) -> Result<bool, String
6464
#[tauri::command]
6565
pub fn set_view_mode<R: Runtime>(app: AppHandle<R>, mode: String) -> Result<(), String> {
6666
let menu_state = app.state::<MenuState<R>>();
67-
let full_guard = menu_state.view_mode_full.lock().unwrap();
68-
let brief_guard = menu_state.view_mode_brief.lock().unwrap();
67+
let full_guard = menu_state.view_mode_full.lock().unwrap_or_else(|e| e.into_inner());
68+
let brief_guard = menu_state.view_mode_brief.lock().unwrap_or_else(|e| e.into_inner());
6969

7070
let (Some(full_item), Some(brief_item)) = (full_guard.as_ref(), brief_guard.as_ref()) else {
7171
return Err("Menu not initialized".to_string());
@@ -179,7 +179,7 @@ pub fn open_in_editor(_path: String) -> Result<(), String> {
179179
/// Executes a menu action for the current context.
180180
pub fn execute_menu_action<R: Runtime>(app: &AppHandle<R>, id: &str) {
181181
let state = app.state::<MenuState<R>>();
182-
let context = state.context.lock().unwrap().clone();
182+
let context = state.context.lock().unwrap_or_else(|e| e.into_inner()).clone();
183183

184184
if context.path.is_empty() {
185185
return;

apps/desktop/src-tauri/src/file_system/write_operations/helpers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ pub(super) fn resolve_conflict(
360360
// Wait for user to call resolve_write_conflict.
361361
// The frontend cancels the operation if the dialog is destroyed, so this timeout
362362
// is only a safety net for when the frontend is completely dead (crash/hang).
363-
let guard = state.conflict_mutex.lock().unwrap();
363+
let guard = state.conflict_mutex.lock().unwrap_or_else(|e| e.into_inner());
364364
let (_guard, wait_result) = state
365365
.conflict_condvar
366366
.wait_timeout_while(guard, Duration::from_secs(300), |_| {

apps/desktop/src-tauri/src/file_system/write_operations/volume_conflict.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ pub(super) fn resolve_volume_conflict(
7979
);
8080

8181
// Wait for user to call resolve_write_conflict
82-
let guard = state.conflict_mutex.lock().unwrap();
82+
let guard = state.conflict_mutex.lock().unwrap_or_else(|e| e.into_inner());
8383
let _guard = state
8484
.conflict_condvar
8585
.wait_while(guard, |_| {

apps/desktop/src-tauri/src/file_viewer/byte_seek.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ impl FileViewerBackend for ByteSeekBackend {
253253
let mut search_start = 0;
254254
while let Some(match_pos) = line_lower[search_start..].find(&query_lower) {
255255
let col = search_start + match_pos;
256-
let mut matches = results.lock().unwrap();
256+
let mut matches = results.lock().unwrap_or_else(|e| e.into_inner());
257257
matches.push(SearchMatch {
258258
line: line_number,
259259
column: col,
@@ -280,7 +280,7 @@ impl FileViewerBackend for ByteSeekBackend {
280280
let mut search_start = 0;
281281
while let Some(match_pos) = line_lower[search_start..].find(&query_lower) {
282282
let col = search_start + match_pos;
283-
let mut matches = results.lock().unwrap();
283+
let mut matches = results.lock().unwrap_or_else(|e| e.into_inner());
284284
matches.push(SearchMatch {
285285
line: line_number,
286286
column: col,

apps/desktop/src-tauri/src/file_viewer/full_load.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ impl FileViewerBackend for FullLoadBackend {
136136
let mut search_start = 0;
137137
while let Some(pos) = line_lower[search_start..].find(&query_lower) {
138138
let col = search_start + pos;
139-
let mut matches = results.lock().unwrap();
139+
let mut matches = results.lock().unwrap_or_else(|e| e.into_inner());
140140
matches.push(SearchMatch {
141141
line: line_idx,
142142
column: col,

apps/desktop/src-tauri/src/file_viewer/line_index.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ impl FileViewerBackend for LineIndexBackend {
274274
let mut search_start = 0;
275275
while let Some(match_pos) = line_lower[search_start..].find(&query_lower) {
276276
let col = search_start + match_pos;
277-
let mut matches = results.lock().unwrap();
277+
let mut matches = results.lock().unwrap_or_else(|e| e.into_inner());
278278
matches.push(SearchMatch {
279279
line: line_number,
280280
column: col,
@@ -300,7 +300,7 @@ impl FileViewerBackend for LineIndexBackend {
300300
let mut search_start = 0;
301301
while let Some(match_pos) = line_lower[search_start..].find(&query_lower) {
302302
let col = search_start + match_pos;
303-
let mut matches = results.lock().unwrap();
303+
let mut matches = results.lock().unwrap_or_else(|e| e.into_inner());
304304
matches.push(SearchMatch {
305305
line: line_number,
306306
column: col,

apps/desktop/src-tauri/src/file_viewer/session.rs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ pub fn open_session(path: &str) -> Result<ViewerOpenResult, ViewerError> {
197197
match LineIndexBackend::open(&path_clone, &cancel_for_indexer) {
198198
Ok(new_backend) => {
199199
if !cancel_for_indexer.load(Ordering::Relaxed) {
200-
let mut sessions = SESSIONS.lock().unwrap();
200+
let mut sessions = SESSIONS.lock().unwrap_or_else(|e| e.into_inner());
201201
if let Some(session) = sessions.get_mut(&session_id_clone) {
202202
debug!(
203203
"Indexing completed for session {}, upgrading to LineIndex",
@@ -259,14 +259,17 @@ pub fn open_session(path: &str) -> Result<ViewerOpenResult, ViewerError> {
259259
is_indexing,
260260
};
261261

262-
SESSIONS.lock().unwrap().insert(session_id, session);
262+
SESSIONS
263+
.lock()
264+
.unwrap_or_else(|e| e.into_inner())
265+
.insert(session_id, session);
263266

264267
Ok(result)
265268
}
266269

267270
/// Gets the current status of a session (backend type, indexing state).
268271
pub fn get_session_status(session_id: &str) -> Result<ViewerSessionStatus, ViewerError> {
269-
let sessions = SESSIONS.lock().unwrap();
272+
let sessions = SESSIONS.lock().unwrap_or_else(|e| e.into_inner());
270273
let session = sessions
271274
.get(session_id)
272275
.ok_or(ViewerError::SessionNotFound(session_id.to_string()))?;
@@ -280,7 +283,7 @@ pub fn get_session_status(session_id: &str) -> Result<ViewerSessionStatus, Viewe
280283

281284
/// Gets a range of lines from a session.
282285
pub fn get_lines(session_id: &str, target: SeekTarget, count: usize) -> Result<LineChunk, ViewerError> {
283-
let sessions = SESSIONS.lock().unwrap();
286+
let sessions = SESSIONS.lock().unwrap_or_else(|e| e.into_inner());
284287
let session = sessions
285288
.get(session_id)
286289
.ok_or(ViewerError::SessionNotFound(session_id.to_string()))?;
@@ -313,7 +316,7 @@ pub fn search_start(session_id: &str, query: String) -> Result<(), ViewerError>
313316

314317
// Get the file path from the session to open a fresh file handle in the search thread
315318
let path = {
316-
let mut sessions = SESSIONS.lock().unwrap();
319+
let mut sessions = SESSIONS.lock().unwrap_or_else(|e| e.into_inner());
317320
let session = sessions
318321
.get_mut(session_id)
319322
.ok_or(ViewerError::SessionNotFound(session_id.to_string()))?;
@@ -332,30 +335,30 @@ pub fn search_start(session_id: &str, query: String) -> Result<(), ViewerError>
332335
let backend = match ByteSeekBackend::open(&path) {
333336
Ok(b) => b,
334337
Err(_) => {
335-
*status_clone.lock().unwrap() = SearchStatus::Done;
338+
*status_clone.lock().unwrap_or_else(|e| e.into_inner()) = SearchStatus::Done;
336339
return;
337340
}
338341
};
339342

340343
let result = backend.search(&query, &cancel_clone, &matches_clone);
341344
let final_scanned: u64 = result.unwrap_or_default();
342345

343-
*bytes_scanned_clone.lock().unwrap() = final_scanned;
346+
*bytes_scanned_clone.lock().unwrap_or_else(|e| e.into_inner()) = final_scanned;
344347

345348
let final_status = if cancel_clone.load(Ordering::Relaxed) {
346349
SearchStatus::Cancelled
347350
} else {
348351
SearchStatus::Done
349352
};
350-
*status_clone.lock().unwrap() = final_status;
353+
*status_clone.lock().unwrap_or_else(|e| e.into_inner()) = final_status;
351354
});
352355

353356
Ok(())
354357
}
355358

356359
/// Polls search progress for a session.
357360
pub fn search_poll(session_id: &str) -> Result<SearchPollResult, ViewerError> {
358-
let sessions = SESSIONS.lock().unwrap();
361+
let sessions = SESSIONS.lock().unwrap_or_else(|e| e.into_inner());
359362
let session = sessions
360363
.get(session_id)
361364
.ok_or(ViewerError::SessionNotFound(session_id.to_string()))?;
@@ -370,9 +373,9 @@ pub fn search_poll(session_id: &str) -> Result<SearchPollResult, ViewerError> {
370373
bytes_scanned: 0,
371374
}),
372375
Some(search) => {
373-
let status = search.status.lock().unwrap().clone();
374-
let matches = search.matches.lock().unwrap().clone();
375-
let bytes_scanned = *search.bytes_scanned.lock().unwrap();
376+
let status = search.status.lock().unwrap_or_else(|e| e.into_inner()).clone();
377+
let matches = search.matches.lock().unwrap_or_else(|e| e.into_inner()).clone();
378+
let bytes_scanned = *search.bytes_scanned.lock().unwrap_or_else(|e| e.into_inner());
376379

377380
Ok(SearchPollResult {
378381
status,
@@ -386,7 +389,7 @@ pub fn search_poll(session_id: &str) -> Result<SearchPollResult, ViewerError> {
386389

387390
/// Cancels an ongoing search.
388391
pub fn search_cancel(session_id: &str) -> Result<(), ViewerError> {
389-
let mut sessions = SESSIONS.lock().unwrap();
392+
let mut sessions = SESSIONS.lock().unwrap_or_else(|e| e.into_inner());
390393
let session = sessions
391394
.get_mut(session_id)
392395
.ok_or(ViewerError::SessionNotFound(session_id.to_string()))?;
@@ -401,7 +404,7 @@ pub fn search_cancel(session_id: &str) -> Result<(), ViewerError> {
401404

402405
/// Closes a viewer session and frees resources.
403406
pub fn close_session(session_id: &str) -> Result<(), ViewerError> {
404-
let mut sessions = SESSIONS.lock().unwrap();
407+
let mut sessions = SESSIONS.lock().unwrap_or_else(|e| e.into_inner());
405408
if let Some(session) = sessions.remove(session_id) {
406409
// Cancel any ongoing search
407410
if let Some(search) = &session.search {

apps/desktop/src-tauri/src/lib.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,19 @@ pub fn run() {
166166

167167
// Store the CheckMenuItem references in app state
168168
let menu_state = MenuState::default();
169-
*menu_state.show_hidden_files.lock().unwrap() = Some(menu_items.show_hidden_files);
170-
*menu_state.view_mode_full.lock().unwrap() = Some(menu_items.view_mode_full);
171-
*menu_state.view_mode_brief.lock().unwrap() = Some(menu_items.view_mode_brief);
172-
*menu_state.view_submenu.lock().unwrap() = Some(menu_items.view_submenu);
173-
*menu_state.view_mode_full_position.lock().unwrap() = menu_items.view_mode_full_position;
174-
*menu_state.view_mode_brief_position.lock().unwrap() = menu_items.view_mode_brief_position;
169+
*menu_state.show_hidden_files.lock().unwrap_or_else(|e| e.into_inner()) =
170+
Some(menu_items.show_hidden_files);
171+
*menu_state.view_mode_full.lock().unwrap_or_else(|e| e.into_inner()) = Some(menu_items.view_mode_full);
172+
*menu_state.view_mode_brief.lock().unwrap_or_else(|e| e.into_inner()) = Some(menu_items.view_mode_brief);
173+
*menu_state.view_submenu.lock().unwrap_or_else(|e| e.into_inner()) = Some(menu_items.view_submenu);
174+
*menu_state
175+
.view_mode_full_position
176+
.lock()
177+
.unwrap_or_else(|e| e.into_inner()) = menu_items.view_mode_full_position;
178+
*menu_state
179+
.view_mode_brief_position
180+
.lock()
181+
.unwrap_or_else(|e| e.into_inner()) = menu_items.view_mode_brief_position;
175182
app.manage(menu_state);
176183

177184
// Set window title based on license status
@@ -208,7 +215,7 @@ pub fn run() {
208215
if id == SHOW_HIDDEN_FILES_ID {
209216
// Get the CheckMenuItem from app state
210217
let menu_state = app.state::<MenuState<tauri::Wry>>();
211-
let guard = menu_state.show_hidden_files.lock().unwrap();
218+
let guard = menu_state.show_hidden_files.lock().unwrap_or_else(|e| e.into_inner());
212219
let Some(check_item) = guard.as_ref() else {
213220
return;
214221
};
@@ -228,8 +235,8 @@ pub fn run() {
228235
let menu_state = app.state::<MenuState<tauri::Wry>>();
229236

230237
let (full_guard, brief_guard) = (
231-
menu_state.view_mode_full.lock().unwrap(),
232-
menu_state.view_mode_brief.lock().unwrap(),
238+
menu_state.view_mode_full.lock().unwrap_or_else(|e| e.into_inner()),
239+
menu_state.view_mode_brief.lock().unwrap_or_else(|e| e.into_inner()),
233240
);
234241

235242
if let (Some(full_item), Some(brief_item)) = (full_guard.as_ref(), brief_guard.as_ref()) {

apps/desktop/src-tauri/src/menu.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -487,22 +487,28 @@ pub fn update_view_mode_accelerator<R: Runtime>(
487487
new_accelerator: Option<&str>,
488488
is_checked: bool,
489489
) -> tauri::Result<CheckMenuItem<R>> {
490-
let view_submenu_guard = menu_state.view_submenu.lock().unwrap();
490+
let view_submenu_guard = menu_state.view_submenu.lock().unwrap_or_else(|e| e.into_inner());
491491
let view_submenu = view_submenu_guard
492492
.as_ref()
493493
.ok_or_else(|| tauri::Error::InvalidWindowHandle)?;
494494

495495
let (menu_item_guard, position_guard, menu_id, label) = if is_full_mode {
496496
(
497-
menu_state.view_mode_full.lock().unwrap(),
498-
menu_state.view_mode_full_position.lock().unwrap(),
497+
menu_state.view_mode_full.lock().unwrap_or_else(|e| e.into_inner()),
498+
menu_state
499+
.view_mode_full_position
500+
.lock()
501+
.unwrap_or_else(|e| e.into_inner()),
499502
VIEW_MODE_FULL_ID,
500503
"Full view",
501504
)
502505
} else {
503506
(
504-
menu_state.view_mode_brief.lock().unwrap(),
505-
menu_state.view_mode_brief_position.lock().unwrap(),
507+
menu_state.view_mode_brief.lock().unwrap_or_else(|e| e.into_inner()),
508+
menu_state
509+
.view_mode_brief_position
510+
.lock()
511+
.unwrap_or_else(|e| e.into_inner()),
506512
VIEW_MODE_BRIEF_ID,
507513
"Brief view",
508514
)

0 commit comments

Comments
 (0)