Files
tf_code/packages/desktop/src-tauri/src/server.rs

171 lines
4.6 KiB
Rust

use std::time::{Duration, Instant};
use tauri::AppHandle;
use tauri_plugin_store::StoreExt;
use tokio::task::JoinHandle;
use crate::{
cli,
cli::CommandChild,
constants::{DEFAULT_SERVER_URL_KEY, SETTINGS_STORE, WSL_ENABLED_KEY},
};
#[derive(Clone, serde::Serialize, serde::Deserialize, specta::Type, Debug, Default)]
pub struct WslConfig {
pub enabled: bool,
}
#[tauri::command]
#[specta::specta]
pub fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> {
let store = app
.store(SETTINGS_STORE)
.map_err(|e| format!("Failed to open settings store: {}", e))?;
let value = store.get(DEFAULT_SERVER_URL_KEY);
match value {
Some(v) => Ok(v.as_str().map(String::from)),
None => Ok(None),
}
}
#[tauri::command]
#[specta::specta]
pub async fn set_default_server_url(app: AppHandle, url: Option<String>) -> Result<(), String> {
let store = app
.store(SETTINGS_STORE)
.map_err(|e| format!("Failed to open settings store: {}", e))?;
match url {
Some(u) => {
store.set(DEFAULT_SERVER_URL_KEY, serde_json::Value::String(u));
}
None => {
store.delete(DEFAULT_SERVER_URL_KEY);
}
}
store
.save()
.map_err(|e| format!("Failed to save settings: {}", e))?;
Ok(())
}
#[tauri::command]
#[specta::specta]
pub fn get_wsl_config(_app: AppHandle) -> Result<WslConfig, String> {
// let store = app
// .store(SETTINGS_STORE)
// .map_err(|e| format!("Failed to open settings store: {}", e))?;
// let enabled = store
// .get(WSL_ENABLED_KEY)
// .as_ref()
// .and_then(|v| v.as_bool())
// .unwrap_or(false);
Ok(WslConfig { enabled: false })
}
#[tauri::command]
#[specta::specta]
pub fn set_wsl_config(app: AppHandle, config: WslConfig) -> Result<(), String> {
let store = app
.store(SETTINGS_STORE)
.map_err(|e| format!("Failed to open settings store: {}", e))?;
store.set(WSL_ENABLED_KEY, serde_json::Value::Bool(config.enabled));
store
.save()
.map_err(|e| format!("Failed to save settings: {}", e))?;
Ok(())
}
pub fn spawn_local_server(
app: AppHandle,
hostname: String,
port: u32,
password: String,
) -> (CommandChild, HealthCheck) {
let (child, exit) = cli::serve(&app, &hostname, port, &password);
let health_check = HealthCheck(tokio::spawn(async move {
let url = format!("http://{hostname}:{port}");
let timestamp = Instant::now();
let ready = async {
loop {
tokio::time::sleep(Duration::from_millis(100)).await;
if check_health(&url, Some(&password)).await {
tracing::info!(elapsed = ?timestamp.elapsed(), "Server ready");
return Ok(());
}
}
};
let terminated = async {
match exit.await {
Ok(payload) => Err(format!(
"Sidecar terminated before becoming healthy (code={:?} signal={:?})",
payload.code, payload.signal
)),
Err(_) => Err("Sidecar terminated before becoming healthy".to_string()),
}
};
tokio::select! {
res = ready => res,
res = terminated => res,
}
}));
(child, health_check)
}
pub struct HealthCheck(pub JoinHandle<Result<(), String>>);
async fn check_health(url: &str, password: Option<&str>) -> bool {
let Ok(url) = reqwest::Url::parse(url) else {
return false;
};
let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(7));
if url
.host_str()
.is_some_and(|host| {
host.eq_ignore_ascii_case("localhost")
|| host
.parse::<std::net::IpAddr>()
.is_ok_and(|ip| ip.is_loopback())
})
{
// Some environments set proxy variables (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY) without
// excluding loopback. reqwest respects these by default, which can prevent the desktop
// app from reaching its own local sidecar server.
builder = builder.no_proxy();
}
let Ok(client) = builder.build() else {
return false;
};
let Ok(health_url) = url.join("/global/health") else {
return false;
};
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)
}