Fix critical integration issues for end-to-end functionality
- Rewrite SidecarManager as singleton with OnceLock, reusing one Python process across all commands instead of spawning per call - Separate stdin/stdout ownership with dedicated BufReader to prevent data corruption between wait_for_ready and send_and_receive - Add ensure_running() for auto-start on first command - Fix asset protocol URL: use convertFileSrc() instead of manual encodeURIComponent which broke file paths with slashes - Add +layout.svelte with global dark theme, CSS reset, and custom scrollbar styling to prevent white flash on startup - Register AppState with Tauri .manage(), initialize SQLite database on app startup at ~/.voicetonotes/voice_to_notes.db - Wire project commands (create/get/list) to real database queries instead of placeholder stubs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,38 +2,82 @@ pub mod ipc;
|
||||
pub mod messages;
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::Mutex;
|
||||
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::sidecar::messages::IPCMessage;
|
||||
|
||||
/// Get the global sidecar manager singleton.
|
||||
pub fn sidecar() -> &'static SidecarManager {
|
||||
static INSTANCE: OnceLock<SidecarManager> = OnceLock::new();
|
||||
INSTANCE.get_or_init(SidecarManager::new)
|
||||
}
|
||||
|
||||
/// Manages the Python sidecar process lifecycle.
|
||||
/// Uses separated stdin/stdout ownership to avoid BufReader conflicts.
|
||||
pub struct SidecarManager {
|
||||
process: Mutex<Option<Child>>,
|
||||
stdin: Mutex<Option<ChildStdin>>,
|
||||
reader: Mutex<Option<BufReader<std::process::ChildStdout>>>,
|
||||
}
|
||||
|
||||
impl SidecarManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
process: Mutex::new(None),
|
||||
stdin: Mutex::new(None),
|
||||
reader: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure the sidecar is running, starting it if needed.
|
||||
pub fn ensure_running(&self) -> Result<(), String> {
|
||||
if self.is_running() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let python_path = std::env::current_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join("../python")
|
||||
.canonicalize()
|
||||
.map_err(|e| format!("Cannot find python directory: {e}"))?;
|
||||
|
||||
self.start(&python_path.to_string_lossy())
|
||||
}
|
||||
|
||||
/// Spawn the Python sidecar process.
|
||||
pub fn start(&self, python_path: &str) -> Result<(), String> {
|
||||
let child = Command::new("python3")
|
||||
// Stop existing process if any
|
||||
self.stop().ok();
|
||||
|
||||
let mut 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
|
||||
.stderr(Stdio::inherit())
|
||||
.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);
|
||||
// Take ownership of stdin and stdout separately
|
||||
let stdin = child.stdin.take().ok_or("Failed to get sidecar stdin")?;
|
||||
let stdout = child.stdout.take().ok_or("Failed to get sidecar stdout")?;
|
||||
let buf_reader = BufReader::new(stdout);
|
||||
|
||||
{
|
||||
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
||||
*proc = Some(child);
|
||||
}
|
||||
{
|
||||
let mut s = self.stdin.lock().map_err(|e| e.to_string())?;
|
||||
*s = Some(stdin);
|
||||
}
|
||||
{
|
||||
let mut r = self.reader.lock().map_err(|e| e.to_string())?;
|
||||
*r = Some(buf_reader);
|
||||
}
|
||||
|
||||
// Wait for the "ready" message
|
||||
self.wait_for_ready()?;
|
||||
@@ -43,23 +87,28 @@ impl SidecarManager {
|
||||
|
||||
/// 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;
|
||||
let mut reader_guard = self.reader.lock().map_err(|e| e.to_string())?;
|
||||
if let Some(ref mut reader) = *reader_guard {
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
line.clear();
|
||||
let bytes = reader
|
||||
.read_line(&mut line)
|
||||
.map_err(|e| format!("Read error: {e}"))?;
|
||||
if bytes == 0 {
|
||||
return Err("Sidecar closed stdout before sending ready".to_string());
|
||||
}
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Ok(msg) = serde_json::from_str::<IPCMessage>(trimmed) {
|
||||
if msg.msg_type == "ready" {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// Non-ready message: something is wrong
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err("Sidecar did not send ready message".to_string())
|
||||
@@ -68,10 +117,10 @@ impl SidecarManager {
|
||||
/// 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 {
|
||||
// Write to stdin
|
||||
{
|
||||
let mut stdin_guard = self.stdin.lock().map_err(|e| e.to_string())?;
|
||||
if let Some(ref mut stdin) = *stdin_guard {
|
||||
let json = serde_json::to_string(msg).map_err(|e| e.to_string())?;
|
||||
stdin
|
||||
.write_all(json.as_bytes())
|
||||
@@ -83,12 +132,13 @@ impl SidecarManager {
|
||||
} 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);
|
||||
// Read from stdout
|
||||
{
|
||||
let mut reader_guard = self.reader.lock().map_err(|e| e.to_string())?;
|
||||
if let Some(ref mut reader) = *reader_guard {
|
||||
let mut line = String::new();
|
||||
// Read lines until we get a response (skip progress messages, collect them)
|
||||
loop {
|
||||
line.clear();
|
||||
let bytes_read = reader
|
||||
@@ -101,40 +151,45 @@ impl SidecarManager {
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let response: IPCMessage =
|
||||
serde_json::from_str(trimmed).map_err(|e| format!("Parse error: {e}"))?;
|
||||
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
|
||||
// Skip progress messages, return the final result/error
|
||||
if response.msg_type != "progress" {
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err("Sidecar stdout not available".to_string());
|
||||
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}"))
|
||||
// Drop stdin to signal EOF
|
||||
{
|
||||
let mut stdin_guard = self.stdin.lock().map_err(|e| e.to_string())?;
|
||||
*stdin_guard = None;
|
||||
}
|
||||
// Drop reader
|
||||
{
|
||||
let mut reader_guard = self.reader.lock().map_err(|e| e.to_string())?;
|
||||
*reader_guard = None;
|
||||
}
|
||||
// Wait for process to exit
|
||||
{
|
||||
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
||||
if let Some(ref mut child) = proc.take() {
|
||||
match child.wait() {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
let _ = child.kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user