From 011ff4e178bf5186df4630c99e74c87ba117e2e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 14:46:01 -0700 Subject: [PATCH] Fix sidecar crash recovery and Windows console window - Fix is_running() to check actual process liveness via try_wait() instead of just checking if the handle exists - Auto-restart sidecar on pipe errors (broken pipe, closed stdout) with one retry attempt - Hide sidecar console window on Windows (CREATE_NO_WINDOW flag) - Log sidecar stderr to sidecar.log file for crash diagnostics - Include exit status in error message when sidecar fails to start Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/sidecar/mod.rs | 114 +++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/sidecar/mod.rs b/src-tauri/src/sidecar/mod.rs index bd7ba28..738e022 100644 --- a/src-tauri/src/sidecar/mod.rs +++ b/src-tauri/src/sidecar/mod.rs @@ -6,6 +6,9 @@ use std::path::{Path, PathBuf}; use std::process::{Child, ChildStdin, Command, Stdio}; use std::sync::{Mutex, OnceLock}; +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; + use crate::sidecar::messages::IPCMessage; /// Resource directory set by the Tauri app during setup. @@ -231,10 +234,28 @@ impl SidecarManager { self.stop().ok(); eprintln!("[sidecar-rs] Starting frozen sidecar: {}", path.display()); - let child = Command::new(path) - .stdin(Stdio::piped()) + // Log sidecar stderr to a file for diagnostics + let stderr_cfg = if let Some(data_dir) = DATA_DIR.get() { + let log_path = data_dir.join("sidecar.log"); + eprintln!("[sidecar-rs] Sidecar stderr → {}", log_path.display()); + match std::fs::File::create(&log_path) { + Ok(f) => Stdio::from(f), + Err(_) => Stdio::inherit(), + } + } else { + Stdio::inherit() + }; + + let mut cmd = Command::new(path); + cmd.stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) + .stderr(stderr_cfg); + + // Hide the console window on Windows (CREATE_NO_WINDOW = 0x08000000) + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); + + let child = cmd .spawn() .map_err(|e| format!("Failed to start sidecar binary: {e}"))?; @@ -300,7 +321,22 @@ impl SidecarManager { .read_line(&mut line) .map_err(|e| format!("Read error: {e}"))?; if bytes == 0 { - return Err("Sidecar closed stdout before sending ready".to_string()); + // Try to get the exit code for diagnostics + let exit_info = { + let mut proc = self.process.lock().map_err(|e| e.to_string())?; + if let Some(ref mut child) = *proc { + match child.try_wait() { + Ok(Some(status)) => format!(" (exit status: {status})"), + _ => String::new(), + } + } else { + String::new() + } + }; + return Err(format!( + "Sidecar closed stdout before sending ready{exit_info}. \ + The Python sidecar may have crashed on startup — check app logs for details." + )); } let trimmed = line.trim(); if trimmed.is_empty() { @@ -330,11 +366,46 @@ impl SidecarManager { /// Send a message and receive the response, calling a callback for intermediate messages. /// Intermediate messages include progress, pipeline.segment, and pipeline.speaker_update. + /// + /// If the sidecar has crashed (broken pipe), automatically restarts it and retries once. pub fn send_and_receive_with_progress( &self, msg: &IPCMessage, on_intermediate: F, ) -> Result + where + F: Fn(&IPCMessage), + { + match self.send_and_receive_inner(msg, &on_intermediate) { + Ok(response) => Ok(response), + Err(e) + if e.contains("Write error") + || e.contains("closed stdout") + || e.contains("not available") => + { + eprintln!("[sidecar-rs] Sidecar communication failed ({e}), restarting..."); + self.cleanup_handles(); + // Stop any zombie process + { + let mut proc = self.process.lock().map_err(|e| e.to_string())?; + if let Some(ref mut child) = proc.take() { + let _ = child.kill(); + let _ = child.wait(); + } + } + self.ensure_running()?; + self.send_and_receive_inner(msg, &on_intermediate) + } + Err(e) => Err(e), + } + } + + /// Inner implementation of send_and_receive. + fn send_and_receive_inner( + &self, + msg: &IPCMessage, + on_intermediate: &F, + ) -> Result where F: Fn(&IPCMessage), { @@ -420,8 +491,39 @@ impl SidecarManager { } pub fn is_running(&self) -> bool { - let proc = self.process.lock().ok(); - proc.map_or(false, |p| p.is_some()) + let mut proc = match self.process.lock() { + Ok(p) => p, + Err(_) => return false, + }; + if let Some(ref mut child) = *proc { + // Check if the process has exited + match child.try_wait() { + Ok(Some(_status)) => { + // Process has exited — clean up handles + eprintln!("[sidecar-rs] Sidecar process has exited"); + drop(proc); + let _ = self.cleanup_handles(); + false + } + Ok(None) => true, // Still running + Err(_) => false, + } + } else { + false + } + } + + /// Clean up stdin/stdout/process handles after the sidecar has exited. + fn cleanup_handles(&self) { + if let Ok(mut s) = self.stdin.lock() { + *s = None; + } + if let Ok(mut r) = self.reader.lock() { + *r = None; + } + if let Ok(mut p) = self.process.lock() { + *p = None; + } } }