From 105254a81785fb96f9f87bb27b687479d3fbdb8c Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Sat, 3 Jan 2026 01:15:43 +0800 Subject: [PATCH] server down handling --- src-tauri/src/app.rs | 57 +++++++++++++------ src-tauri/src/lib.rs | 2 +- src-tauri/src/remotes/health.rs | 71 +++++++++++++++++------- src-tauri/src/services/app_menu.rs | 2 +- src-tauri/src/services/health_manager.rs | 32 ++++++++++- src-tauri/src/services/ws.rs | 22 +++++++- src/routes/health-manager/+page.svelte | 48 ++++++++++++++-- 7 files changed, 187 insertions(+), 47 deletions(-) diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index d5debcb..8a8bf3e 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -1,13 +1,13 @@ +use reqwest::StatusCode; use std::time::Duration; - use tokio::time::{sleep, Instant}; -use tracing::info; +use tracing::{info, warn}; use crate::{ - remotes::health::HealthRemote, + remotes::health::{HealthError, HealthRemote}, services::{ auth::{get_access_token, get_tokens}, - health_manager::open_health_manager_window, + health_manager::show_health_manager_with_error, scene::{close_splash_window, open_scene_window, open_splash_window}, welcome::open_welcome_window, ws::init_ws_client, @@ -18,7 +18,10 @@ use crate::{ pub async fn start_fdoll() { init_system_tray(); - init_startup_sequence().await; + if let Err(err) = init_startup_sequence().await { + tracing::error!("startup sequence failed: {err}"); + show_health_manager_with_error(Some(err.to_string())); + } } async fn init_ws_after_auth() { @@ -74,21 +77,41 @@ pub async fn bootstrap() { /// Perform checks for environment, network condition /// and handle situations where startup would not be appropriate. -async fn init_startup_sequence() { - let health_remote = HealthRemote::new(); - let server_health = health_remote.get_health().await; - match server_health { - Ok(response) => { - if response.status == "OK" { +async fn init_startup_sequence() -> Result<(), HealthError> { + let health_remote = HealthRemote::try_new()?; + + // simple retry loop to smooth transient network issues + const MAX_ATTEMPTS: u8 = 3; + const BACKOFF_MS: u64 = 500; + + for attempt in 1..=MAX_ATTEMPTS { + match health_remote.get_health().await { + Ok(_) => { bootstrap().await; - } else { - info!("Server health check failed"); + return Ok(()); + } + Err(HealthError::NonOkStatus(status)) => { + warn!(attempt, "server health reported non-OK status: {status}"); + return Err(HealthError::NonOkStatus(status)); + } + Err(HealthError::UnexpectedStatus(status)) => { + warn!(attempt, "server health check failed with status: {status}"); + return Err(HealthError::UnexpectedStatus(status)); + } + Err(err) => { + warn!(attempt, "server health check failed: {err}"); + if attempt == MAX_ATTEMPTS { + return Err(err); + } } } - Err(err) => { - info!("Server health check failed: {}", err); - open_health_manager_window(); - close_splash_window(); + + if attempt < MAX_ATTEMPTS { + sleep(Duration::from_millis(BACKOFF_MS)).await; } } + + Err(HealthError::UnexpectedStatus( + StatusCode::SERVICE_UNAVAILABLE, + )) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2581e08..db135cd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -335,7 +335,7 @@ fn quit_app() -> Result<(), String> { } #[tauri::command] -fn restart_app() -> Result<(), String> { +fn restart_app() { let app_handle = get_app_handle(); app_handle.restart(); } diff --git a/src-tauri/src/remotes/health.rs b/src-tauri/src/remotes/health.rs index dd42d47..609a9a2 100644 --- a/src-tauri/src/remotes/health.rs +++ b/src-tauri/src/remotes/health.rs @@ -1,5 +1,6 @@ -use reqwest::{Client, Error}; +use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; +use thiserror::Error; use ts_rs::TS; use crate::{lock_r, state::FDOLL}; @@ -14,34 +15,66 @@ pub struct HealthResponseDto { pub db: String, } +#[derive(Error, Debug)] +pub enum HealthError { + #[error("app configuration missing {0}")] + ConfigMissing(&'static str), + #[error("health request failed: {0}")] + Request(reqwest::Error), + #[error("unexpected health status: {0}")] + UnexpectedStatus(StatusCode), + #[error("health status reported not OK: {0}")] + NonOkStatus(String), + #[error("health response decode failed: {0}")] + Decode(reqwest::Error), +} + pub struct HealthRemote { pub base_url: String, pub client: Client, } impl HealthRemote { - pub fn new() -> Self { + pub fn try_new() -> Result { let guard = lock_r!(FDOLL); - Self { - base_url: guard - .app_config - .api_base_url - .as_ref() - .expect("App configuration error") - .clone(), - client: guard - .clients - .as_ref() - .expect("App configuration error") - .http_client - .clone(), - } + let base_url = guard + .app_config + .api_base_url + .as_ref() + .cloned() + .ok_or(HealthError::ConfigMissing("api_base_url"))?; + + let client = guard + .clients + .as_ref() + .map(|c| c.http_client.clone()) + .ok_or(HealthError::ConfigMissing("http_client"))?; + + Ok(Self { base_url, client }) } - pub async fn get_health(&self) -> Result { + pub async fn get_health(&self) -> Result { let url = format!("{}/health", self.base_url); - let resp = self.client.get(url).send().await?; - let health = resp.json().await?; + + let resp = self + .client + .get(url) + .send() + .await + .map_err(HealthError::Request)?; + + let resp = resp.error_for_status().map_err(|err| { + err.status() + .map(HealthError::UnexpectedStatus) + .unwrap_or_else(|| HealthError::Request(err)) + })?; + + let health: HealthResponseDto = resp.json().await.map_err(HealthError::Decode)?; + + if health.status != "OK" { + return Err(HealthError::NonOkStatus(health.status)); + } + Ok(health) } } diff --git a/src-tauri/src/services/app_menu.rs b/src-tauri/src/services/app_menu.rs index 9897c97..6eb547a 100644 --- a/src-tauri/src/services/app_menu.rs +++ b/src-tauri/src/services/app_menu.rs @@ -3,7 +3,7 @@ use tracing::{error, info}; use crate::get_app_handle; -static APP_MENU_WINDOW_LABEL: &str = "app_menu"; +pub static APP_MENU_WINDOW_LABEL: &str = "app_menu"; pub fn open_app_menu_window() { let app_handle = get_app_handle(); diff --git a/src-tauri/src/services/health_manager.rs b/src-tauri/src/services/health_manager.rs index 8310856..661350f 100644 --- a/src-tauri/src/services/health_manager.rs +++ b/src-tauri/src/services/health_manager.rs @@ -1,13 +1,29 @@ use crate::get_app_handle; -use tauri::Manager; +use tauri::{Emitter, Manager}; use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder, MessageDialogKind}; use tauri_plugin_positioner::WindowExt; use tracing::{error, info}; pub static HEALTH_MANAGER_WINDOW_LABEL: &str = "health_manager"; +pub static HEALTH_MANAGER_EVENT: &str = "health-error"; -pub fn open_health_manager_window() { +fn close_window_if_exists(label: &str) { let app_handle = get_app_handle(); + if let Some(window) = app_handle.get_window(label) { + if let Err(e) = window.close() { + error!("Failed to close {} window: {}", label, e); + } + } +} + +/// Closes primary UI windows and shows the health manager with an optional error message. +pub fn show_health_manager_with_error(error_message: Option) { + let app_handle = get_app_handle(); + // Ensure other windows are closed before showing health manager + close_window_if_exists(crate::services::scene::SPLASH_WINDOW_LABEL); + close_window_if_exists(crate::services::scene::SCENE_WINDOW_LABEL); + close_window_if_exists(crate::services::app_menu::APP_MENU_WINDOW_LABEL); + let existing_webview_window = app_handle.get_window(HEALTH_MANAGER_WINDOW_LABEL); if let Some(window) = existing_webview_window { @@ -21,6 +37,12 @@ pub fn open_health_manager_window() { .kind(MessageDialogKind::Error) .show(|_| {}); } + + if let Some(message) = error_message { + if let Err(e) = window.emit(HEALTH_MANAGER_EVENT, message.clone()) { + error!("Failed to emit health error event: {}", e); + } + } return; } @@ -58,6 +80,12 @@ pub fn open_health_manager_window() { error!("Failed to move health manager window to center: {}", e); } + if let Some(message) = error_message { + if let Err(e) = webview_window.emit(HEALTH_MANAGER_EVENT, message.clone()) { + error!("Failed to emit health error event: {}", e); + } + } + if let Err(e) = webview_window.show() { error!("Failed to show health manager window: {}", e); MessageDialogBuilder::new( diff --git a/src-tauri/src/services/ws.rs b/src-tauri/src/services/ws.rs index cd7f775..d418d76 100644 --- a/src-tauri/src/services/ws.rs +++ b/src-tauri/src/services/ws.rs @@ -7,6 +7,8 @@ use crate::{ get_app_handle, lock_r, lock_w, models::app_config::AppConfig, services::cursor::{normalized_to_absolute, CursorPosition, CursorPositions}, + services::health_manager::{close_health_manager_window, show_health_manager_with_error}, + services::scene::open_scene_window, state::{init_app_data_scoped, AppDataRefreshScope, FDOLL}, }; use serde::{Deserialize, Serialize}; @@ -74,6 +76,10 @@ fn on_initialized(payload: Payload, _socket: RawClient) { if let Some(clients) = guard.clients.as_mut() { clients.is_ws_initialized = true; } + + // Connection restored: close health manager and reopen scene + close_health_manager_window(); + open_scene_window(); } else { info!("Received initialized event with empty payload"); } @@ -385,8 +391,20 @@ pub async fn report_cursor_data(cursor_position: CursorPosition) { .await { Ok(Ok(_)) => (), - Ok(Err(e)) => error!("Failed to emit cursor report: {}", e), - Err(e) => error!("Failed to execute blocking task for cursor report: {}", e), + Ok(Err(e)) => { + error!("Failed to emit cursor report: {}", e); + show_health_manager_with_error(Some(format!( + "WebSocket emit failed: {}", + e + ))); + } + Err(e) => { + error!("Failed to execute blocking task for cursor report: {}", e); + show_health_manager_with_error(Some(format!( + "WebSocket task failed: {}", + e + ))); + } } } } diff --git a/src/routes/health-manager/+page.svelte b/src/routes/health-manager/+page.svelte index ab6db22..db381e4 100644 --- a/src/routes/health-manager/+page.svelte +++ b/src/routes/health-manager/+page.svelte @@ -1,5 +1,35 @@ -
@@ -9,13 +39,21 @@

Seems like the server is inaccessible. Check your network?

+ {#if errorMessage} +

{errorMessage}

+ {/if}
+ {#if isRestarting} + Retrying… + {:else} + Try again + {/if} +