166 lines
6.0 KiB
Rust
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))
|
|
}
|