server.rs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. use std::time::{Duration, Instant};
  2. use tauri::AppHandle;
  3. use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
  4. use tauri_plugin_shell::process::CommandChild;
  5. use tauri_plugin_store::StoreExt;
  6. use tokio::task::JoinHandle;
  7. use crate::{
  8. cli,
  9. constants::{DEFAULT_SERVER_URL_KEY, SETTINGS_STORE},
  10. };
  11. #[tauri::command]
  12. #[specta::specta]
  13. pub fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> {
  14. let store = app
  15. .store(SETTINGS_STORE)
  16. .map_err(|e| format!("Failed to open settings store: {}", e))?;
  17. let value = store.get(DEFAULT_SERVER_URL_KEY);
  18. match value {
  19. Some(v) => Ok(v.as_str().map(String::from)),
  20. None => Ok(None),
  21. }
  22. }
  23. #[tauri::command]
  24. #[specta::specta]
  25. pub async fn set_default_server_url(app: AppHandle, url: Option<String>) -> Result<(), String> {
  26. let store = app
  27. .store(SETTINGS_STORE)
  28. .map_err(|e| format!("Failed to open settings store: {}", e))?;
  29. match url {
  30. Some(u) => {
  31. store.set(DEFAULT_SERVER_URL_KEY, serde_json::Value::String(u));
  32. }
  33. None => {
  34. store.delete(DEFAULT_SERVER_URL_KEY);
  35. }
  36. }
  37. store
  38. .save()
  39. .map_err(|e| format!("Failed to save settings: {}", e))?;
  40. Ok(())
  41. }
  42. pub async fn get_saved_server_url(app: &tauri::AppHandle) -> Option<String> {
  43. if let Some(url) = get_default_server_url(app.clone()).ok().flatten() {
  44. println!("Using desktop-specific custom URL: {url}");
  45. return Some(url);
  46. }
  47. if let Some(cli_config) = cli::get_config(app).await
  48. && let Some(url) = get_server_url_from_config(&cli_config)
  49. {
  50. println!("Using custom server URL from config: {url}");
  51. return Some(url);
  52. }
  53. None
  54. }
  55. pub fn spawn_local_server(
  56. app: AppHandle,
  57. hostname: String,
  58. port: u32,
  59. password: String,
  60. ) -> (CommandChild, HealthCheck) {
  61. let child = cli::serve(&app, &hostname, port, &password);
  62. let health_check = HealthCheck(tokio::spawn(async move {
  63. let url = format!("http://{hostname}:{port}");
  64. let timestamp = Instant::now();
  65. loop {
  66. tokio::time::sleep(Duration::from_millis(100)).await;
  67. if check_health(&url, Some(&password)).await {
  68. println!("Server ready after {:?}", timestamp.elapsed());
  69. break;
  70. }
  71. }
  72. }));
  73. (child, health_check)
  74. }
  75. pub struct HealthCheck(pub JoinHandle<()>);
  76. pub async fn check_health(url: &str, password: Option<&str>) -> bool {
  77. let Ok(url) = reqwest::Url::parse(url) else {
  78. return false;
  79. };
  80. let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(3));
  81. if url_is_localhost(&url) {
  82. // Some environments set proxy variables (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY) without
  83. // excluding loopback. reqwest respects these by default, which can prevent the desktop
  84. // app from reaching its own local sidecar server.
  85. builder = builder.no_proxy();
  86. };
  87. let Ok(client) = builder.build() else {
  88. return false;
  89. };
  90. let Ok(health_url) = url.join("/global/health") else {
  91. return false;
  92. };
  93. let mut req = client.get(health_url);
  94. if let Some(password) = password {
  95. req = req.basic_auth("opencode", Some(password));
  96. }
  97. req.send()
  98. .await
  99. .map(|r| r.status().is_success())
  100. .unwrap_or(false)
  101. }
  102. fn url_is_localhost(url: &reqwest::Url) -> bool {
  103. url.host_str().is_some_and(|host| {
  104. host.eq_ignore_ascii_case("localhost")
  105. || host
  106. .parse::<std::net::IpAddr>()
  107. .is_ok_and(|ip| ip.is_loopback())
  108. })
  109. }
  110. /// Converts a bind address hostname to a valid URL hostname for connection.
  111. /// - `0.0.0.0` and `::` are wildcard bind addresses, not valid connect targets
  112. /// - IPv6 addresses need brackets in URLs (e.g., `::1` -> `[::1]`)
  113. fn normalize_hostname_for_url(hostname: &str) -> String {
  114. // Wildcard bind addresses -> localhost equivalents
  115. if hostname == "0.0.0.0" {
  116. return "127.0.0.1".to_string();
  117. }
  118. if hostname == "::" {
  119. return "[::1]".to_string();
  120. }
  121. // IPv6 addresses need brackets in URLs
  122. if hostname.contains(':') && !hostname.starts_with('[') {
  123. return format!("[{}]", hostname);
  124. }
  125. hostname.to_string()
  126. }
  127. fn get_server_url_from_config(config: &cli::Config) -> Option<String> {
  128. let server = config.server.as_ref()?;
  129. let port = server.port?;
  130. println!("server.port found in OC config: {port}");
  131. let hostname = server
  132. .hostname
  133. .as_ref()
  134. .map(|v| normalize_hostname_for_url(v))
  135. .unwrap_or_else(|| "127.0.0.1".to_string());
  136. Some(format!("http://{}:{}", hostname, port))
  137. }
  138. pub async fn check_health_or_ask_retry(app: &AppHandle, url: &str) -> bool {
  139. println!("Checking health for {url}");
  140. loop {
  141. if check_health(url, None).await {
  142. return true;
  143. }
  144. const RETRY: &str = "Retry";
  145. let res = app.dialog()
  146. .message(format!("Could not connect to configured server:\n{}\n\nWould you like to retry or start a local server instead?", url))
  147. .title("Connection Failed")
  148. .buttons(MessageDialogButtons::OkCancelCustom(RETRY.to_string(), "Start Local".to_string()))
  149. .blocking_show_with_result();
  150. match res {
  151. MessageDialogResult::Custom(name) if name == RETRY => {
  152. continue;
  153. }
  154. _ => {
  155. break;
  156. }
  157. }
  158. }
  159. false
  160. }