From f05f175842d57003692ea766b07857d6f4b05f2b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 13 Jan 2026 13:58:00 +0800 Subject: feat(desktop): spawn local server with password (#8139) --- packages/desktop/src-tauri/src/lib.rs | 76 ++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 20 deletions(-) (limited to 'packages/desktop/src-tauri/src') diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index b479ed0b6..e2682ec71 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -3,6 +3,7 @@ mod window_customizer; use cli::{install_cli, sync_cli}; use futures::FutureExt; +use futures::future; use std::{ collections::VecDeque, net::TcpListener, @@ -13,22 +14,29 @@ use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, Webvie use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_store::StoreExt; +use tokio::sync::oneshot; use crate::window_customizer::PinchZoomDisablePlugin; const SETTINGS_STORE: &str = "opencode.settings.dat"; const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl"; +#[derive(Clone, serde::Serialize)] +struct ServerReadyData { + url: String, + password: Option, +} + #[derive(Clone)] struct ServerState { child: Arc>>, - status: futures::future::Shared>>, + status: future::Shared>>, } impl ServerState { pub fn new( child: Option, - status: tokio::sync::oneshot::Receiver>, + status: oneshot::Receiver>, ) -> Self { Self { child: Arc::new(Mutex::new(child)), @@ -80,7 +88,7 @@ async fn get_logs(app: AppHandle) -> Result { } #[tauri::command] -async fn ensure_server_ready(state: State<'_, ServerState>) -> Result { +async fn ensure_server_ready(state: State<'_, ServerState>) -> Result { state .status .clone() @@ -137,13 +145,14 @@ fn get_sidecar_port() -> u32 { }) as u32 } -fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild { +fn spawn_sidecar(app: &AppHandle, port: u32, password: &str) -> CommandChild { let log_state = app.state::(); let log_state_clone = log_state.inner().clone(); println!("spawning sidecar on port {port}"); let (mut rx, child) = cli::create_command(app, format!("serve --port {port}").as_str()) + .env("OPENCODE_SERVER_PASSWORD", password) .spawn() .expect("Failed to spawn opencode"); @@ -184,7 +193,7 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild { child } -async fn check_server_health(url: &str) -> bool { +async fn check_server_health(url: &str, password: Option<&str>) -> bool { let health_url = format!("{}/health", url.trim_end_matches('/')); let client = reqwest::Client::builder() .timeout(Duration::from_secs(3)) @@ -194,9 +203,13 @@ async fn check_server_health(url: &str) -> bool { return false; }; - client - .get(&health_url) - .send() + let mut req = client.get(&health_url); + + if let Some(password) = password { + req = req.basic_auth("opencode", Some(password)); + } + + req.send() .await .map(|r| r.status().is_success()) .unwrap_or(false) @@ -267,7 +280,7 @@ pub fn run() { window_builder.build().expect("Failed to create window"); - let (tx, rx) = tokio::sync::oneshot::channel(); + let (tx, rx) = oneshot::channel(); app.manage(ServerState::new(None, rx)); { @@ -344,12 +357,18 @@ fn get_server_url_from_config(config: &cli::Config) -> Option { async fn setup_server_connection( app: &AppHandle, custom_url: Option, -) -> Result<(Option, String), String> { +) -> Result<(Option, ServerReadyData), String> { if let Some(url) = custom_url { loop { - if check_server_health(&url).await { + if check_server_health(&url, None).await { println!("Connected to custom server: {}", url); - return Ok((None, url.clone())); + return Ok(( + None, + ServerReadyData { + url: url.clone(), + password: None, + }, + )); } const RETRY: &str = "Retry"; @@ -374,19 +393,36 @@ async fn setup_server_connection( let local_port = get_sidecar_port(); let local_url = format!("http://127.0.0.1:{local_port}"); - if !check_server_health(&local_url).await { - match spawn_local_server(app, local_port).await { - Ok(child) => Ok(Some(child)), + if !check_server_health(&local_url, None).await { + let password = uuid::Uuid::new_v4().to_string(); + + match spawn_local_server(app, local_port, &password).await { + Ok(child) => Ok(( + Some(child), + ServerReadyData { + url: local_url, + password: Some(password), + }, + )), Err(err) => Err(err), } } else { - Ok(None) + Ok(( + None, + ServerReadyData { + url: local_url, + password: None, + }, + )) } - .map(|child| (child, local_url)) } -async fn spawn_local_server(app: &AppHandle, port: u32) -> Result { - let child = spawn_sidecar(app, port); +async fn spawn_local_server( + app: &AppHandle, + port: u32, + password: &str, +) -> Result { + let child = spawn_sidecar(app, port, password); let url = format!("http://127.0.0.1:{port}"); let timestamp = Instant::now(); @@ -400,7 +436,7 @@ async fn spawn_local_server(app: &AppHandle, port: u32) -> Result