From 7775bbf8f8aec5f42cc77c32fd1232a80f55cf26 Mon Sep 17 00:00:00 2001 From: zipg Date: Thu, 30 Jul 2026 15:55:04 +0800 Subject: [PATCH] fix(windows): improve startup window recovery and diagnostics --- src-tauri/src/commands/app_settings.rs | 5 +- src-tauri/src/lib.rs | 224 +++++++++++++++++++++++-- src-tauri/src/window_state_guard.rs | 16 +- 3 files changed, 232 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/commands/app_settings.rs b/src-tauri/src/commands/app_settings.rs index 53f060697..f1f61bcc9 100644 --- a/src-tauri/src/commands/app_settings.rs +++ b/src-tauri/src/commands/app_settings.rs @@ -8,8 +8,8 @@ use tauri::{AppHandle, Manager, State, Window}; use super::connection::AppState; use crate::{ - apply_debug_log_level, apply_desktop_settings, hide_main_window_for_close, refresh_native_menus, request_app_close, - AppLocaleState, CloseBehaviorState, + apply_debug_log_level, apply_desktop_settings, clear_startup_probe_after_frontend_ready, + hide_main_window_for_close, refresh_native_menus, request_app_close, AppLocaleState, CloseBehaviorState, }; const DEVELOPMENT_OPEN_TABS_STATE_KEY: &str = "development_open_tabs"; @@ -101,6 +101,7 @@ pub fn mark_frontend_ready(app: AppHandle) -> Result<(), String> { let state = app.try_state::().ok_or_else(|| "close behavior state is unavailable".to_string())?; state.set_frontend_ready(true); + clear_startup_probe_after_frontend_ready(); Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 235c08a14..4579a8782 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,6 +10,9 @@ use commands::connection::AppState; use dbx_core::sql_dialect::dialect_loader::{register_core_dialects, DialectPluginLoader, DialectRegistry}; use dbx_core::sql_dialect::hot_reload::DialectHotReload; use dbx_core::storage::{maybe_import_user_data_db, DesktopIconTheme, DesktopSettings, Storage}; +use std::ffi::OsString; +use std::io::Write; +use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -31,6 +34,10 @@ use tauri_plugin_deep_link::DeepLinkExt; const DESKTOP_TRAY_ID: &str = "main-tray"; const APP_CLOSE_REQUESTED_EVENT: &str = "dbx-app-close-requested"; +const STARTUP_PROBE_LOG_FILE: &str = "startup.log"; +const STARTUP_PROBE_LOG_DIR_ENV: &str = "DBX_STARTUP_LOG_DIR"; +const STARTUP_PROBE_KEEP_ENV: &str = "DBX_KEEP_STARTUP_LOG"; +const WINDOWS_APP_DATA_DIR_NAME: &str = "com.dbx.app"; #[cfg(target_os = "windows")] const WEBVIEW2_NO_SANDBOX_ENV: &str = "DBX_WEBVIEW2_NO_SANDBOX"; #[cfg(target_os = "macos")] @@ -146,6 +153,97 @@ fn should_show_main_window_after_setup() -> bool { true } +fn should_show_main_window_before_setup_tasks() -> bool { + true +} + +fn startup_probe_log_dir_from_inputs( + target_os: &str, + explicit_dir: Option, + windows_appdata: Option, +) -> Option { + if let Some(dir) = explicit_dir.filter(|value| !value.is_empty()) { + return Some(PathBuf::from(dir)); + } + if target_os == "windows" { + return windows_appdata + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .map(|dir| dir.join(WINDOWS_APP_DATA_DIR_NAME)); + } + None +} + +fn startup_probe_log_dir() -> Option { + startup_probe_log_dir_from_inputs( + std::env::consts::OS, + std::env::var_os(STARTUP_PROBE_LOG_DIR_ENV), + std::env::var_os("APPDATA"), + ) +} + +fn startup_probe_log_path() -> Option { + startup_probe_log_dir().map(|dir| dir.join(STARTUP_PROBE_LOG_FILE)) +} + +fn startup_probe_should_keep_after_frontend_ready_from_value(value: Option<&str>) -> bool { + matches!(value, Some("1")) +} + +fn startup_probe_should_keep_after_frontend_ready() -> bool { + startup_probe_should_keep_after_frontend_ready_from_value(std::env::var(STARTUP_PROBE_KEEP_ENV).ok().as_deref()) +} + +fn ensure_startup_probe_parent_dir(path: &std::path::Path) -> bool { + let Some(dir) = path.parent() else { + return false; + }; + if std::fs::create_dir_all(dir).is_err() { + return false; + } + true +} + +fn reset_startup_probe() { + let Some(path) = startup_probe_log_path() else { + return; + }; + if !ensure_startup_probe_parent_dir(&path) { + return; + } + let _ = std::fs::remove_file(path); +} + +fn append_startup_probe(message: impl AsRef) { + let Some(path) = startup_probe_log_path() else { + return; + }; + if !ensure_startup_probe_parent_dir(&path) { + return; + } + let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) else { + return; + }; + let _ = writeln!( + file, + "[{}][pid={}] {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"), + std::process::id(), + message.as_ref() + ); +} + +pub(crate) fn clear_startup_probe_after_frontend_ready() { + if startup_probe_should_keep_after_frontend_ready() { + append_startup_probe("frontend ready; keeping startup probe by request"); + return; + } + let Some(path) = startup_probe_log_path() else { + return; + }; + let _ = std::fs::remove_file(path); +} + #[cfg(target_os = "windows")] fn configure_webview2_sandbox_compat() { if !matches!(std::env::var(WEBVIEW2_NO_SANDBOX_ENV).as_deref(), Ok("1")) { @@ -460,6 +558,30 @@ fn show_main_window(app: &tauri::AppHandle) { } } +fn main_window_probe_state(app: &tauri::AppHandle) -> String { + let Some(window) = app.get_webview_window("main") else { + return "main_window=missing".to_string(); + }; + format!( + "main_window visible={:?} minimized={:?} maximized={:?} fullscreen={:?} position={:?} size={:?}", + window.is_visible(), + window.is_minimized(), + window.is_maximized(), + window.is_fullscreen(), + window.outer_position(), + window.outer_size() + ) +} + +fn prepare_main_window_for_display(app: &tauri::AppHandle) { + if let Some(decorations) = native_window_decorations_override(std::env::consts::OS) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.set_decorations(decorations); + } + } + window_state_guard::enforce_main_window_bounds(app); +} + fn clear_main_webview_focus(app: &tauri::AppHandle) { if let Some(window) = app.get_webview_window("main") { let _ = window.eval( @@ -805,10 +927,12 @@ mod tests { linux_appimage_wayland_backend_override, linux_nvidia_driver_from_state, linux_selected_drm_render_device, linux_webkit_rendering_workarounds, native_window_decorations_override, should_confirm_app_exit_request, should_enable_single_instance, should_fallback_to_native_quit, should_hide_window_on_close, - should_setup_desktop_tray, should_show_main_window_after_setup, tray_menu_labels_for_locale, - uses_application_level_icon, LinuxDrmRenderDevice, LinuxNvidiaDriver, + should_setup_desktop_tray, should_show_main_window_after_setup, should_show_main_window_before_setup_tasks, + startup_probe_log_dir_from_inputs, startup_probe_should_keep_after_frontend_ready_from_value, + tray_menu_labels_for_locale, uses_application_level_icon, LinuxDrmRenderDevice, LinuxNvidiaDriver, + WINDOWS_APP_DATA_DIR_NAME, }; - use std::ffi::OsStr; + use std::ffi::{OsStr, OsString}; use std::path::{Path, PathBuf}; const TEST_GTK3_IMMODULES_CACHE: &str = "/usr/lib/test/gtk-3.0/3.0.0/immodules.cache"; @@ -918,6 +1042,45 @@ mod tests { assert!(should_show_main_window_after_setup()); } + #[test] + fn shows_main_window_while_startup_setup_continues() { + assert!(should_show_main_window_before_setup_tasks()); + } + + #[test] + fn startup_probe_log_dir_prefers_explicit_override() { + assert_eq!( + startup_probe_log_dir_from_inputs( + "windows", + Some(OsString::from(r"D:\DBXDiagnostics")), + Some(OsString::from(r"C:\Users\test\AppData\Roaming")), + ), + Some(PathBuf::from(r"D:\DBXDiagnostics")) + ); + } + + #[test] + fn startup_probe_log_dir_uses_windows_appdata() { + assert_eq!( + startup_probe_log_dir_from_inputs("windows", None, Some(OsString::from(r"C:\Users\test\AppData\Roaming")),), + Some(PathBuf::from(r"C:\Users\test\AppData\Roaming").join(WINDOWS_APP_DATA_DIR_NAME)) + ); + } + + #[test] + fn startup_probe_log_dir_is_disabled_without_windows_appdata() { + assert_eq!(startup_probe_log_dir_from_inputs("windows", None, None), None); + assert_eq!(startup_probe_log_dir_from_inputs("macos", None, Some(OsString::from("/Users/test/Library"))), None); + } + + #[test] + fn startup_probe_log_is_kept_only_when_requested() { + assert!(startup_probe_should_keep_after_frontend_ready_from_value(Some("1"))); + assert!(!startup_probe_should_keep_after_frontend_ready_from_value(None)); + assert!(!startup_probe_should_keep_after_frontend_ready_from_value(Some("true"))); + assert!(!startup_probe_should_keep_after_frontend_ready_from_value(Some("0"))); + } + #[test] fn only_user_requested_app_exit_needs_frontend_confirmation() { assert!(should_confirm_app_exit_request("windows", None, false)); @@ -1116,8 +1279,17 @@ mod tests { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + reset_startup_probe(); + append_startup_probe(format!( + "process start version={} os={} arch={} exe={:?}", + env!("CARGO_PKG_VERSION"), + std::env::consts::OS, + std::env::consts::ARCH, + std::env::current_exe() + )); rustls::crypto::aws_lc_rs::default_provider().install_default().expect("Failed to install rustls crypto provider"); configure_webview2_sandbox_compat(); + append_startup_probe("runtime prerequisites configured"); #[cfg(target_os = "linux")] apply_linux_webkit_rendering_workarounds(); @@ -1159,7 +1331,11 @@ pub fn run() { .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()) - .plugin(tauri_plugin_window_state::Builder::default().build()); + .plugin( + tauri_plugin_window_state::Builder::default() + .with_state_flags(window_state_guard::persisted_main_window_state_flags()) + .build(), + ); // macOS app menu (Cmd+Q / Dock Quit). Skip on Linux/Windows so an empty menu bar // is not installed where there was none before. @@ -1187,12 +1363,24 @@ pub fn run() { .setup(move |app| { let setup_start = Instant::now(); eprintln!("[STARTUP] plugins registered in {:?}", startup_begin.elapsed()); + append_startup_probe(format!("setup entered after {:?}", startup_begin.elapsed())); + if should_show_main_window_before_setup_tasks() { + prepare_main_window_for_display(app.handle()); + show_main_window(app.handle()); + append_startup_probe(format!( + "early main window show requested; {}", + main_window_probe_state(app.handle()) + )); + } + + append_startup_probe("resolving app data dir"); let default_data_dir = app.path().app_data_dir().map_err(|e| e.to_string()).expect("Failed to resolve app data dir"); let data_dir_resolution = data_dir::resolve_data_dir_with_mode(default_data_dir); let data_dir = data_dir_resolution.data_dir.clone(); std::fs::create_dir_all(&data_dir).expect("Failed to create data dir"); + append_startup_probe(format!("data dir ready: {}", data_dir.display())); let alternative_data_dir = data_dir::alternative_data_dir(&data_dir_resolution); match maybe_import_user_data_db(&data_dir, alternative_data_dir.as_deref()) { Ok(result) => eprintln!("[STARTUP] data db fallback import: {result:?}"), @@ -1201,12 +1389,15 @@ pub fn run() { let db_path = data_dir.join("dbx.db"); let t = Instant::now(); + append_startup_probe(format!("opening storage: {}", db_path.display())); let storage = tauri::async_runtime::block_on(async { let s = Storage::open(&db_path).await.expect("Failed to open storage"); eprintln!("[STARTUP] Storage::open in {:?}", t.elapsed()); + append_startup_probe(format!("storage opened in {:?}", t.elapsed())); let t2 = Instant::now(); s.migrate_from_json(&data_dir).await.expect("Failed to migrate JSON data"); eprintln!("[STARTUP] migrate_from_json in {:?}", t2.elapsed()); + append_startup_probe(format!("json migration completed in {:?}", t2.elapsed())); s }); let desktop_settings = tauri::async_runtime::block_on(storage.load_desktop_settings()).unwrap_or_default(); @@ -1227,6 +1418,7 @@ pub fn run() { )?; apply_debug_log_level(desktop_settings.debug_logging_enabled); eprintln!("[STARTUP] storage ready in {:?}", t.elapsed()); + append_startup_probe(format!("storage ready in {:?}", t.elapsed())); // Initialize core dialect registry and load external plugin dialects let dialect_init_start = Instant::now(); @@ -1241,6 +1433,13 @@ pub fn run() { load_result.skipped.len(), dialect_init_start.elapsed() ); + append_startup_probe(format!( + "dialect plugins loaded: {} success, {} errors, {} skipped in {:?}", + load_result.loaded.len(), + load_result.errors.len(), + load_result.skipped.len(), + dialect_init_start.elapsed() + )); // Start dialect YAML hot-reload watcher let watch_dirs = plugin_dirs.clone(); @@ -1289,12 +1488,13 @@ pub fn run() { let app_handle = app.handle().clone(); commands::mcp_bridge::start(app_handle, state, data_dir); eprintln!("[STARTUP] setup complete in {:?} (total {:?})", setup_start.elapsed(), startup_begin.elapsed()); + append_startup_probe(format!( + "setup tasks complete in {:?} total {:?}", + setup_start.elapsed(), + startup_begin.elapsed() + )); - if let Some(decorations) = native_window_decorations_override(std::env::consts::OS) { - if let Some(window) = app.get_webview_window("main") { - let _ = window.set_decorations(decorations); - } - } + prepare_main_window_for_display(app.handle()); if should_setup_desktop_tray( std::env::consts::OS, desktop_settings.show_tray_icon, @@ -1305,13 +1505,17 @@ pub fn run() { apply_desktop_icon_theme(app.handle(), desktop_settings.icon_theme)?; #[cfg(target_os = "macos")] apply_macos_development_dock_badge(app.handle())?; - window_state_guard::enforce_main_window_bounds(app.handle()); if should_show_main_window_after_setup() { show_main_window(app.handle()); + append_startup_probe(format!( + "final main window show requested; {}", + main_window_probe_state(app.handle()) + )); } #[cfg(any(windows, target_os = "linux"))] let _ = app.deep_link().register_all(); + append_startup_probe("setup finished"); Ok(()) }) .on_window_event(|window, event| { diff --git a/src-tauri/src/window_state_guard.rs b/src-tauri/src/window_state_guard.rs index 56c391910..f43f4d36d 100644 --- a/src-tauri/src/window_state_guard.rs +++ b/src-tauri/src/window_state_guard.rs @@ -33,11 +33,15 @@ pub(crate) fn corrected_window_rect( pub(crate) fn enforce_main_window_bounds(app: &AppHandle) { if let Some(window) = app.get_webview_window("main") { if enforce_window_bounds(&window) { - let _ = app.save_window_state(StateFlags::all()); + let _ = app.save_window_state(persisted_main_window_state_flags()); } } } +pub(crate) fn persisted_main_window_state_flags() -> StateFlags { + StateFlags::SIZE | StateFlags::POSITION | StateFlags::MAXIMIZED | StateFlags::DECORATIONS | StateFlags::FULLSCREEN +} + fn enforce_window_bounds(window: &WebviewWindow) -> bool { if window.is_maximized().unwrap_or(false) || window.is_fullscreen().unwrap_or(false) { return false; @@ -94,6 +98,16 @@ fn centered_axis_position(window_start: i32, window_length: u32, monitor_start: #[cfg(test)] mod tests { use super::{corrected_window_rect, PhysicalMonitorRect, PhysicalWindowRect}; + use tauri_plugin_window_state::StateFlags; + + #[test] + fn persisted_state_excludes_window_visibility() { + let flags = super::persisted_main_window_state_flags(); + + assert!(flags.contains(StateFlags::SIZE)); + assert!(flags.contains(StateFlags::POSITION)); + assert!(!flags.contains(StateFlags::VISIBLE)); + } #[test] fn moves_window_back_onto_monitor_when_restored_above_screen() {