mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-05-08 07:08:05 +03:00
macos: re-enable CGEventTap on tap timeout
The kernel disables a session-level CGEventTap when its callback runs longer than ~1 s on a single event — typical causes are heavy load, scheduler contention, or the process being briefly suspended (App Nap on a long idle, debugger pause). It is not a fatal condition: Apple's documented recovery is to call CGEventTapEnable and resume processing. Before this change the tap stayed dead until the user manually clicked Re-enable from the menubar. Stash the tap's mach port pointer in an Arc<OnceLock<usize>> set immediately after CGEventTap::new returns, and on TapDisabledByTimeout call CGEventTapEnable from the callback to revive the tap while preserving capture state — the user doesn't see the cursor pop back to the local screen mid-session for a transient slow callback. TapDisabledByUserInput keeps the existing teardown path: those causes (TCC Accessibility revoked mid-session, secure-input mode, explicit kill) are not safely recoverable from inside the callback, and the existing fallthrough-fix from59d9e45/d1e963estill applies there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
Ferdinand Schober
parent
373e382152
commit
3b4b3a51aa
@@ -2,7 +2,7 @@ use super::{Capture, CaptureError, CaptureEvent, Position, error::MacosCaptureCr
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use core_foundation::{
|
use core_foundation::{
|
||||||
base::{CFRelease, kCFAllocatorDefault},
|
base::{CFRelease, TCFType, kCFAllocatorDefault},
|
||||||
date::CFTimeInterval,
|
date::CFTimeInterval,
|
||||||
number::{CFBooleanRef, kCFBooleanTrue},
|
number::{CFBooleanRef, kCFBooleanTrue},
|
||||||
runloop::{CFRunLoop, CFRunLoopSource, kCFRunLoopCommonModes},
|
runloop::{CFRunLoop, CFRunLoopSource, kCFRunLoopCommonModes},
|
||||||
@@ -28,7 +28,7 @@ use std::{
|
|||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
ffi::{CString, c_char},
|
ffi::{CString, c_char},
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::Arc,
|
sync::{Arc, OnceLock},
|
||||||
task::{Context, Poll, ready},
|
task::{Context, Poll, ready},
|
||||||
thread::{self},
|
thread::{self},
|
||||||
};
|
};
|
||||||
@@ -395,6 +395,14 @@ fn create_event_tap<'a>(
|
|||||||
notify_tx: Sender<ProducerEvent>,
|
notify_tx: Sender<ProducerEvent>,
|
||||||
event_tx: Sender<(Position, CaptureEvent)>,
|
event_tx: Sender<(Position, CaptureEvent)>,
|
||||||
) -> Result<CGEventTap<'a>, MacosCaptureCreationError> {
|
) -> Result<CGEventTap<'a>, MacosCaptureCreationError> {
|
||||||
|
// Shared slot for the tap's mach port pointer. Stored as `usize`
|
||||||
|
// because raw pointers aren't `Send`, but the integer
|
||||||
|
// representation is — and CGEventTapEnable is documented as
|
||||||
|
// thread-safe. Set immediately after CGEventTap::new returns;
|
||||||
|
// read by the callback to recover from a TapDisabledByTimeout.
|
||||||
|
let tap_mach_port: Arc<OnceLock<usize>> = Arc::new(OnceLock::new());
|
||||||
|
let tap_mach_port_cb = Arc::clone(&tap_mach_port);
|
||||||
|
|
||||||
let cg_events_of_interest: Vec<CGEventType> = vec![
|
let cg_events_of_interest: Vec<CGEventType> = vec![
|
||||||
CGEventType::LeftMouseDown,
|
CGEventType::LeftMouseDown,
|
||||||
CGEventType::LeftMouseUp,
|
CGEventType::LeftMouseUp,
|
||||||
@@ -419,21 +427,43 @@ fn create_event_tap<'a>(
|
|||||||
let mut capture_position = None;
|
let mut capture_position = None;
|
||||||
let mut res_events = vec![];
|
let mut res_events = vec![];
|
||||||
|
|
||||||
if matches!(
|
if matches!(event_type, CGEventType::TapDisabledByTimeout) {
|
||||||
event_type,
|
// The kernel disables the tap when our callback runs
|
||||||
CGEventType::TapDisabledByTimeout | CGEventType::TapDisabledByUserInput
|
// longer than ~1s on a single event — typical causes
|
||||||
) {
|
// are heavy load, scheduler contention, or this
|
||||||
// When the tap is disabled (including the case where TCC
|
// process being briefly suspended (e.g. App Nap on a
|
||||||
// Accessibility is revoked mid-session), we MUST drop
|
// long idle). It is NOT a fatal condition: Apple's
|
||||||
// captured state synchronously and return Keep on this
|
// documented recovery is to call CGEventTapEnable
|
||||||
// event. Otherwise the `current_pos.is_some()` branch
|
// and resume processing. Re-enable in place and KEEP
|
||||||
// below would drop this event (and any racing callback
|
// existing capture state so the user doesn't see the
|
||||||
// still in flight) back into `CallbackResult::Drop`,
|
// cursor pop back to the local screen mid-session.
|
||||||
// silently eating the user's clicks and keypresses while
|
if let Some(&port) = tap_mach_port_cb.get() {
|
||||||
// the tap winds down. Clear state + show the cursor
|
log::warn!("CGEventTap disabled by timeout — re-enabling");
|
||||||
// here, then notify the producer loop so the service
|
unsafe {
|
||||||
// can tear down cleanly.
|
CGEventTapEnable(port as *mut c_void, true);
|
||||||
log::error!("CGEventTap disabled, releasing capture state");
|
}
|
||||||
|
} else {
|
||||||
|
log::error!(
|
||||||
|
"CGEventTap disabled by timeout, but mach port not yet stored — cannot re-enable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return CallbackResult::Keep;
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(event_type, CGEventType::TapDisabledByUserInput) {
|
||||||
|
// Deliberate kill — secure-input mode (e.g. password
|
||||||
|
// field), TCC Accessibility revoked mid-session, or
|
||||||
|
// the user disabling event-monitoring. We can't
|
||||||
|
// recover from this; drop captured state synchronously
|
||||||
|
// and return Keep on this event. Otherwise the
|
||||||
|
// `current_pos.is_some()` branch below would drop this
|
||||||
|
// event (and any racing callback still in flight) back
|
||||||
|
// into `CallbackResult::Drop`, silently eating the
|
||||||
|
// user's clicks and keypresses while the tap winds
|
||||||
|
// down. Clear state + show the cursor here, then
|
||||||
|
// notify the producer loop so the service can tear
|
||||||
|
// down cleanly.
|
||||||
|
log::error!("CGEventTap disabled by user input, releasing capture state");
|
||||||
if state.current_pos.is_some() {
|
if state.current_pos.is_some() {
|
||||||
let _ = CGDisplay::show_cursor(&CGDisplay::main());
|
let _ = CGDisplay::show_cursor(&CGDisplay::main());
|
||||||
state.current_pos = None;
|
state.current_pos = None;
|
||||||
@@ -507,6 +537,13 @@ fn create_event_tap<'a>(
|
|||||||
)
|
)
|
||||||
.map_err(|_| MacosCaptureCreationError::EventTapCreation)?;
|
.map_err(|_| MacosCaptureCreationError::EventTapCreation)?;
|
||||||
|
|
||||||
|
// Hand the mach port pointer to the callback so it can re-enable
|
||||||
|
// the tap on TapDisabledByTimeout. The pointer is valid for the
|
||||||
|
// lifetime of `tap` (which lives on the event-tap thread until
|
||||||
|
// the run loop exits).
|
||||||
|
let port_ptr = tap.mach_port().as_concrete_TypeRef() as usize;
|
||||||
|
let _ = tap_mach_port.set(port_ptr);
|
||||||
|
|
||||||
let tap_source: CFRunLoopSource = tap
|
let tap_source: CFRunLoopSource = tap
|
||||||
.mach_port()
|
.mach_port()
|
||||||
.create_runloop_source(0)
|
.create_runloop_source(0)
|
||||||
@@ -711,6 +748,12 @@ extern "C" {
|
|||||||
seconds: CFTimeInterval,
|
seconds: CFTimeInterval,
|
||||||
);
|
);
|
||||||
fn CGPreflightListenEventAccess() -> bool;
|
fn CGPreflightListenEventAccess() -> bool;
|
||||||
|
/// Re-enable an event tap that was disabled by a
|
||||||
|
/// `kCGEventTapDisabledByTimeout` event. The Apple-documented
|
||||||
|
/// recovery path: see Quartz Event Services Reference. The `tap`
|
||||||
|
/// argument is a `CFMachPortRef`; we pass the raw pointer so we
|
||||||
|
/// can store it as `usize` for cross-thread sharing.
|
||||||
|
fn CGEventTapEnable(tap: *mut c_void, enable: bool);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[link(name = "ApplicationServices", kind = "framework")]
|
#[link(name = "ApplicationServices", kind = "framework")]
|
||||||
|
|||||||
Reference in New Issue
Block a user