pub mod ipc; pub mod messages; use std::io::{BufRead, BufReader, Write}; use std::process::{Child, Command, Stdio}; use std::sync::Mutex; use crate::sidecar::messages::IPCMessage; /// Manages the Python sidecar process lifecycle. pub struct SidecarManager { process: Mutex>, } impl SidecarManager { pub fn new() -> Self { Self { process: Mutex::new(None), } } /// Spawn the Python sidecar process. pub fn start(&self, python_path: &str) -> Result<(), String> { let child = Command::new("python3") .arg("-m") .arg("voice_to_notes.main") .current_dir(python_path) .env("PYTHONPATH", python_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) // Let sidecar logs go to parent's stderr .spawn() .map_err(|e| format!("Failed to start sidecar: {e}"))?; let mut proc = self.process.lock().map_err(|e| e.to_string())?; *proc = Some(child); // Wait for the "ready" message self.wait_for_ready()?; Ok(()) } /// Wait for the sidecar to send its ready message. fn wait_for_ready(&self) -> Result<(), String> { let mut proc = self.process.lock().map_err(|e| e.to_string())?; if let Some(ref mut child) = *proc { if let Some(ref mut stdout) = child.stdout { let reader = BufReader::new(stdout); for line in reader.lines() { let line = line.map_err(|e| format!("Read error: {e}"))?; if line.is_empty() { continue; } if let Ok(msg) = serde_json::from_str::(&line) { if msg.msg_type == "ready" { return Ok(()); } } // If we got a non-ready message, something's wrong but don't block forever break; } } } Err("Sidecar did not send ready message".to_string()) } /// Send a message to the sidecar and read the response. /// This is a blocking call. pub fn send_and_receive(&self, msg: &IPCMessage) -> Result { let mut proc = self.process.lock().map_err(|e| e.to_string())?; if let Some(ref mut child) = *proc { // Write message to stdin if let Some(ref mut stdin) = child.stdin { let json = serde_json::to_string(msg).map_err(|e| e.to_string())?; stdin .write_all(json.as_bytes()) .map_err(|e| format!("Write error: {e}"))?; stdin .write_all(b"\n") .map_err(|e| format!("Write error: {e}"))?; stdin.flush().map_err(|e| format!("Flush error: {e}"))?; } else { return Err("Sidecar stdin not available".to_string()); } // Read response from stdout if let Some(ref mut stdout) = child.stdout { let mut reader = BufReader::new(stdout); let mut line = String::new(); // Read lines until we get a response (skip progress messages, collect them) loop { line.clear(); let bytes_read = reader .read_line(&mut line) .map_err(|e| format!("Read error: {e}"))?; if bytes_read == 0 { return Err("Sidecar closed stdout".to_string()); } let trimmed = line.trim(); if trimmed.is_empty() { continue; } let response: IPCMessage = serde_json::from_str(trimmed).map_err(|e| format!("Parse error: {e}"))?; // If it's a progress message, we could emit it as an event // For now, skip progress and return the final result/error if response.msg_type != "progress" { return Ok(response); } } } else { return Err("Sidecar stdout not available".to_string()); } } else { Err("Sidecar not running".to_string()) } } /// Stop the sidecar process. pub fn stop(&self) -> Result<(), String> { let mut proc = self.process.lock().map_err(|e| e.to_string())?; if let Some(ref mut child) = proc.take() { // Close stdin to signal EOF drop(child.stdin.take()); // Wait briefly for clean exit, then kill match child.wait() { Ok(_) => Ok(()), Err(e) => { let _ = child.kill(); Err(format!("Sidecar did not exit cleanly: {e}")) } } } else { Ok(()) } } pub fn is_running(&self) -> bool { let proc = self.process.lock().ok(); proc.map_or(false, |p| p.is_some()) } } impl Drop for SidecarManager { fn drop(&mut self) { let _ = self.stop(); } }