fix(windows): improve startup window recovery and diagnostics
This commit is contained in:
parent
79873443e4
commit
7775bbf8f8
|
|
@ -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::<CloseBehaviorState>().ok_or_else(|| "close behavior state is unavailable".to_string())?;
|
||||
state.set_frontend_ready(true);
|
||||
clear_startup_probe_after_frontend_ready();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<OsString>,
|
||||
windows_appdata: Option<OsString>,
|
||||
) -> Option<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<str>) {
|
||||
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<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
|
|||
}
|
||||
}
|
||||
|
||||
fn main_window_probe_state<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> 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<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
|
||||
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<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
|
||||
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| {
|
||||
|
|
|
|||
|
|
@ -33,11 +33,15 @@ pub(crate) fn corrected_window_rect(
|
|||
pub(crate) fn enforce_main_window_bounds<R: Runtime>(app: &AppHandle<R>) {
|
||||
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<R: Runtime>(window: &WebviewWindow<R>) -> 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() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue