| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- 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)
- }
|