server.rs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. use std::time::{Duration, Instant};
  2. use tauri::AppHandle;
  3. use tauri_plugin_store::StoreExt;
  4. use tokio::task::JoinHandle;
  5. use crate::{
  6. cli,
  7. cli::CommandChild,
  8. constants::{DEFAULT_SERVER_URL_KEY, SETTINGS_STORE, WSL_ENABLED_KEY},
  9. };
  10. #[derive(Clone, serde::Serialize, serde::Deserialize, specta::Type, Debug, Default)]
  11. pub struct WslConfig {
  12. pub enabled: bool,
  13. }
  14. #[tauri::command]
  15. #[specta::specta]
  16. pub fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> {
  17. let store = app
  18. .store(SETTINGS_STORE)
  19. .map_err(|e| format!("Failed to open settings store: {}", e))?;
  20. let value = store.get(DEFAULT_SERVER_URL_KEY);
  21. match value {
  22. Some(v) => Ok(v.as_str().map(String::from)),
  23. None => Ok(None),
  24. }
  25. }
  26. #[tauri::command]
  27. #[specta::specta]
  28. pub async fn set_default_server_url(app: AppHandle, url: Option<String>) -> Result<(), String> {
  29. let store = app
  30. .store(SETTINGS_STORE)
  31. .map_err(|e| format!("Failed to open settings store: {}", e))?;
  32. match url {
  33. Some(u) => {
  34. store.set(DEFAULT_SERVER_URL_KEY, serde_json::Value::String(u));
  35. }
  36. None => {
  37. store.delete(DEFAULT_SERVER_URL_KEY);
  38. }
  39. }
  40. store
  41. .save()
  42. .map_err(|e| format!("Failed to save settings: {}", e))?;
  43. Ok(())
  44. }
  45. #[tauri::command]
  46. #[specta::specta]
  47. pub fn get_wsl_config(_app: AppHandle) -> Result<WslConfig, String> {
  48. // let store = app
  49. // .store(SETTINGS_STORE)
  50. // .map_err(|e| format!("Failed to open settings store: {}", e))?;
  51. // let enabled = store
  52. // .get(WSL_ENABLED_KEY)
  53. // .as_ref()
  54. // .and_then(|v| v.as_bool())
  55. // .unwrap_or(false);
  56. Ok(WslConfig { enabled: false })
  57. }
  58. #[tauri::command]
  59. #[specta::specta]
  60. pub fn set_wsl_config(app: AppHandle, config: WslConfig) -> Result<(), String> {
  61. let store = app
  62. .store(SETTINGS_STORE)
  63. .map_err(|e| format!("Failed to open settings store: {}", e))?;
  64. store.set(WSL_ENABLED_KEY, serde_json::Value::Bool(config.enabled));
  65. store
  66. .save()
  67. .map_err(|e| format!("Failed to save settings: {}", e))?;
  68. Ok(())
  69. }
  70. pub fn spawn_local_server(
  71. app: AppHandle,
  72. hostname: String,
  73. port: u32,
  74. password: String,
  75. ) -> (CommandChild, HealthCheck) {
  76. let (child, exit) = cli::serve(&app, &hostname, port, &password);
  77. let health_check = HealthCheck(tokio::spawn(async move {
  78. let url = format!("http://{hostname}:{port}");
  79. let timestamp = Instant::now();
  80. let ready = async {
  81. loop {
  82. tokio::time::sleep(Duration::from_millis(100)).await;
  83. if check_health(&url, Some(&password)).await {
  84. tracing::info!(elapsed = ?timestamp.elapsed(), "Server ready");
  85. return Ok(());
  86. }
  87. }
  88. };
  89. let terminated = async {
  90. match exit.await {
  91. Ok(payload) => Err(format!(
  92. "Sidecar terminated before becoming healthy (code={:?} signal={:?})",
  93. payload.code, payload.signal
  94. )),
  95. Err(_) => Err("Sidecar terminated before becoming healthy".to_string()),
  96. }
  97. };
  98. tokio::select! {
  99. res = ready => res,
  100. res = terminated => res,
  101. }
  102. }));
  103. (child, health_check)
  104. }
  105. pub struct HealthCheck(pub JoinHandle<Result<(), String>>);
  106. async fn check_health(url: &str, password: Option<&str>) -> bool {
  107. let Ok(url) = reqwest::Url::parse(url) else {
  108. return false;
  109. };
  110. let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(7));
  111. if url
  112. .host_str()
  113. .is_some_and(|host| {
  114. host.eq_ignore_ascii_case("localhost")
  115. || host
  116. .parse::<std::net::IpAddr>()
  117. .is_ok_and(|ip| ip.is_loopback())
  118. })
  119. {
  120. // Some environments set proxy variables (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY) without
  121. // excluding loopback. reqwest respects these by default, which can prevent the desktop
  122. // app from reaching its own local sidecar server.
  123. builder = builder.no_proxy();
  124. }
  125. let Ok(client) = builder.build() else {
  126. return false;
  127. };
  128. let Ok(health_url) = url.join("/global/health") else {
  129. return false;
  130. };
  131. let mut req = client.get(health_url);
  132. if let Some(password) = password {
  133. req = req.basic_auth("opencode", Some(password));
  134. }
  135. req.send()
  136. .await
  137. .map(|r| r.status().is_success())
  138. .unwrap_or(false)
  139. }