diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 28b01f4..d61d26e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -29,9 +29,9 @@ pub fn run() { .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_process::init()) - .manage(sidecar::ManagedSidecar(Mutex::new( + .manage(sidecar::ManagedSidecar(std::sync::Arc::new(Mutex::new( sidecar::SidecarManager::new(), - ))) + )))) .setup(|app| { let resource_dir = app .path() diff --git a/src-tauri/src/sidecar/mod.rs b/src-tauri/src/sidecar/mod.rs index 9ffb970..1165704 100644 --- a/src-tauri/src/sidecar/mod.rs +++ b/src-tauri/src/sidecar/mod.rs @@ -587,23 +587,44 @@ impl SidecarManager { } fn wait_for_ready(stdout: std::process::ChildStdout) -> Result { - let reader = std::io::BufReader::new(stdout); - let timeout = std::time::Duration::from_secs(120); - let start = std::time::Instant::now(); + use std::sync::mpsc; - for line in reader.lines() { - if start.elapsed() > timeout { - return Err("Timed out waiting for sidecar ready event".into()); - } - let line = line.map_err(|e| format!("IO error reading stdout: {e}"))?; - if let Ok(evt) = serde_json::from_str::(&line) { - if evt.event == "ready" { - return Ok(evt.port); + let timeout = std::time::Duration::from_secs(120); + + // Read stdout in a background thread so we can enforce a real timeout. + // BufReader::lines() blocks indefinitely if no data arrives. + let (tx, rx) = mpsc::channel(); + + std::thread::spawn(move || { + let reader = std::io::BufReader::new(stdout); + for line in reader.lines() { + match line { + Ok(line) => { + eprintln!("[sidecar-stdout] {}", line); + if let Ok(evt) = serde_json::from_str::(&line) { + if evt.event == "ready" { + let _ = tx.send(Ok(evt.port)); + return; + } + } + } + Err(e) => { + let _ = tx.send(Err(format!("IO error reading stdout: {e}"))); + return; + } } } - // Ignore other lines (e.g. log output) - } - Err("Sidecar process exited before sending ready event".into()) + let _ = tx.send(Err( + "Sidecar process exited before sending ready event".into(), + )); + }); + + rx.recv_timeout(timeout).unwrap_or_else(|_| { + Err(format!( + "Timed out after {}s waiting for sidecar ready event", + timeout.as_secs() + )) + }) } } @@ -612,7 +633,8 @@ impl SidecarManager { // --------------------------------------------------------------------------- /// Wrapper so we can store `SidecarManager` in Tauri's managed state. -pub struct ManagedSidecar(pub Mutex); +/// Uses Arc so it can be cloned into background threads for async commands. +pub struct ManagedSidecar(pub std::sync::Arc>); #[tauri::command] pub fn get_sidecar_port(state: tauri::State<'_, ManagedSidecar>) -> Result, String> { @@ -628,12 +650,16 @@ pub fn get_sidecar_port(state: tauri::State<'_, ManagedSidecar>) -> Result) -> Result { - let mut mgr = state - .0 - .lock() - .map_err(|e| format!("Lock error: {e}"))?; - mgr.ensure_running() +pub async fn start_sidecar(state: tauri::State<'_, ManagedSidecar>) -> Result { + let mgr = state.0.clone(); + // Run blocking sidecar launch in a background thread so it doesn't + // freeze the Tauri UI while waiting for the ready event (up to 120s). + tokio::task::spawn_blocking(move || { + let mut mgr = mgr.lock().map_err(|e| format!("Lock error: {e}"))?; + mgr.ensure_running() + }) + .await + .map_err(|e| format!("Task join error: {e}"))? } #[tauri::command]