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,