1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
//! Small concurrency conveniences.
use std::sync::{Mutex, MutexGuard};
/// Lock a [`Mutex`], recovering the guard if the lock was poisoned.
///
/// dirge never relies on lock poisoning for correctness — a panic while a lock
/// is held shouldn't cascade into every other locker panicking too — so the
/// codebase always recovers the inner guard. This replaces the ~120 repeated,
/// cryptic `.lock().unwrap_or_else(|e| e.into_inner())` call sites with one
/// named intent (`.lock_ignore_poison()`).
pub trait LockExt<T> {
fn lock_ignore_poison(&self) -> MutexGuard<'_, T>;
/// Non-blocking lock that ignores poisoning. Returns `None` only when
/// the lock is currently held by another thread (`WouldBlock`); a
/// poisoned-but-free mutex still yields its guard, matching
/// [`LockExt::lock_ignore_poison`]'s never-cascade policy.
///
/// Used on the UI keystroke path (dirge-w11c) so a plugin-bound key
/// pressed while a plugin tool holds the manager mutex is dropped with
/// a "busy" line instead of freezing the event loop, mirroring the
/// loop-top `try_lock` drains (H-R1).
// The sole production caller is plugin-gated; without `plugin` this is
// exercised only by the unit tests (a separate cfg(test) build).
#[cfg_attr(not(feature = "plugin"), allow(dead_code))]
fn try_lock_ignore_poison(&self) -> Option<MutexGuard<'_, T>>;
}
impl<T> LockExt<T> for Mutex<T> {
fn lock_ignore_poison(&self) -> MutexGuard<'_, T> {
self.lock().unwrap_or_else(|e| e.into_inner())
}
#[cfg_attr(not(feature = "plugin"), allow(dead_code))]
fn try_lock_ignore_poison(&self) -> Option<MutexGuard<'_, T>> {
use std::sync::TryLockError;
match self.try_lock() {
Ok(g) => Some(g),
Err(TryLockError::Poisoned(e)) => Some(e.into_inner()),
Err(TryLockError::WouldBlock) => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn recovers_guard_after_poison() {
let m = Arc::new(Mutex::new(7));
let m2 = m.clone();
// Poison the lock by panicking while it's held.
let _ = std::thread::spawn(move || {
let _g = m2.lock().unwrap();
panic!("poison it");
})
.join();
// A plain .lock() would now return Err; lock_ignore_poison recovers.
assert_eq!(*m.lock_ignore_poison(), 7);
*m.lock_ignore_poison() = 9;
assert_eq!(*m.lock_ignore_poison(), 9);
}
#[test]
fn try_lock_returns_none_when_contended() {
let m = Mutex::new(1);
let _held = m.lock_ignore_poison();
// Same thread, lock already held → WouldBlock → None.
assert!(m.try_lock_ignore_poison().is_none());
}
#[test]
fn try_lock_recovers_guard_after_poison() {
let m = Arc::new(Mutex::new(7));
let m2 = m.clone();
let _ = std::thread::spawn(move || {
let _g = m2.lock().unwrap();
panic!("poison it");
})
.join();
// Poisoned but uncontended → Some (never cascade the panic).
let g = m.try_lock_ignore_poison();
assert!(g.is_some());
assert_eq!(*g.unwrap(), 7);
}
#[test]
fn try_lock_succeeds_when_free() {
let m = Mutex::new(3);
assert_eq!(*m.try_lock_ignore_poison().unwrap(), 3);
}
}