From 0dfacfa661028ea54833fef7cd9d0a8fe2138828 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 12 Sep 2026 20:42:57 +0200 Subject: [PATCH] fix(tauri): cancel peer work at native close boundary The launcher previously began peer cancellation only after the frontend had finished draining its pending invokes and the Tauri process emitted Exit. A pending acknowledgement could therefore keep the close handler waiting while the peer still owned the work needed to complete that acknowledgement. Close admission and unrar cancellation now begin on the native main-window CloseRequested event. A tracked application task signals the currently owned peer runtime early, while the existing Exit handler still waits for all admitted invokes, takes and joins the runtime, and drains background tasks. An atomic latch keeps repeated close and exit notifications from scheduling multiple shutdown requests, and a runtime that is being created remains owned by its in-flight invoke until the final join boundary. Test Plan: - `just fmt` -- passed - `just test` -- passed (796 Rust tests) - `just frontend-test` -- passed (98 tests) - `just clippy` -- passed - `just build` -- passed - `git diff --check` -- passed - Native window-close timing was not available in the automated UI surface. --- .../src-tauri/src/lib.rs | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs index 87c930d..a18768b 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -9,7 +9,7 @@ use std::{ Mutex, OnceLock, Weak, - atomic::{AtomicU64, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, }, time::{Duration, SystemTime, UNIX_EPOCH}, }; @@ -667,6 +667,7 @@ struct LanSpreadState { game_transfer_status: Arc>, background_tasks: AppTaskScope, app_invokes: AppInvokeScope, + application_shutdown_started: AtomicBool, /// Cancellation controls for lexically owned unrar process workers. /// Workers themselves synchronously join their child and pipe readers. active_unrar_children: Arc>, @@ -4767,6 +4768,13 @@ pub fn run() { .manage(LanSpreadState::default()) .manage(PeerEventTx(tx_peer_event)) .manage(UiStateTx(tx_ui_state)) + .on_window_event(|window, event| { + if window.label() == "main" + && matches!(event, tauri::WindowEvent::CloseRequested { .. }) + { + begin_application_shutdown(window.app_handle()); + } + }) .setup(move |app| { let state_dir = app.path().app_data_dir()?; std::fs::create_dir_all(&state_dir)?; @@ -4823,17 +4831,12 @@ pub fn run() { } fn shutdown_application(app_handle: &AppHandle) { + begin_application_shutdown(app_handle); let state = app_handle.state::(); let peer_runtime = state.peer_runtime.clone(); let app_invokes = state.app_invokes.clone(); let background_tasks = state.background_tasks.clone(); - // Close every invoke entry point before any shutdown mutation. Unrar is - // cancelled next; its lexical process worker settles before the peer - // runtime can report that the install task has stopped. - app_invokes.close_admission(); - cancel_active_unrar_workers(app_handle); - tauri::async_runtime::block_on(async move { // Once admission is closed, every earlier application invoke must // return before the runtime can be taken. This both owns setup-process @@ -4854,6 +4857,35 @@ fn shutdown_application(app_handle: &AppHandle) { }); } +/// Starts application shutdown at the first native close boundary. +/// +/// The frontend close handler still drains its own asynchronous ownership before +/// destroying the webview, while this synchronous boundary cancels peer work +/// early enough for pending frontend invokes to observe dropped acknowledgements +/// instead of waiting for the work they are acknowledging to finish naturally. +fn begin_application_shutdown(app_handle: &AppHandle) { + let state = app_handle.state::(); + if state + .application_shutdown_started + .swap(true, Ordering::AcqRel) + { + return; + } + + state.app_invokes.close_admission(); + cancel_active_unrar_workers(app_handle); + + // The runtime slot is async because startup and replacement are serialized + // with application invokes. Keep this request inside the application task + // scope so a concurrent startup cannot leave the early signal detached. + let peer_runtime = state.peer_runtime.clone(); + state.background_tasks.spawn(async move { + if let Some(handle) = peer_runtime.read().await.as_ref() { + handle.shutdown(); + } + }); +} + async fn await_with_slow_warning(future: F, warn_after: Duration, warning: &str) -> F::Output where F: std::future::Future,