Skip to content

Commit e97d3db

Browse files
committed
Drag: harden swizzle against API changes
- Replace .unwrap()/.expect() with graceful fallbacks - Wrap swizzled fn bodies in catch_unwind to prevent panics from crossing the FFI boundary - Guard deprecated draggedImage with respondsToSelector: - Add warn-once logging with actionable guidance for each failure mode - Extract call_original_entered/updated helpers so wry's original implementation always runs, even after panic
1 parent 6220b9d commit e97d3db

1 file changed

Lines changed: 147 additions & 34 deletions

File tree

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

Lines changed: 147 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,18 @@
77
//! Events emitted:
88
//! - `drag-image-size` `{ width, height }` — on drag enter
99
//! - `drag-modifiers` `{ altHeld }` — on drag enter and every drag update (only when changed)
10+
//!
11+
//! ## Resilience
12+
//!
13+
//! All native API calls are guarded against class/method removal. If wry renames its
14+
//! internal webview class or macOS deprecates APIs we rely on, the swizzle degrades gracefully:
15+
//! - Drag image detection disabled → the DOM overlay is always shown (redundant but functional)
16+
//! - Modifier key detection disabled → falls back to JS keydown/keyup (works when webview has focus)
17+
//!
18+
//! Rust panics inside swizzled functions are caught via `catch_unwind` to prevent crashes
19+
//! across the FFI boundary. Warning messages include actionable guidance for future maintainers.
1020
21+
use std::panic::{AssertUnwindSafe, catch_unwind};
1122
use std::ptr::NonNull;
1223
use std::sync::OnceLock;
1324
use std::sync::atomic::{AtomicBool, Ordering};
@@ -41,13 +52,33 @@ static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
4152
/// Tracks previous alt state so we only emit `drag-modifiers` when it changes.
4253
static LAST_ALT_HELD: AtomicBool = AtomicBool::new(false);
4354

55+
// Warn-once flags to prevent log spam for issues that recur on every drag event.
56+
static WARNED_NSEVENT_MISSING: AtomicBool = AtomicBool::new(false);
57+
static WARNED_ENTERED_PANIC: AtomicBool = AtomicBool::new(false);
58+
static WARNED_UPDATED_PANIC: AtomicBool = AtomicBool::new(false);
59+
static WARNED_NSARRAY_MISSING: AtomicBool = AtomicBool::new(false);
60+
static WARNED_DRAGGED_IMAGE_REMOVED: AtomicBool = AtomicBool::new(false);
61+
62+
/// Logs a warning message at most once per app session.
63+
fn warn_once(flag: &AtomicBool, msg: &str) {
64+
if !flag.swap(true, Ordering::Relaxed) {
65+
log::warn!("{msg}");
66+
}
67+
}
68+
4469
/// Installs swizzles on WryWebView. Call once during app setup.
4570
pub fn install(app_handle: AppHandle) {
4671
APP_HANDLE.set(app_handle).ok();
4772

4873
unsafe {
4974
let Some(cls) = AnyClass::get(c"WryWebView") else {
50-
log::warn!("drag_image_detection: WryWebView class not found, skipping swizzle");
75+
log::warn!(
76+
"drag_image_detection: WryWebView class not found — swizzle skipped. \
77+
Drag image detection and modifier tracking during drags are disabled. \
78+
This is likely caused by a wry update that renamed the webview class; \
79+
search wry's source for the ObjC class name and update the c\"WryWebView\" \
80+
lookup in drag_image_detection.rs."
81+
);
5182
return;
5283
};
5384

@@ -58,7 +89,12 @@ pub fn install(app_handle: AppHandle) {
5889
swizzled_dragging_entered as *const (),
5990
));
6091
} else {
61-
log::warn!("drag_image_detection: draggingEntered: not found on WryWebView");
92+
log::warn!(
93+
"drag_image_detection: draggingEntered: not found on WryWebView — \
94+
drag image size detection is disabled. \
95+
Wry may have changed how it implements NSDraggingDestination; \
96+
check wry's drag-and-drop event handling in its ObjC layer."
97+
);
6298
}
6399

64100
// Swizzle draggingUpdated:
@@ -68,7 +104,12 @@ pub fn install(app_handle: AppHandle) {
68104
swizzled_dragging_updated as *const (),
69105
));
70106
} else {
71-
log::warn!("drag_image_detection: draggingUpdated: not found on WryWebView");
107+
log::warn!(
108+
"drag_image_detection: draggingUpdated: not found on WryWebView — \
109+
live modifier key tracking during drags is disabled. \
110+
Wry may have changed how it implements NSDraggingDestination; \
111+
check wry's drag-and-drop event handling in its ObjC layer."
112+
);
72113
}
73114

74115
log::info!("drag_image_detection: swizzles installed on WryWebView");
@@ -77,8 +118,19 @@ pub fn install(app_handle: AppHandle) {
77118

78119
/// Reads the current Option/Alt key state from `[NSEvent modifierFlags]`.
79120
/// This is a class method that reads hardware state — works even when the webview isn't focused.
121+
/// Returns `false` if NSEvent can't be found (graceful degradation).
80122
fn is_option_held() -> bool {
81-
let flags: usize = unsafe { msg_send![AnyClass::get(c"NSEvent").unwrap(), modifierFlags] };
123+
let Some(cls) = AnyClass::get(c"NSEvent") else {
124+
warn_once(
125+
&WARNED_NSEVENT_MISSING,
126+
"drag_image_detection: NSEvent class not found — Alt/Option detection during drags \
127+
is disabled. This is a core AppKit class and shouldn't disappear; if it did, check \
128+
whether macOS moved it to a different framework or renamed it. Modifier detection \
129+
falls back to JS keydown/keyup, which doesn't work during OS-level drags.",
130+
);
131+
return false;
132+
};
133+
let flags: usize = unsafe { msg_send![cls, modifierFlags] };
82134
flags & NS_EVENT_MODIFIER_FLAG_OPTION != 0
83135
}
84136

@@ -102,48 +154,86 @@ fn emit_modifiers_forced() {
102154
}
103155
}
104156

157+
// --- Helpers for forwarding to original implementations ---
158+
159+
/// Forwards to wry's original `draggingEntered:`. Always safe to call — returns
160+
/// `NSDragOperation::Copy` if the original wasn't saved (shouldn't happen in practice).
161+
unsafe fn call_original_entered(this: &AnyObject, cmd: Sel, drag_info: &AnyObject) -> usize { unsafe {
162+
if let Some(&original) = ORIGINAL_ENTERED_IMP.get() {
163+
let f = std::mem::transmute::<Imp, unsafe extern "C-unwind" fn(&AnyObject, Sel, &AnyObject) -> usize>(original);
164+
f(this, cmd, drag_info)
165+
} else {
166+
NSDragOperation::Copy.0
167+
}
168+
}}
169+
170+
/// Forwards to wry's original `draggingUpdated:`. Same fallback as above.
171+
unsafe fn call_original_updated(this: &AnyObject, cmd: Sel, drag_info: &AnyObject) -> usize { unsafe {
172+
if let Some(&original) = ORIGINAL_UPDATED_IMP.get() {
173+
let f = std::mem::transmute::<Imp, unsafe extern "C-unwind" fn(&AnyObject, Sel, &AnyObject) -> usize>(original);
174+
f(this, cmd, drag_info)
175+
} else {
176+
NSDragOperation::Copy.0
177+
}
178+
}}
179+
105180
// --- draggingEntered: swizzle ---
106181

107182
unsafe extern "C-unwind" fn swizzled_dragging_entered(this: &AnyObject, cmd: Sel, drag_info: &AnyObject) -> usize {
108-
let size = unsafe { read_drag_image_size(drag_info) };
183+
// Our custom logic: read drag image size and emit modifier events.
184+
// Wrapped in catch_unwind to prevent any unexpected Rust panic from crossing the FFI boundary
185+
// and crashing the app mid-drag.
186+
let result = catch_unwind(AssertUnwindSafe(|| {
187+
let size = unsafe { read_drag_image_size(drag_info) };
188+
189+
if let Some(app_handle) = APP_HANDLE.get() {
190+
let _ = app_handle.emit(
191+
"drag-image-size",
192+
DragImageSize {
193+
width: size.0,
194+
height: size.1,
195+
},
196+
);
197+
}
109198

110-
if let Some(app_handle) = APP_HANDLE.get() {
111-
let _ = app_handle.emit(
112-
"drag-image-size",
113-
DragImageSize {
114-
width: size.0,
115-
height: size.1,
116-
},
199+
// Always emit modifiers on enter (initial state for this drag session)
200+
emit_modifiers_forced();
201+
}));
202+
203+
if result.is_err() {
204+
warn_once(
205+
&WARNED_ENTERED_PANIC,
206+
"drag_image_detection: panic in draggingEntered swizzle — drag image detection \
207+
and initial modifier state may not work for this session. \
208+
This is likely caused by a wry or macOS API change; check that NSDraggingItem's \
209+
draggingFrame() and NSEvent's modifierFlags still match the expected signatures \
210+
in drag_image_detection.rs.",
117211
);
118212
}
119213

120-
// Always emit modifiers on enter (initial state for this drag session)
121-
emit_modifiers_forced();
122-
123-
if let Some(&original) = ORIGINAL_ENTERED_IMP.get() {
124-
let f = unsafe {
125-
std::mem::transmute::<Imp, unsafe extern "C-unwind" fn(&AnyObject, Sel, &AnyObject) -> usize>(original)
126-
};
127-
unsafe { f(this, cmd, drag_info) }
128-
} else {
129-
NSDragOperation::Copy.0
130-
}
214+
// Always forward to wry's original implementation, even if our logic failed.
215+
unsafe { call_original_entered(this, cmd, drag_info) }
131216
}
132217

133218
// --- draggingUpdated: swizzle ---
134219

135220
unsafe extern "C-unwind" fn swizzled_dragging_updated(this: &AnyObject, cmd: Sel, drag_info: &AnyObject) -> usize {
136221
// Only emit when modifier state changes (avoids flooding on every mouse move)
137-
emit_modifiers_if_changed();
138-
139-
if let Some(&original) = ORIGINAL_UPDATED_IMP.get() {
140-
let f = unsafe {
141-
std::mem::transmute::<Imp, unsafe extern "C-unwind" fn(&AnyObject, Sel, &AnyObject) -> usize>(original)
142-
};
143-
unsafe { f(this, cmd, drag_info) }
144-
} else {
145-
NSDragOperation::Copy.0
222+
let result = catch_unwind(AssertUnwindSafe(|| {
223+
emit_modifiers_if_changed();
224+
}));
225+
226+
if result.is_err() {
227+
warn_once(
228+
&WARNED_UPDATED_PANIC,
229+
"drag_image_detection: panic in draggingUpdated swizzle — live modifier key \
230+
tracking during drags is disabled for this session. \
231+
Check NSEvent.modifierFlags usage in drag_image_detection.rs.",
232+
);
146233
}
234+
235+
// Always forward to wry's original implementation.
236+
unsafe { call_original_updated(this, cmd, drag_info) }
147237
}
148238

149239
// --- Drag image size reading ---
@@ -154,7 +244,22 @@ unsafe fn read_drag_image_size(drag_info: &AnyObject) -> (f64, f64) {
154244
return size;
155245
}
156246

157-
// Fallback: try the deprecated draggedImage() — works for same-process drags
247+
// Fallback: try the deprecated `draggedImage()` — works for same-process drags.
248+
// Guard with respondsToSelector: since Apple may remove this deprecated API entirely.
249+
let responds: Bool = unsafe { msg_send![drag_info, respondsToSelector: sel!(draggedImage)] };
250+
if !responds.as_bool() {
251+
warn_once(
252+
&WARNED_DRAGGED_IMAGE_REMOVED,
253+
"drag_image_detection: draggedImage selector no longer exists on NSDraggingInfo — \
254+
Apple removed this deprecated API in this macOS version. \
255+
The primary path (enumerateDraggingItems) still works; this only affects \
256+
same-process drag size detection as a fallback. \
257+
Remove the draggedImage fallback from read_drag_image_size() in \
258+
drag_image_detection.rs.",
259+
);
260+
return (0.0, 0.0);
261+
}
262+
158263
let image: *const AnyObject = unsafe { msg_send![drag_info, draggedImage] };
159264
if !image.is_null() {
160265
let ns_size: objc2_foundation::NSSize = unsafe { msg_send![image, size] };
@@ -172,7 +277,15 @@ unsafe fn enumerate_dragging_frames(drag_info: &AnyObject) -> (f64, f64) {
172277
let Some(nsurl_cls) = AnyClass::get(c"NSURL") else {
173278
return (0.0, 0.0);
174279
};
175-
let nsarray_cls = AnyClass::get(c"NSArray").expect("NSArray class must exist");
280+
let Some(nsarray_cls) = AnyClass::get(c"NSArray") else {
281+
warn_once(
282+
&WARNED_NSARRAY_MISSING,
283+
"drag_image_detection: NSArray class not found — drag frame enumeration is disabled. \
284+
NSArray is a core Foundation class; if it's missing, something is fundamentally \
285+
wrong with the ObjC runtime. Check if Foundation is loaded correctly.",
286+
);
287+
return (0.0, 0.0);
288+
};
176289
let class_array: *const AnyObject =
177290
unsafe { msg_send![nsarray_cls, arrayWithObject: nsurl_cls as *const AnyClass] };
178291
if class_array.is_null() {

0 commit comments

Comments
 (0)