From 03bd2f25ce575e8e9c979a35d3e96edc08322ddf Mon Sep 17 00:00:00 2001 From: Jeramie Date: Sun, 14 Jun 2026 00:56:22 -0700 Subject: [PATCH] fix: wait for DirectWrite font subsystem before starting at logon When launched at logon via the HKCU Run key, the app could fail with a system-modal "Failed to launch" error box and never start, while a manual launch always worked. gpui::Application::new() constructs WindowsPlatform::new(), which creates a shared DirectWrite factory (DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)) and enumerates the system font collection (GetSystemFontCollection). Both depend on the Windows Font Cache Service, which is often still starting in the first seconds of a session, so those calls fail and GPUI panics after showing the error box. Waiting for Shell_TrayWnd is not sufficient because it can appear before the font subsystem is ready. Add boot::wait_for_font_subsystem(), called right before gpui::Application::new(), which probes the same DirectWrite calls with backoff (~30s) until they succeed and logs late readiness/failures to %APPDATA%\Polterdesk\startup.log. On a warm machine the probe succeeds on the first attempt, so there is no added startup latency. The tray thread is spawned before the wait so the tray icon still appears promptly at boot. --- Cargo.toml | 1 + src/boot.rs | 129 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 10 +++- tests/boot_test.rs | 57 ++++++++++++++++++++ 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 src/boot.rs create mode 100644 tests/boot_test.rs diff --git a/Cargo.toml b/Cargo.toml index ef45c54..9085744 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ features = [ "Win32_System_Registry", "Win32_System_LibraryLoader", "Win32_Graphics_Gdi", + "Win32_Graphics_DirectWrite", ] [profile.release] diff --git a/src/boot.rs b/src/boot.rs new file mode 100644 index 0000000..da0fe2b --- /dev/null +++ b/src/boot.rs @@ -0,0 +1,129 @@ +//! Boot-time readiness gating. +//! +//! When the app is launched at logon via the HKCU Run key, parts of the Windows +//! graphics/shell stack are not necessarily ready yet. In particular, +//! gpui::Application::new() constructs WindowsPlatform::new(), which creates a +//! shared DirectWrite factory (DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)) +//! and enumerates the system font collection (GetSystemFontCollection). Both of +//! those depend on the Windows Font Cache Service (FontCache), which is often +//! still StartPending in the first seconds of a session. +//! +//! When they fail, GPUI calls show_error("Failed to launch", ...) a +//! MB_ICONERROR | MB_SYSTEMMODAL message box — and then .unwrap() panics, so the +//! process aborts and the app never starts. That is the "Windows error message box at +//! startup" symptom. +//! +//! To remove the race we gate GPUI construction on the *same* DirectWrite calls +//! succeeding here first, with backoff. On a manual launch the font subsystem is +//! already up, so the probe succeeds on the first attempt and we return immediately. + +use std::time::Duration; + +/// Number of probe attempts before giving up (120 × 250ms ≈ 30s). +const FONT_WAIT_ATTEMPTS: u32 = 120; +/// Delay between probe attempts. +const FONT_WAIT_DELAY: Duration = Duration::from_millis(250); + +/// Polls probe until it returns Ok, sleeping delay between attempts, up to +/// max_attempts times. +/// +/// Returns Ok(attempt_count) on the first success, or Err(last_error) if every +/// attempt failed (or max_attempts is 0). This is a pure control-flow harness with +/// the OS call injected, so it can be unit-tested deterministically. +pub fn poll_until_ready(max_attempts: u32, delay: Duration, mut probe: F) -> Result +where + F: FnMut() -> Result<(), String>, +{ + let mut last_error = String::from("probe never ran"); + for attempt in 1..=max_attempts { + match probe() { + Ok(()) => return Ok(attempt), + Err(e) => { + last_error = e; + if attempt < max_attempts { + std::thread::sleep(delay); + } + } + } + } + Err(last_error) +} + +/// Probes the DirectWrite font subsystem once, mirroring the Font-Cache-dependent +/// calls GPUI makes during WindowsPlatform::new(): create a shared factory and +/// enumerate the system font collection. +fn probe_font_subsystem() -> Result<(), String> { + use windows::Win32::Graphics::DirectWrite::{ + DWriteCreateFactory, IDWriteFactory, IDWriteFontCollection, DWRITE_FACTORY_TYPE_SHARED, + }; + + // SAFETY: DWriteCreateFactory returns an owned COM interface on success. + // GetSystemFontCollection is a read-only query; we pass a valid out-pointer and + // only read the returned interface. No raw pointers escape this block. + unsafe { + let factory: IDWriteFactory = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED) + .map_err(|e| format!("DWriteCreateFactory(shared) failed: {e}"))?; + + let mut collection: Option = None; + factory + .GetSystemFontCollection(&mut collection, false) + .map_err(|e| format!("GetSystemFontCollection failed: {e}"))?; + + match collection { + // A populated collection means the Font Cache Service answered. + Some(c) if c.GetFontFamilyCount() > 0 => Ok(()), + Some(_) => Err("system font collection is empty".to_string()), + None => Err("system font collection was null".to_string()), + } + } +} + +/// Blocks until the DirectWrite font subsystem is ready, or a ~30s timeout elapses. +/// +/// Must be called before gpui::Application::new(). See the module docs for why. +/// If the timeout is hit we proceed anyway (GPUI may then show its own error box), +/// having recorded the last error to the startup log for diagnosis. +pub fn wait_for_font_subsystem() { + match poll_until_ready(FONT_WAIT_ATTEMPTS, FONT_WAIT_DELAY, probe_font_subsystem) { + // The common case (manual launch / fast boot): ready immediately, no log noise. + Ok(1) => {} + Ok(attempts) => { + let waited_ms = (attempts - 1) * FONT_WAIT_DELAY.as_millis() as u32; + log_startup(&format!( + "Font subsystem ready after {attempts} attempt(s) (~{waited_ms}ms wait)." + )); + } + Err(last_error) => { + let timeout_s = FONT_WAIT_ATTEMPTS as u128 * FONT_WAIT_DELAY.as_millis() / 1000; + log_startup(&format!( + "Font subsystem NOT ready after ~{timeout_s}s; constructing GPUI anyway. \ + Last error: {last_error}" + )); + } + } +} + +/// Appends a timestamped line to %APPDATA%\Polterdesk\startup.log. Best-effort; +/// any failure is ignored (we must never block startup on logging). +fn log_startup(msg: &str) { + use std::io::Write; + + let Ok(appdata) = std::env::var("APPDATA") else { + return; + }; + let dir = std::path::PathBuf::from(appdata).join("Polterdesk"); + let _ = std::fs::create_dir_all(&dir); + + let unix_secs = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(dir.join("startup.log")) + { + let _ = writeln!(f, "[{unix_secs}] {msg}"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 64cc868..489a154 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod app_state; +pub mod boot; pub mod desktop; pub mod settings; pub mod startup; diff --git a/src/main.rs b/src/main.rs index 9c3944b..0fb6d62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #![windows_subsystem = "windows"] mod app_state; +mod boot; mod desktop; mod settings; mod startup; @@ -41,9 +42,16 @@ fn main() { let state = AppState::new(settings.clone(), gpui_tx); - // Spawn the WinAPI background thread (hotkey, tray, mouse hook) + // Spawn the WinAPI background thread (hotkey, tray, mouse hook). + // Done before the font wait so the tray icon appears promptly at boot. let _winapi_handle = winapi_thread::spawn(state.clone()); + // At logon the Windows Font Cache Service may still be starting, which makes + // gpui::Application::new() -> WindowsPlatform::new() fail in DirectWrite and show a + // system-modal "Failed to launch" error box, then abort. Gate GPUI construction on + // the font subsystem actually being ready. See boot::wait_for_font_subsystem. + boot::wait_for_font_subsystem(); + // Start the GPUI application on the main thread gpui::Application::new().run(move |cx| { ui::theme::apply_theme(cx); diff --git a/tests/boot_test.rs b/tests/boot_test.rs new file mode 100644 index 0000000..6ddb31a --- /dev/null +++ b/tests/boot_test.rs @@ -0,0 +1,57 @@ +use polterdesk::boot::poll_until_ready; +use std::cell::Cell; +use std::time::Duration; + +// The boot-readiness retry harness gates GPUI construction on the DirectWrite +// font subsystem becoming ready at logon. The OS probe is injected, so these +// tests exercise the control flow deterministically with a zero delay. + +#[test] +fn returns_on_first_success_without_retrying() { + let calls = Cell::new(0); + let result = poll_until_ready(5, Duration::ZERO, || { + calls.set(calls.get() + 1); + Ok(()) + }); + assert_eq!(result, Ok(1)); + assert_eq!(calls.get(), 1, "must not probe again after first success"); +} + +#[test] +fn retries_until_probe_succeeds() { + let calls = Cell::new(0); + let result = poll_until_ready(10, Duration::ZERO, || { + let n = calls.get() + 1; + calls.set(n); + if n >= 3 { + Ok(()) + } else { + Err(format!("font subsystem not ready (attempt {n})")) + } + }); + assert_eq!(result, Ok(3)); + assert_eq!(calls.get(), 3); +} + +#[test] +fn gives_up_after_max_attempts_returning_last_error() { + let calls = Cell::new(0); + let result = poll_until_ready(4, Duration::ZERO, || { + let n = calls.get() + 1; + calls.set(n); + Err(format!("boom {n}")) + }); + assert_eq!(result, Err("boom 4".to_string())); + assert_eq!(calls.get(), 4, "must attempt exactly max_attempts times"); +} + +#[test] +fn zero_max_attempts_never_probes() { + let calls = Cell::new(0); + let result = poll_until_ready(0, Duration::ZERO, || { + calls.set(calls.get() + 1); + Ok(()) + }); + assert!(result.is_err()); + assert_eq!(calls.get(), 0); +}