151 lines
5.2 KiB
Rust
151 lines
5.2 KiB
Rust
|
|
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<Option<Child>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
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::<IPCMessage>(&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<IPCMessage, String> {
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
}
|