mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-05-09 15:48:04 +03:00
macos: quit immediately when Accessibility is revoked mid-session
Even with the earlier event-tap-callback cleanup fix, revoking Accessibility in System Settings while Lan Mouse was running with an active capture tap could still leave system input wedged — clicks and keypresses silently dropped until the app was force-quit. The reliable fix is to not rely on in-process tap teardown at all. When AX is revoked: - The kernel guarantees an active CGEventTap is dismantled when the owning process exits. - SIGINT+wait on the daemon child (main.rs already does this on GUI exit) drops the daemon's tap and restores the cursor. So: continuously poll AX state (1-second GLib timer, replacing the one-shot grant watcher), and on a revoke transition call `app.quit()`. Input is restored within ~1-2 seconds regardless of capture state — no force-quit required, no stuck cursor, no silently consumed events beyond the brief window until the poller fires. The grant-transition case is preserved: on a 0→1 flip the warning row swaps to its "relaunch required" state, same as before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
Ferdinand Schober
parent
07cc40f6ba
commit
94e9301e9c
@@ -200,18 +200,32 @@ fn build_ui(app: &Application) {
|
|||||||
macos_status_item::setup(app, &window);
|
macos_status_item::setup(app, &window);
|
||||||
// First-launch TCC prompts. No-op when already granted.
|
// First-launch TCC prompts. No-op when already granted.
|
||||||
macos_privacy::fire_initial_prompts();
|
macos_privacy::fire_initial_prompts();
|
||||||
// If Accessibility wasn't granted at startup, watch for the grant
|
// Watch the Accessibility grant continuously for the lifetime
|
||||||
// and switch the status row into its "relaunch required" state
|
// of the process. On a grant, swap the warning row into its
|
||||||
// when it lands. The daemon subprocess initialized without AX
|
// "relaunch required" state (the daemon subprocess already
|
||||||
// (bailed with "accessibility permission is required") and can't
|
// bailed and can't recover without a restart). On a REVOKE,
|
||||||
// recover without a restart, so a live AX toggle without a
|
// quit immediately — an active CGEventTap at
|
||||||
// relaunch leaves the app in a broken state otherwise.
|
// HeadInsertEventTap can wedge system input if the process
|
||||||
|
// lingers after losing AX, and forcing the process to exit is
|
||||||
|
// the only bulletproof way to guarantee the kernel tears the
|
||||||
|
// tap down.
|
||||||
let window_weak = window.downgrade();
|
let window_weak = window.downgrade();
|
||||||
macos_privacy::watch_for_accessibility_grant(move || {
|
let app_weak = app.downgrade();
|
||||||
|
macos_privacy::watch_accessibility_state(move |change| match change {
|
||||||
|
macos_privacy::AccessibilityChange::Granted => {
|
||||||
if let Some(window) = window_weak.upgrade() {
|
if let Some(window) = window_weak.upgrade() {
|
||||||
window.present();
|
window.present();
|
||||||
window.refresh_capture_emulation_status();
|
window.refresh_capture_emulation_status();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
macos_privacy::AccessibilityChange::Revoked => {
|
||||||
|
log::warn!(
|
||||||
|
"Accessibility revoked — quitting to avoid wedging system input"
|
||||||
|
);
|
||||||
|
if let Some(app) = app_weak.upgrade() {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,29 +74,46 @@ pub fn accessibility_granted() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Poll for an Accessibility grant transition. Starts a 1-second GLib
|
pub enum AccessibilityChange {
|
||||||
/// timer that fires `on_granted` once, the first time
|
/// AX was missing at startup and the user has now granted it.
|
||||||
/// `AXIsProcessTrusted()` returns true. A no-op if AX is already granted.
|
/// Capture/emulation still need a relaunch to take effect, since
|
||||||
|
/// the daemon subprocess already bailed.
|
||||||
|
Granted,
|
||||||
|
/// AX was granted and the user has now revoked it. Quit immediately
|
||||||
|
/// — leaving the process alive with an active CGEventTap at
|
||||||
|
/// HeadInsertEventTap can wedge system input (clicks/keys silently
|
||||||
|
/// consumed) until the process dies. See
|
||||||
|
/// macos-cgeventtap-drop-fallthrough-tcc-revoke skill for the
|
||||||
|
/// underlying event-tap-disable footgun.
|
||||||
|
Revoked,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll for Accessibility grant/revoke transitions. Starts a 1-second
|
||||||
|
/// GLib timer that fires `on_change` every time `AXIsProcessTrusted()`
|
||||||
|
/// flips, and keeps running for the lifetime of the process.
|
||||||
///
|
///
|
||||||
/// We rely on polling rather than AXObserver because the AX notification
|
/// We rely on polling rather than AXObserver because the AX notification
|
||||||
/// API requires a trusted process to subscribe — the precondition we're
|
/// API requires a trusted process to subscribe — the precondition we
|
||||||
/// waiting for. This runs on the GTK main thread (via timeout_add_seconds_local).
|
/// can't assume. This runs on the GTK main thread (via
|
||||||
pub fn watch_for_accessibility_grant<F>(mut on_granted: F)
|
/// `timeout_add_seconds_local`).
|
||||||
|
pub fn watch_accessibility_state<F>(mut on_change: F)
|
||||||
where
|
where
|
||||||
F: FnMut() + 'static,
|
F: FnMut(AccessibilityChange) + 'static,
|
||||||
{
|
{
|
||||||
if accessibility_granted() {
|
let mut last = accessibility_granted();
|
||||||
return;
|
log::info!("watching Accessibility state (initial = {last})");
|
||||||
}
|
|
||||||
log::info!("watching for Accessibility grant");
|
|
||||||
glib::timeout_add_seconds_local(1, move || {
|
glib::timeout_add_seconds_local(1, move || {
|
||||||
if accessibility_granted() {
|
let current = accessibility_granted();
|
||||||
log::info!("Accessibility granted; firing relaunch prompt");
|
if current != last {
|
||||||
on_granted();
|
log::info!("Accessibility state flip: {last} -> {current}");
|
||||||
glib::ControlFlow::Break
|
on_change(if current {
|
||||||
|
AccessibilityChange::Granted
|
||||||
} else {
|
} else {
|
||||||
glib::ControlFlow::Continue
|
AccessibilityChange::Revoked
|
||||||
|
});
|
||||||
|
last = current;
|
||||||
}
|
}
|
||||||
|
glib::ControlFlow::Continue
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user