fischer-agentkit/src-tauri/src/sidecar.rs

166 lines
6.0 KiB
Rust

use std::sync::OnceLock;
use tauri::{AppHandle, Emitter, Manager, State};
use tauri_plugin_shell::process::CommandEvent;
use tauri_plugin_shell::ShellExt;
use tokio_util::sync::CancellationToken;
static AGENTKIT_PORT_RE: OnceLock<regex::Regex> = OnceLock::new();
static UVICORN_PORT_RE: OnceLock<regex::Regex> = OnceLock::new();
#[tauri::command]
pub async fn start_backend(
app: AppHandle,
state: State<'_, crate::BackendState>,
) -> Result<u16, String> {
// If already started, return existing port
if let Some(port) = *state.port.lock().unwrap() {
return Ok(port);
}
// Spawn sidecar with --port 0 for random port assignment
let sidecar_command = app
.shell()
.sidecar("agentkit-server")
.map_err(|e| format!("Failed to create sidecar command: {}", e))?
.args(["--port", "0", "--host", "127.0.0.1"])
.env("AGENTKIT_GUI_MODE", "1");
let (mut rx, child) = sidecar_command
.spawn()
.map_err(|e| format!("Failed to spawn sidecar: {}", e))?;
// Store the CommandChild for proper cleanup later
*state.child.lock().unwrap() = Some(child);
let port_clone = state.port.clone();
let cancel_token = CancellationToken::new();
*state.cancel_token.lock().unwrap() = Some(cancel_token.clone());
let app_clone = app.clone();
// Listen for stdout/stderr to detect port
let cancel = cancel_token.clone();
tokio::spawn(async move {
tokio::select! {
_ = cancel.cancelled() => {
log::info!("Backend listener task cancelled");
}
result = async {
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => {
let output = String::from_utf8_lossy(&line);
log::info!("[backend:stdout] {}", output.trim());
if let Some(port) = extract_port(&output) {
*port_clone.lock().unwrap() = Some(port);
let _ = app_clone.emit("backend-ready", serde_json::json!({ "port": port }));
break;
}
}
CommandEvent::Stderr(line) => {
let output = String::from_utf8_lossy(&line);
log::info!("[backend:stderr] {}", output.trim());
if let Some(port) = extract_port(&output) {
*port_clone.lock().unwrap() = Some(port);
let _ = app_clone.emit("backend-ready", serde_json::json!({ "port": port }));
break;
}
}
CommandEvent::Terminated(payload) => {
log::error!("Backend process terminated: {:?}", payload);
break;
}
_ => {}
}
}
} => { result }
}
});
// Wait for port to be ready (with timeout)
let max_wait = std::time::Duration::from_secs(30);
let start = std::time::Instant::now();
while start.elapsed() < max_wait {
{
let guard = state.port.lock().unwrap();
if let Some(port) = *guard {
log::info!("Backend started on port {}", port);
// Close splash, show main window
if let Some(splash) = app.get_webview_window("splash") {
let _ = splash.close();
}
if let Some(main) = app.get_webview_window("main") {
let _ = main.show();
let _ = main.set_focus();
}
return Ok(port);
}
}
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
// Timeout: clean up the started backend process
log::warn!("Backend startup timed out, cleaning up...");
cleanup_backend(&state);
Err("Backend startup timed out after 30 seconds".into())
}
fn extract_port(output: &str) -> Option<u16> {
let re = AGENTKIT_PORT_RE.get_or_init(|| regex::Regex::new(r"AGENTKIT_PORT=(\d{1,5})").unwrap());
if let Some(caps) = re.captures(output) {
return caps[1].parse().ok();
}
let re2 = UVICORN_PORT_RE.get_or_init(|| regex::Regex::new(r"Uvicorn running on https?://[\d.]+:(\d{1,5})").unwrap());
if let Some(caps) = re2.captures(output) {
return caps[1].parse().ok();
}
None
}
/// Clean up backend process: cancel listener task, kill child process, reset state.
fn cleanup_backend(state: &State<'_, crate::BackendState>) {
// Cancel the listener task
if let Some(token) = state.cancel_token.lock().unwrap().take() {
token.cancel();
}
// Kill the child process
if let Some(child) = state.child.lock().unwrap().take() {
if let Err(e) = child.kill() {
log::warn!("Failed to kill backend child process: {}", e);
}
}
// Reset state
*state.port.lock().unwrap() = None;
}
#[tauri::command]
pub async fn get_backend_port(state: State<'_, crate::BackendState>) -> Result<u16, String> {
state
.port
.lock()
.unwrap()
.ok_or_else(|| "Backend not started".into())
}
#[tauri::command]
pub async fn stop_backend(state: State<'_, crate::BackendState>) -> Result<(), String> {
cleanup_backend(&state);
Ok(())
}
#[tauri::command]
pub async fn check_backend_health(
state: State<'_, crate::BackendState>,
) -> Result<bool, String> {
let port = state.port.lock().unwrap().ok_or("Backend not started")?;
let url = format!("http://127.0.0.1:{}/api/v1/health", port);
let client = reqwest::Client::new();
let resp = client
.get(&url)
.timeout(std::time::Duration::from_secs(3))
.send()
.await;
Ok(resp.map(|r| r.status().is_success()).unwrap_or(false))
}