From 304211a25f06be2ea5a6235a3c38dcebbf3bc480 Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Tue, 1 Sep 2026 22:26:25 +0800 Subject: [PATCH] cursor tracker start loop bug fix --- src-tauri/src/application/mod.rs | 12 ++-- src-tauri/src/cursor/mod.rs | 119 +++++++++++++++++++++---------- src-tauri/src/macos/mod.rs | 22 ++++-- src-tauri/src/onboarding/mod.rs | 2 +- 4 files changed, 104 insertions(+), 51 deletions(-) diff --git a/src-tauri/src/application/mod.rs b/src-tauri/src/application/mod.rs index 1b38e05..ff42539 100644 --- a/src-tauri/src/application/mod.rs +++ b/src-tauri/src/application/mod.rs @@ -15,7 +15,7 @@ pub async fn init(handle: &AppHandle) -> Result<(), String> { .map_err(crate::db::command_error)?; if settings.onboarding_done { - start(handle); + start(handle).await; if cfg!(target_os = "macos") && !accessibility_permission_granted { crate::ui::onboarding::show_accessibility_page(handle)?; } @@ -25,7 +25,7 @@ pub async fn init(handle: &AppHandle) -> Result<(), String> { Ok(()) } -pub fn start(handle: &AppHandle) { +pub async fn start(handle: &AppHandle) { let state = handle.state::(); if state .started @@ -36,7 +36,7 @@ pub fn start(handle: &AppHandle) { } crate::ui::init(handle); - reconcile_cursor(handle); + reconcile_cursor(handle).await; } pub fn is_started(handle: &AppHandle) -> bool { @@ -46,12 +46,12 @@ pub fn is_started(handle: &AppHandle) -> bool { .load(Ordering::Acquire) } -pub fn reconcile_cursor(handle: &AppHandle) { +pub async fn reconcile_cursor(handle: &AppHandle) { if !is_started(handle) || !crate::macos::accessibility_permission_granted(handle) { return; } - if let Err(error) = crate::cursor::start_tracking(handle) { - eprintln!("failed to initialize cursor tracking; will retry: {error}"); + if let Err(error) = crate::cursor::start_tracking(handle).await { + eprintln!("failed to initialize cursor tracking: {error}"); } } diff --git a/src-tauri/src/cursor/mod.rs b/src-tauri/src/cursor/mod.rs index 172feaa..ddefed2 100644 --- a/src-tauri/src/cursor/mod.rs +++ b/src-tauri/src/cursor/mod.rs @@ -8,8 +8,7 @@ use std::sync::RwLock; use std::time::Duration; use tauri::{AppHandle, Manager}; use tauri_specta::Event; -use tokio::sync::mpsc; -use tokio::sync::watch; +use tokio::sync::{mpsc, oneshot, watch}; #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Type)] #[serde(rename_all = "camelCase")] @@ -104,13 +103,18 @@ fn current_position(handle: &AppHandle) -> Result { // complains even when there's no external references. // Possibly because of `lazy_static!`. // Just leave it public I guess. -pub struct CursorTask { +struct CursorTask { stop_tx: watch::Sender, task: tauri::async_runtime::JoinHandle<()>, } +enum CursorTracker { + Starting, + Running(CursorTask), +} + lazy_static! { - pub static ref CURSOR_TASK: Mutex> = Mutex::new(None); + static ref CURSOR_TRACKER: Mutex> = Mutex::new(None); } pub fn init(app: &AppHandle) { @@ -118,47 +122,71 @@ pub fn init(app: &AppHandle) { } /// Starts cursor tracking after Accessibility permission is available. -pub fn start_tracking(app: &AppHandle) -> Result<(), String> { - println!("init_cursor_tracking called"); - +pub async fn start_tracking(app: &AppHandle) -> Result<(), String> { if !crate::macos::accessibility_permission_granted(app) { return Err("macOS Accessibility permission has not been granted".to_owned()); } - let mut tracker = match CURSOR_TASK.lock() { - Ok(tracker) => tracker, - Err(e) => { - return Err(format!("Failed to lock cursor tracker state: {e}")); - } - }; - - if tracker.is_some() { - println!("Cursor tracking already initialized"); - return Ok(()); - } - - match current_position(app) { - Ok(positions) => update_cursor_position(app, positions), - Err(error) => eprintln!("Failed to resolve current cursor position: {error}"), - } - - let (stop_tx, stop_rx) = watch::channel(false); - - println!("Spawning cursor tracking task"); - let primary_monitor = app .primary_monitor() .map_err(|error| error.to_string())? .ok_or_else(|| "Failed to resolve primary monitor".to_owned())?; + { + let mut tracker = CURSOR_TRACKER + .lock() + .map_err(|error| format!("Failed to lock cursor tracker state: {error}"))?; + if tracker.is_some() { + return Ok(()); + } + *tracker = Some(CursorTracker::Starting); + } + + println!("Initializing cursor tracking..."); + match current_position(app) { + Ok(positions) => update_cursor_position(app, positions), + Err(error) => eprintln!("Failed to resolve current cursor position: {error}"), + } + + let (stop_tx, stop_rx) = watch::channel(false); + let (ready_tx, ready_rx) = oneshot::channel(); + let handle = app.clone(); let task = tauri::async_runtime::spawn(async move { - if let Err(e) = init_cursor_tracking_i(stop_rx, primary_monitor, handle).await { + if let Err(e) = init_cursor_tracking_i(stop_rx, primary_monitor, handle, ready_tx).await { println!("Failed to initialize cursor tracking: {}", e); } }); - *tracker = Some(CursorTask { stop_tx, task }); + let startup = ready_rx + .await + .unwrap_or_else(|_| Err("Cursor tracking task stopped during initialization".to_owned())); + if let Err(error) = startup { + if let Ok(mut tracker) = CURSOR_TRACKER.lock() + && matches!(*tracker, Some(CursorTracker::Starting)) + { + *tracker = None; + } + let _ = task.await; + return Err(error); + } + + let installed = { + let mut tracker = CURSOR_TRACKER + .lock() + .map_err(|error| format!("Failed to lock cursor tracker state: {error}"))?; + if matches!(*tracker, Some(CursorTracker::Starting)) { + *tracker = Some(CursorTracker::Running(CursorTask { stop_tx, task })); + true + } else { + false + } + }; + + if !installed { + return Err("Cursor tracking startup was cancelled".to_owned()); + } + println!("EVENT: Cursor Tracker Enabled"); Ok(()) } @@ -297,7 +325,7 @@ mod tests { pub async fn stop_cursor_tracking() { println!("stop_cursor_tracking called"); - let tracker = match CURSOR_TASK.lock() { + let tracker = match CURSOR_TRACKER.lock() { Ok(mut tracker) => tracker.take(), Err(e) => { println!("Failed to lock cursor tracker state: {}", e); @@ -305,9 +333,16 @@ pub async fn stop_cursor_tracking() { } }; - let Some(tracker) = tracker else { - println!("Cursor tracking is not running"); - return; + let tracker = match tracker { + Some(CursorTracker::Running(tracker)) => tracker, + Some(CursorTracker::Starting) => { + println!("Cursor tracking startup cancelled"); + return; + } + None => { + println!("Cursor tracking is not running"); + return; + } }; if let Err(e) = tracker.stop_tx.send(true) { @@ -325,9 +360,8 @@ async fn init_cursor_tracking_i( mut stop_rx: watch::Receiver, monitor: tauri::Monitor, handle: AppHandle, + ready_tx: oneshot::Sender>, ) -> Result<(), String> { - println!("Initializing cursor tracking..."); - // Create a channel to decouple event generation (producer) from processing (consumer). // Capacity 100 is plenty for 500ms polling (2Hz). let (tx, mut rx) = mpsc::channel::(100); @@ -345,8 +379,14 @@ async fn init_cursor_tracking_i( println!("Cursor event consumer stopped (channel closed)"); }); - let device_state = DeviceEventsHandler::new(Duration::from_millis(500)) - .ok_or("Failed to create device event handler (already running?)")?; + let device_state = match DeviceEventsHandler::new(Duration::from_millis(500)) { + Some(device_state) => device_state, + None => { + let error = "Failed to create device event handler (already running?)".to_owned(); + let _ = ready_tx.send(Err(error.clone())); + return Err(error); + } + }; println!("Device event handler created successfully"); println!("Setting up mouse move handler for event broadcasting..."); @@ -383,6 +423,9 @@ async fn init_cursor_tracking_i( } }); + ready_tx + .send(Ok(())) + .map_err(|_| "Cursor tracking startup was cancelled".to_owned())?; println!("Mouse move handler registered - now broadcasting cursor events to all windows"); // Keep the handler alive while tracking is enabled. diff --git a/src-tauri/src/macos/mod.rs b/src-tauri/src/macos/mod.rs index aaefd99..fe75f1e 100644 --- a/src-tauri/src/macos/mod.rs +++ b/src-tauri/src/macos/mod.rs @@ -49,11 +49,6 @@ pub fn accessibility_permission_granted(handle: &AppHandle) -> bool { } async fn apply_permission_state(handle: &AppHandle, granted: bool) -> Result { - if handle.state::().granted() == granted { - crate::application::reconcile_cursor(handle); - return Ok(false); - } - if !handle .state::() .replace(granted) @@ -64,7 +59,7 @@ async fn apply_permission_state(handle: &AppHandle, granted: bool) -> Result Result