use std::{ io::{Error as IoError, ErrorKind}, net::{SocketAddr, TcpListener, TcpStream}, path::PathBuf, sync::Mutex, time::{Duration, Instant}, }; use tauri::{path::BaseDirectory, AppHandle, Manager, RunEvent, State}; use tauri_plugin_shell::{ process::{CommandChild, CommandEvent}, ShellExt, }; #[derive(Default)] struct ServerState(Mutex); struct ServerRuntime { url: String, child: CommandChild, } #[derive(Default)] struct ServerStatus { runtime: Option, startup_error: Option, } #[tauri::command] fn get_server_url(state: State<'_, ServerState>) -> Result { let guard = state .0 .lock() .map_err(|_| "desktop server state is unavailable".to_string())?; if let Some(runtime) = guard.runtime.as_ref() { return Ok(runtime.url.clone()); } Err(guard .startup_error .clone() .unwrap_or_else(|| "desktop server did not start".to_string())) } fn reserve_local_port() -> Result { let listener = TcpListener::bind("127.0.0.1:0").map_err(|err| format!("bind local port: {err}"))?; let port = listener .local_addr() .map_err(|err| format!("read local port: {err}"))? .port(); drop(listener); Ok(port) } fn wait_for_server(url_host: &str, port: u16) -> Result<(), String> { let addr: SocketAddr = format!("{url_host}:{port}") .parse() .map_err(|err| format!("parse server address: {err}"))?; let deadline = Instant::now() + Duration::from_secs(10); while Instant::now() < deadline { if TcpStream::connect_timeout(&addr, Duration::from_millis(200)).is_ok() { return Ok(()); } std::thread::sleep(Duration::from_millis(150)); } Err(format!( "desktop server did not start listening on {url_host}:{port} within 10 seconds" )) } fn resolve_app_root(app: &AppHandle) -> Result { if cfg!(debug_assertions) { return PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../..") .canonicalize() .map_err(|err| format!("resolve development app root: {err}")); } app.path() .resolve("app", BaseDirectory::Resource) .map_err(|err| format!("resolve bundled app root: {err}")) } fn start_server_sidecar(app: &AppHandle) -> Result { let host = "127.0.0.1"; let port = reserve_local_port()?; let url = format!("http://{host}:{port}"); let app_root = resolve_app_root(app)?; let app_root_arg = app_root.to_string_lossy().to_string(); let sidecar = app .shell() .sidecar("claude-server") .map_err(|err| format!("resolve server sidecar: {err}"))? .args([ "--app-root", &app_root_arg, "--host", host, "--port", &port.to_string(), ]); let (mut rx, child) = sidecar .spawn() .map_err(|err| format!("spawn server sidecar: {err}"))?; tauri::async_runtime::spawn(async move { while let Some(event) = rx.recv().await { match event { CommandEvent::Stdout(line) => { let line = String::from_utf8_lossy(&line); println!("[claude-server] {}", line.trim_end()); } CommandEvent::Stderr(line) => { let line = String::from_utf8_lossy(&line); eprintln!("[claude-server] {}", line.trim_end()); } _ => {} } } }); wait_for_server(host, port)?; Ok(ServerRuntime { url, child }) } fn stop_server_sidecar(app: &AppHandle) { let Some(state) = app.try_state::() else { return; }; let Ok(mut guard) = state.0.lock() else { return; }; if let Some(runtime) = guard.runtime.take() { let _ = runtime.child.kill(); } } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let app = tauri::Builder::default() .manage(ServerState::default()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .invoke_handler(tauri::generate_handler![get_server_url]) .setup(|app| { let state = app.state::(); let mut guard = state .0 .lock() .map_err(|_| IoError::new(ErrorKind::Other, "server state lock poisoned"))?; match start_server_sidecar(&app.handle()) { Ok(runtime) => { guard.runtime = Some(runtime); guard.startup_error = None; } Err(err) => { eprintln!("[desktop] failed to start local server: {err}"); guard.runtime = None; guard.startup_error = Some(err); } } let _window = app.get_webview_window("main").unwrap(); Ok(()) }) .build(tauri::generate_context!()) .expect("error while building tauri application"); app.run(|app_handle, event| { if matches!(event, RunEvent::Exit | RunEvent::ExitRequested { .. }) { stop_server_sidecar(app_handle); } }); }