Fix app hanging on sidecar startup
All checks were successful
All checks were successful
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:
@@ -29,9 +29,9 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.manage(sidecar::ManagedSidecar(Mutex::new(
|
.manage(sidecar::ManagedSidecar(std::sync::Arc::new(Mutex::new(
|
||||||
sidecar::SidecarManager::new(),
|
sidecar::SidecarManager::new(),
|
||||||
)))
|
))))
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let resource_dir = app
|
let resource_dir = app
|
||||||
.path()
|
.path()
|
||||||
|
|||||||
@@ -587,23 +587,44 @@ impl SidecarManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_ready(stdout: std::process::ChildStdout) -> Result<u16, String> {
|
fn wait_for_ready(stdout: std::process::ChildStdout) -> Result<u16, String> {
|
||||||
let reader = std::io::BufReader::new(stdout);
|
use std::sync::mpsc;
|
||||||
let timeout = std::time::Duration::from_secs(120);
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
|
|
||||||
|
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() {
|
for line in reader.lines() {
|
||||||
if start.elapsed() > timeout {
|
match line {
|
||||||
return Err("Timed out waiting for sidecar ready event".into());
|
Ok(line) => {
|
||||||
}
|
eprintln!("[sidecar-stdout] {}", line);
|
||||||
let line = line.map_err(|e| format!("IO error reading stdout: {e}"))?;
|
|
||||||
if let Ok(evt) = serde_json::from_str::<ReadyEvent>(&line) {
|
if let Ok(evt) = serde_json::from_str::<ReadyEvent>(&line) {
|
||||||
if evt.event == "ready" {
|
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.
|
/// 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]
|
#[tauri::command]
|
||||||
pub fn get_sidecar_port(state: tauri::State<'_, ManagedSidecar>) -> Result<Option<u16>, String> {
|
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]
|
#[tauri::command]
|
||||||
pub fn start_sidecar(state: tauri::State<'_, ManagedSidecar>) -> Result<u16, String> {
|
pub async fn start_sidecar(state: tauri::State<'_, ManagedSidecar>) -> Result<u16, String> {
|
||||||
let mut mgr = state
|
let mgr = state.0.clone();
|
||||||
.0
|
// Run blocking sidecar launch in a background thread so it doesn't
|
||||||
.lock()
|
// freeze the Tauri UI while waiting for the ready event (up to 120s).
|
||||||
.map_err(|e| format!("Lock error: {e}"))?;
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let mut mgr = mgr.lock().map_err(|e| format!("Lock error: {e}"))?;
|
||||||
mgr.ensure_running()
|
mgr.ensure_running()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Task join error: {e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
Reference in New Issue
Block a user