fix(launcher): authorize window destruction after close drains

A native WM_DELETE_WINDOW reproduction completed the frontend drain and then
failed with "window.destroy not allowed". All three windows lacked the destroy
permission used by Tauri's onCloseRequested wrapper. Removing the listener had
previously let a second click bypass that denied IPC; retaining the listener
made every click fail. The frontend was not stuck waiting on its own listener.

Grant window destruction to the three configured application windows. Keep the
existing frontend drains, early peer cancellation, and final runtime/task joins.
Test the application's actual generated RuntimeAuthority, including expanded
plugin defaults, instead of assuming a mocked successful destroy proves access.

Explicitly include generated capabilities and ACL manifests in the context
constructor's rustc dependencies. The configured compiler cache reused the old
library after a permission-only edit because Tauri's macro reads these files
without recording compiler dependencies. A remove/restore native probe now
rebuilds with the correct permission in both directions.

Document the complete ownership chain and failure evidence. Add a PID-checked
X11 WM_DELETE_WINDOW helper for repeatable close-button probes without killing
the process or bypassing the frontend boundary.

Test Plan:
- Native baseline: destroy denied and process remained alive after one request.
- Resolved-ACL regression failed before the permission fix and passes afterward.
- Native main/companion close probes: one request per window; normal process exit.
- Permission-only rebuild with compiler cache: normal exit in 197 ms.
- Production executable: normal exit in 43 ms; QUIC port released and rebound.
- just test: 797 passed on unchanged rerun after one initial subprocess fixture
  startup-marker timeout, before that test exercised cancellation.
- just frontend-test: 99 passed.
- just fmt, just clippy, just build, and git diff --cached --check: passed.
- Native helper compiled with -Wall -Wextra -Werror.
- Native probes ran on Linux X11/XWayland; Windows/macOS were not measured.
This commit is contained in:
ddidderr committed 2026-09-12 21:36:06 +02:00
1 parent 25bf56ff13
commit 9171560ad4
7 files changed
+261 -5

No files matched your search

@@ -149,8 +149,8 @@ impl AppTaskScope {
///
/// Tauri owns the invoke futures, so they cannot be spawned in our task scope.
/// A tracker token instead makes each admitted future part of the application
/// shutdown boundary: shutdown closes admission, waits for every token to be
/// dropped, and only then takes ownership of the peer runtime to stop it. The
/// shutdown boundary: shutdown closes admission, requests peer cancellation,
/// waits for every token to be dropped, and then takes the runtime to join it. The
/// separate serial lock orders game-directory startup, sharing transitions,
/// and sharing-gated Call-to-Play publication through their acknowledgements
/// and UI commits without participating in shutdown locking.
@@ -4727,6 +4727,15 @@ struct ProtocolMismatchSnapshot {
mismatch: Option<ProtocolMismatch>,
}
fn application_context() -> tauri::Context<tauri::Wry> {
// generate_context! reads these files through the proc macro's filesystem
// API. Explicit includes also put them in rustc's dependency information,
// so compiler caches cannot reuse an application with an older ACL.
const _: &str = include_str!(concat!(env!("OUT_DIR"), "/capabilities.json"));
const _: &str = include_str!(concat!(env!("OUT_DIR"), "/acl-manifests.json"));
tauri::generate_context!()
}
#[allow(clippy::missing_panics_doc)]
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@@ -4821,7 +4830,7 @@ pub fn run() {
spawn_peer_event_loop(app.handle().clone(), rx_peer_event, rx_ui_state);
Ok(())
})
.build(tauri::generate_context!())
.build(application_context())
.expect("error while building tauri application")
.run(|app_handle, event| {
if matches!(event, tauri::RunEvent::Exit) {
@@ -4904,6 +4913,40 @@ where
mod tests {
use super::*;
#[test]
fn every_frontend_close_boundary_can_destroy_its_window() {
// Test the application's resolved ACL, including Tauri's default
// permission sets. Mocked frontend invokes cannot detect a missing
// permission at the end of an otherwise successful close drain.
let mut context = application_context();
let authority = context.runtime_authority_mut();
for label in ["main", "main-logs", "unpack-logs"] {
for command in [
"plugin:event|listen",
"plugin:event|unlisten",
"plugin:window|destroy",
] {
assert!(
authority
.resolve_access(command, label, label, &tauri::ipc::Origin::Local)
.is_some(),
"{label} must be allowed to call {command} for its frontend close boundary",
);
}
}
assert!(
authority
.resolve_access(
"plugin:window|destroy",
"untrusted",
"untrusted",
&tauri::ipc::Origin::Local
)
.is_none(),
"window destruction must remain limited to the configured application windows",
);
}
fn download_attempt(id: &str, attempt_id: u64) -> DownloadAttemptKey {
serde_json::from_value(serde_json::json!({
"id": id,