Fix app hanging on sidecar startup
All checks were successful
Release / Run Tests (push) Successful in 11s
Tests / Python Backend Tests (push) Successful in 4s
Tests / Frontend Tests (push) Successful in 7s
Tests / Rust Sidecar Tests (push) Successful in 3m13s
Release / Bump version and tag (push) Successful in 14s

Two issues caused the app to freeze on "Starting sidecar...":

1. wait_for_ready() used a blocking BufReader::lines() iterator
   with a timeout check between lines. If the sidecar produced no
   stdout output (crashed, missing binary, or slow model loading),
   the read blocked forever. Now uses a background thread with
   mpsc::recv_timeout() for a real 120s deadline.

2. start_sidecar was a synchronous Tauri command that blocked the
   main thread during the entire sidecar startup (up to 120s).
   Now async via tokio::spawn_blocking, keeping the UI responsive.

Also logs all sidecar stdout lines to stderr with [sidecar-stdout]
prefix for debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Developer
2026-04-07 12:11:04 -07:00
parent 36b4f7dad5
commit 5d22adcaa4
2 changed files with 49 additions and 23 deletions

View File

@@ -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()

View File

@@ -587,23 +587,44 @@ impl SidecarManager {
}
fn wait_for_ready(stdout: std::process::ChildStdout) -> Result<u16, String> {
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;
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() {
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}"))?;
match line {
Ok(line) => {
eprintln!("[sidecar-stdout] {}", line);
if let Ok(evt) = serde_json::from_str::<ReadyEvent>(&line) {
if evt.event == "ready" {
return Ok(evt.port);
let _ = tx.send(Ok(evt.port));
return;
}
}
// Ignore other lines (e.g. log output)
}
Err("Sidecar process exited before sending ready event".into())
Err(e) => {
let _ = tx.send(Err(format!("IO error reading stdout: {e}")));
return;
}
}
}
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<SidecarManager>);
/// Uses Arc so it can be cloned into background threads for async commands.
pub struct ManagedSidecar(pub std::sync::Arc<Mutex<SidecarManager>>);
#[tauri::command]
pub fn get_sidecar_port(state: tauri::State<'_, ManagedSidecar>) -> Result<Option<u16>, String> {
@@ -628,12 +650,16 @@ pub fn get_sidecar_port(state: tauri::State<'_, ManagedSidecar>) -> Result<Optio
}
#[tauri::command]
pub fn start_sidecar(state: tauri::State<'_, ManagedSidecar>) -> Result<u16, String> {
let mut mgr = state
.0
.lock()
.map_err(|e| format!("Lock error: {e}"))?;
pub async fn start_sidecar(state: tauri::State<'_, ManagedSidecar>) -> Result<u16, String> {
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]