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 = OnceLock::new(); static UVICORN_PORT_RE: OnceLock = OnceLock::new(); #[tauri::command] pub async fn start_backend( app: AppHandle, state: State<'_, crate::BackendState>, ) -> Result { // 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 { 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 { 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 { 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)) }