Compare commits

..

1 Commits

Author SHA1 Message Date
NimBold 6b802b0ad4 test(startup): isolate persisted geometry regression (#37)
- Issue #37: remove only the persisted main-window startup geometry path and restore the pre-1.4.0 window dimensions for an A/B package run.\n- Keep the application startup workaround disabled so the test isolates the 1.4.0 regression instead of masking it.\n\nRefs #37.
2026-08-27 18:43:46 +03:30
6 changed files with 5 additions and 179 deletions
+1 -45
View File
@@ -37,7 +37,6 @@ const child = spawn(executable, [], {
});
let stderr = '';
let stdout = '';
let spawnError = null;
let readyPort = null;
let childExit = null;
@@ -56,9 +55,7 @@ child.on('exit', (code, signal) => {
child.stderr.on('data', data => {
stderr += data.toString();
});
child.stdout.on('data', data => {
stdout += data.toString();
});
child.stdout.on('data', () => {});
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
@@ -190,45 +187,6 @@ if ($visible.Count -gt 0) {
}
}
function windowsCrashDiagnostics() {
if (process.platform !== 'win32') {
return '';
}
const script = `
$start = (Get-Date).AddMinutes(-15)
$events = @(Get-WinEvent -FilterHashtable @{ LogName = 'Application'; StartTime = $start } -ErrorAction SilentlyContinue |
Where-Object {
$_.ProviderName -in @('Application Error', 'Windows Error Reporting') -and
$_.Message -match '(?i)firelink'
} |
Select-Object -First 8)
foreach ($event in $events) {
Write-Output ("EVENT " + $event.TimeCreated.ToString('o') + " " + $event.ProviderName + " ID=" + $event.Id)
Write-Output $event.Message
}
`;
try {
return execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
}).trim();
} catch {
return '';
}
}
function startupDiagnostics() {
const sections = [];
if (stdout.trim()) sections.push(`Firelink stdout:\n${stdout.slice(-6000)}`);
if (stderr.trim()) sections.push(`Firelink stderr:\n${stderr.slice(-6000)}`);
const crashEvents = windowsCrashDiagnostics();
if (crashEvents) sections.push(`Windows application events:\n${crashEvents.slice(-12000)}`);
return sections.join('\n\n');
}
function waitForChildExit(timeoutMs) {
if (childExit) {
return Promise.resolve(true);
@@ -438,8 +396,6 @@ try {
console.log(`Packaged Firelink smoke passed on 127.0.0.1:${readyPort} with ${stabilityMs}ms stability`);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
const diagnostics = startupDiagnostics();
if (diagnostics) console.error(`\nStartup diagnostics:\n${diagnostics}`);
process.exitCode = 1;
} finally {
if (!await terminateChild()) {
-130
View File
@@ -3659,23 +3659,6 @@ pub struct AppState {
#[derive(Default)]
struct MainWindowRestoreState {
requested: AtomicBool,
startup_complete: AtomicBool,
}
fn startup_trace(marker: &str) {
if std::env::var_os("FIRELINK_SMOKE_TEST").is_some() {
eprintln!("[firelink-startup] {marker}");
}
}
static STARTUP_WINDOW_EVENT_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
fn startup_trace_window_event(label: &str) {
let count = STARTUP_WINDOW_EVENT_COUNT.fetch_add(1, Ordering::Relaxed);
if count < 64 {
startup_trace(label);
}
}
#[cfg(target_os = "macos")]
@@ -3972,25 +3955,6 @@ where
}
pub(crate) fn restore_main_window(app_handle: &tauri::AppHandle) {
let startup_complete = app_handle
.try_state::<MainWindowRestoreState>()
.is_none_or(|state| state.startup_complete.load(Ordering::Acquire));
if !startup_complete {
if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() {
// The first window can be requested by a single-instance callback
// or an opened .torrent path while the native host is still being
// constructed. Do not touch the HWND until RunEvent::Ready.
state.requested.store(true, Ordering::Release);
// Ready may have won the transition between the first load and
// the request store. Re-check before returning so that request is
// serviced by this call instead of being left pending forever.
if !state.startup_complete.load(Ordering::Acquire) {
return;
}
} else {
return;
}
}
let Some(window) = app_handle.get_webview_window("main") else {
if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() {
state.requested.store(true, Ordering::Release);
@@ -4003,24 +3967,6 @@ pub(crate) fn restore_main_window(app_handle: &tauri::AppHandle) {
let _ = window.set_focus();
}
fn mark_main_window_startup_complete(app_handle: &tauri::AppHandle) {
if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() {
state.startup_complete.store(true, Ordering::Release);
}
}
#[cfg(target_os = "windows")]
fn reveal_main_window(app_handle: &tauri::AppHandle) {
if let Some(window) = app_handle.get_webview_window("main") {
if let Err(error) = window.set_focusable(true) {
log::warn!("Could not make the main window focusable: {error}");
}
if let Err(error) = window.show() {
eprintln!("Failed to reveal the main window: {error}");
}
}
}
fn restore_pending_main_window(app_handle: &tauri::AppHandle) {
if app_handle
.try_state::<MainWindowRestoreState>()
@@ -18275,12 +18221,10 @@ pub fn run() {
.plugin(tauri_plugin_deep_link::init())
.manage(Aria2DaemonGuard::new())
.setup(move |app| {
startup_trace("setup:begin");
let storage_layout = crate::storage::StorageLayout::resolve(
app.handle(),
setup_storage_mode.clone(),
)?;
startup_trace("setup:storage");
let main_window_config = app
.config()
.app
@@ -18298,7 +18242,6 @@ pub fn run() {
main_window_builder =
main_window_builder.data_directory(storage_layout.webview_dir().to_path_buf());
}
startup_trace("setup:window-builder");
let mut sys = sysinfo::System::new_all();
sys.refresh_all();
@@ -18312,7 +18255,6 @@ pub fn run() {
log::info!("==========================");
build_main_tray(app.handle())
.map_err(|error| format!("failed to create tray menu: {error}"))?;
startup_trace("setup:tray");
#[cfg(target_os = "macos")]
{
use tauri::menu::{MenuItem, WINDOW_SUBMENU_ID};
@@ -18365,7 +18307,6 @@ pub fn run() {
// malformed or interrupted transaction cannot reach a missing
// Tauri state entry at startup.
app.manage(database);
startup_trace("setup:database");
if let Err(error) = recover_torrent_move_journals(
app.handle(),
&*app.state::<crate::db::DbState>(),
@@ -18379,7 +18320,6 @@ pub fn run() {
) {
log::warn!("Download replacement recovery did not complete: {error}");
}
startup_trace("setup:recovery");
// Establish Firelink-owned Aria2 routing-table paths after the
// existing data-root initializer has created the selected storage
// directory, but before the daemon launcher is scheduled. A
@@ -18441,7 +18381,6 @@ pub fn run() {
log::warn!("could not identify retained torrent metadata: {error}");
}
}
startup_trace("setup:torrent-cache");
let initial_pairing_token = {
// Generate a temporary session token for the extension server on startup.
// The frontend will hydrate the real token via IPC once it mounts,
@@ -18538,60 +18477,18 @@ pub fn run() {
scheduler_settings: Arc::clone(&scheduler_settings),
queue_manager,
});
startup_trace("setup:app-state");
if let Err(error) = power_manager.activate() {
log::error!("power: failed to activate backend power management: {error}");
}
startup_trace("setup:power");
// Build the window only after all command state is registered. This
// prevents the frontend from racing startup and invoking IPC before
// the database and portable storage layout are available.
let startup_size = persisted_settings
.as_ref()
.and_then(|settings| settings.main_window_size.as_ref())
.and_then(|size| crate::window_geometry::normalize_main_window_size(Some(size)))
.unwrap_or_else(crate::window_geometry::default_main_window_size);
let startup_size = app
.primary_monitor()
.ok()
.flatten()
.and_then(|monitor| {
let scale_factor = monitor.scale_factor();
if !scale_factor.is_finite() || scale_factor <= 0.0 {
return None;
}
let work_area = monitor.work_area().size;
let logical_width = (work_area.width as f64 / scale_factor).round() as u32;
let logical_height = (work_area.height as f64 / scale_factor).round() as u32;
Some(crate::window_geometry::clamp_main_window_size(
startup_size.clone(),
logical_width,
logical_height,
))
})
.unwrap_or(startup_size);
main_window_builder = main_window_builder
.inner_size(startup_size.width as f64, startup_size.height as f64)
.prevent_overflow();
#[cfg(target_os = "windows")]
{
// Wry installs its parent WM_SETFOCUS handler while WebView2
// is still being initialized. Keep the host hidden and
// non-activatable until RunEvent::Ready so Windows cannot
// re-enter that handler during native construction.
main_window_builder = main_window_builder
.visible(false)
.focused(false)
.focusable(false);
}
main_window_builder
.build()
.map_err(|error| format!("failed to create main window: {error}"))?;
startup_trace("setup:window-built");
restore_pending_main_window(app.handle());
startup_trace("setup:pending-window-restored");
#[cfg(any(target_os = "windows", target_os = "linux"))]
dispatch_opened_torrent_paths(
@@ -18612,9 +18509,7 @@ pub fn run() {
Ok(None) => {}
Err(error) => eprintln!("Failed to read startup deep link: {error}"),
}
startup_trace("setup:deep-link");
crate::scheduler::spawn_scheduler(app.handle().clone(), scheduler_settings);
startup_trace("setup:scheduler");
let global_speed_limit = persisted_settings
.as_ref()
@@ -18657,12 +18552,10 @@ pub fn run() {
let aria2_secret_clone = aria2_secret.clone();
let app_handle_bg = app.handle().clone();
tauri::async_runtime::spawn(async move {
startup_trace("aria2-task:begin");
let mut ws_port = 6800;
match resolve_bundled_binary_path(&app_handle_bg, "aria2c") {
Ok(binary_path) => {
startup_trace("aria2-task:binary");
let mut success = false;
let mut attempted_rpc_port = false;
let mut startup_failure = None;
@@ -18756,7 +18649,6 @@ pub fn run() {
match cmd.spawn() {
Ok(mut child) => {
startup_trace("aria2-task:spawned");
// Give it a moment to fail if port is in use
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
if let Ok(Some(status)) = child.try_wait() {
@@ -18846,7 +18738,6 @@ pub fn run() {
attempt_port,
async_dns_supported
);
startup_trace("aria2-task:ready");
ready = true;
break;
}
@@ -18899,10 +18790,8 @@ pub fn run() {
ws_retries = 0;
}
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
startup_trace("aria2-task:websocket-attempt");
let ws_url = format!("ws://127.0.0.1:{}/jsonrpc", ws_port);
if let Ok((ws_stream, _)) = tokio_tungstenite::connect_async(&ws_url).await {
startup_trace("aria2-task:websocket-connected");
ws_retries = 0; // reset on success
if let Ok(mut startup_error) = app_handle_bg
.state::<Aria2DaemonGuard>()
@@ -18916,7 +18805,6 @@ pub fn run() {
}
}
reconcile_aria2_downloads(&app_handle_bg).await;
startup_trace("aria2-task:reconciled");
use futures_util::StreamExt;
let (_, mut read) = ws_stream.split();
while let Some(msg) = read.next().await {
@@ -19043,7 +18931,6 @@ pub fn run() {
let poll_secret = aria2_secret.clone();
let poll_mgr = Arc::clone(&queue_manager_poll);
tauri::async_runtime::spawn(async move {
startup_trace("poller:begin");
let mut interval = tokio::time::interval(std::time::Duration::from_millis(1000));
let mut observations: HashMap<String, Aria2ConnectionObservation> = HashMap::new();
let mut missing_gid_recovery_at: HashMap<String, Instant> = HashMap::new();
@@ -19055,7 +18942,6 @@ pub fn run() {
let mut relocation_checks = HashSet::new();
loop {
interval.tick().await;
startup_trace("poller:tick");
// Terminal cleanup removes a download's GID mapping. Do
// not retain one observation per historical download for
// the lifetime of the poller.
@@ -19103,11 +18989,9 @@ pub fn run() {
.unwrap_or_else(|| "none".to_string()),
active_poll_started.elapsed().as_millis()
);
startup_trace("poller:rpc-failed");
continue;
}
};
startup_trace("poller:rpc-ok");
if let Some(active_arr) = active_list.as_array() {
let mut seen_ids = HashSet::new();
let mut seen_gids = HashSet::new();
@@ -19787,7 +19671,6 @@ pub fn run() {
let ext_app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
startup_trace("extension-task:begin");
while let Err(error) = extension_server::start_server(
ext_app_handle.clone(),
server_pairing_token.clone(),
@@ -19800,7 +19683,6 @@ pub fn run() {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
}
});
startup_trace("setup:tasks-started");
Ok(())
})
.plugin(
@@ -19847,9 +19729,6 @@ pub fn run() {
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_clipboard_manager::init())
.on_window_event(|window, event| {
if window.label() == "main" {
startup_trace_window_event("window-event:main");
}
if properties_window::is_properties_window_label(window.label()) {
let resize = match event {
tauri::WindowEvent::Resized(size) => {
@@ -19941,15 +19820,6 @@ pub fn run() {
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app_handle, event| match event {
tauri::RunEvent::Ready => {
startup_trace("run-event:ready-begin");
mark_main_window_startup_complete(app_handle);
#[cfg(target_os = "windows")]
reveal_main_window(app_handle);
#[cfg(not(target_os = "windows"))]
restore_pending_main_window(app_handle);
startup_trace("run-event:ready-end");
}
#[cfg(target_os = "macos")]
tauri::RunEvent::Opened { urls } => {
let paths = collect_opened_torrent_paths(
+1 -1
View File
@@ -16,7 +16,7 @@
"create": false,
"title": "Firelink",
"width": 1280,
"height": 800,
"height": 760,
"minWidth": 960,
"minHeight": 640,
"transparent": false
+1 -1
View File
@@ -5,7 +5,7 @@
"create": false,
"title": "Firelink",
"width": 1280,
"height": 800,
"height": 760,
"minWidth": 960,
"minHeight": 640,
"transparent": false,
+1 -1
View File
@@ -5,7 +5,7 @@
"create": false,
"title": "Firelink",
"width": 1280,
"height": 800,
"height": 760,
"minWidth": 960,
"minHeight": 640,
"transparent": true,
+1 -1
View File
@@ -5,7 +5,7 @@
"create": false,
"title": "Firelink",
"width": 1280,
"height": 800,
"height": 760,
"minWidth": 960,
"minHeight": 640,
"transparent": true,