Fix sidecar crash recovery and Windows console window
All checks were successful
Release / Bump version and tag (push) Successful in 3s
Release / Build (macOS) (push) Successful in 4m40s
Release / Build (Linux) (push) Successful in 7m34s
Release / Build (Windows) (push) Successful in 17m2s

- 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 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-21 14:46:01 -07:00
parent d2d111a5c7
commit 011ff4e178

View File

@@ -6,6 +6,9 @@ use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio}; use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::{Mutex, OnceLock}; use std::sync::{Mutex, OnceLock};
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use crate::sidecar::messages::IPCMessage; use crate::sidecar::messages::IPCMessage;
/// Resource directory set by the Tauri app during setup. /// Resource directory set by the Tauri app during setup.
@@ -231,10 +234,28 @@ impl SidecarManager {
self.stop().ok(); self.stop().ok();
eprintln!("[sidecar-rs] Starting frozen sidecar: {}", path.display()); eprintln!("[sidecar-rs] Starting frozen sidecar: {}", path.display());
let child = Command::new(path) // Log sidecar stderr to a file for diagnostics
.stdin(Stdio::piped()) 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()) .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() .spawn()
.map_err(|e| format!("Failed to start sidecar binary: {e}"))?; .map_err(|e| format!("Failed to start sidecar binary: {e}"))?;
@@ -300,7 +321,22 @@ impl SidecarManager {
.read_line(&mut line) .read_line(&mut line)
.map_err(|e| format!("Read error: {e}"))?; .map_err(|e| format!("Read error: {e}"))?;
if bytes == 0 { 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(); let trimmed = line.trim();
if trimmed.is_empty() { if trimmed.is_empty() {
@@ -330,11 +366,46 @@ impl SidecarManager {
/// Send a message and receive the response, calling a callback for intermediate messages. /// Send a message and receive the response, calling a callback for intermediate messages.
/// Intermediate messages include progress, pipeline.segment, and pipeline.speaker_update. /// 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<F>( pub fn send_and_receive_with_progress<F>(
&self, &self,
msg: &IPCMessage, msg: &IPCMessage,
on_intermediate: F, on_intermediate: F,
) -> Result<IPCMessage, String> ) -> Result<IPCMessage, String>
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<F>(
&self,
msg: &IPCMessage,
on_intermediate: &F,
) -> Result<IPCMessage, String>
where where
F: Fn(&IPCMessage), F: Fn(&IPCMessage),
{ {
@@ -420,8 +491,39 @@ impl SidecarManager {
} }
pub fn is_running(&self) -> bool { pub fn is_running(&self) -> bool {
let proc = self.process.lock().ok(); let mut proc = match self.process.lock() {
proc.map_or(false, |p| p.is_some()) 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;
}
} }
} }