feat(desktop): spawn local server with password (#8139)

This commit is contained in:
Brendan Allan
2026-01-13 13:58:00 +08:00
committed by GitHub
parent c0b214232d
commit f05f175842
9 changed files with 118 additions and 44 deletions

View File

@@ -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<String>,
}
#[derive(Clone)]
struct ServerState {
child: Arc<Mutex<Option<CommandChild>>>,
status: futures::future::Shared<tokio::sync::oneshot::Receiver<Result<String, String>>>,
status: future::Shared<oneshot::Receiver<Result<ServerReadyData, String>>>,
}
impl ServerState {
pub fn new(
child: Option<CommandChild>,
status: tokio::sync::oneshot::Receiver<Result<String, String>>,
status: oneshot::Receiver<Result<ServerReadyData, String>>,
) -> Self {
Self {
child: Arc::new(Mutex::new(child)),
@@ -80,7 +88,7 @@ async fn get_logs(app: AppHandle) -> Result<String, String> {
}
#[tauri::command]
async fn ensure_server_ready(state: State<'_, ServerState>) -> Result<String, String> {
async fn ensure_server_ready(state: State<'_, ServerState>) -> Result<ServerReadyData, String> {
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::<LogState>();
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<String> {
async fn setup_server_connection(
app: &AppHandle,
custom_url: Option<String>,
) -> Result<(Option<CommandChild>, String), String> {
) -> Result<(Option<CommandChild>, 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<CommandChild, String> {
let child = spawn_sidecar(app, port);
async fn spawn_local_server(
app: &AppHandle,
port: u32,
password: &str,
) -> Result<CommandChild, String> {
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<CommandChild,
tokio::time::sleep(Duration::from_millis(10)).await;
if check_server_health(&url).await {
if check_server_health(&url, Some(password)).await {
println!("Server ready after {:?}", timestamp.elapsed());
break Ok(child);
}