fix(deeplink): the file lock ate deep links while the app was running — relay them - #492
Merged
Merged
Conversation
… — relay them Found live during end-to-end verification of #491 on the canonical layout: with 4DA running, "Open in 4DA" spawned a second instance that died at the pre-Tauri single-instance file lock — long before tauri_plugin_single_instance (whose argv forwarding is the designed warm-path delivery) ever initializes. The URL went nowhere. The isolated worktree test had passed only because its two processes resolved DIFFERENT data dirs, so the file lock never collided; in production both resolve the same dir, so EVERY warm deep link was dropped. The relay keeps the lock's WAL-corruption guarantee intact: the rejected instance validates the URL, parks it in data_dir/.deeplink-relay (atomic temp+rename, timestamped), and exits without touching the database. The primary polls the path once a second, consumes at most once, discards stale entries (>60s — a crashed primary must not replay an old activation), brings the window to front the way the plugin callback would have, and emits through the same validated deep-link path the event listener uses. The plugin's own forwarding stays registered — it still covers the case where the file lock soft-fails on I/O error and the second instance reaches Tauri. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EnyENPW2GcG8GU7x1rJ8wN
runyourempire
enabled auto-merge (squash)
August 18, 2026 15:15
runyourempire
added a commit
that referenced
this pull request
Aug 18, 2026
…er land in a windowless app (#493) ## The relay delivered — into a windowless app Operator's live repro (screenshots, 2026-08-19): Chrome's protocol dialog fired, the secondary's console flashed, then nothing. The primary's log shows `Deep-link received via relay` at 15:50:31 — the #492 handoff worked — but the **main window had been destroyed**, so `get_webview_window("main")` was `None` and the emit went nowhere. Window introspection confirmed only the hidden briefing window remained. The killer left no trace: no `CloseRequested` (hide-to-tray never logged), no app-side `destroy()` caller exists for main (only briefing/notification recreate themselves), and the notification window died the same way minutes earlier ("JS never loaded — recreating") — suspected WebView2-level death. Whatever kills it, nothing in the app could ever bring main back: deep links, tray **Show 4DA**, and tray left-click all silently no-op until an app restart. ## Fix: `ensure_main_window()` — return main, or rebuild it from its config - **Deep-link delivery** (plugin event listener + relay poll, unified in `deliver_deep_link`): window present → front it and emit; window gone → **park the URL via the cold-start pending mechanism and recreate the window** — the fresh frontend collects the parked URL on mount via `take_pending_deep_link`, so no emit races the webview boot. - **All three tray show paths** use the same primitive — a destroyed window is no longer unrecoverable. - **Forensic hook**: `WindowEvent::Destroyed` is now logged with its label, so the next silent window death names its moment. ## Second bug from the same trace: the rejected instance opened the shared DB The secondary's argv scan ran `validate_deep_link_url`, whose rejection path writes a security event via `get_database()` — the operator's console shows the REJECTED second instance logging `Initializing database ... 4da.db` (argv[0], the exe path, always fails validation → a bogus "rejected deep-link" security event on every relay). That is the exact WAL hazard the pre-Tauri lock exists to prevent. The scan is now a bare scheme-prefix check on `args().skip(1)` with zero DB contact; the primary validates on receipt, where it always did. ## Verification - clippy `-D warnings` clean on the plain lib target; `single_instance` 8/8, `utils::url` 20/20; size + doc gates pass. - Post-merge live plan (canonical layout, real key): (1) warm path with window present; (2) **destroy the main window, fire the deep link, watch it recreate and activate** — the operator's exact failure; (3) secondary console shows no DB init and no bogus security event. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01EnyENPW2GcG8GU7x1rJ8wN Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The file lock ate deep links while the app was running
Found during live end-to-end verification of #491 on the canonical layout. With 4DA running, "Open in 4DA" spawns a second process carrying the
fourda://URL in argv — and the pre-Tauri single-instance file lock exits it long beforetauri_plugin_single_instance(the designed warm-path forwarder, wired up in #491) ever initializes. The URL was dropped silently; the app log showed no "Deep-link received".The isolated-worktree test in #491's verification had passed only by accident of layout: its two processes resolved different data dirs, so the file lock never collided and the plugin got its chance. In production both processes resolve the same data dir — so every warm deep link died at the lock. (The engine coexists by design:
initialize_pre_tauri(false)skips the lock, so the lock holder is always a GUI instance.)Fix: relay through the data dir, keep the lock's guarantee
The lock exists to stop a second process reaching the SQLite WAL — that property is untouched. The rejected instance now validates the URL, parks it in
data_dir/.deeplink-relay(atomic temp+rename, timestamped), and exits without touching the database. The primary polls the path once per second: consume-at-most-once, stale entries (>60s) discarded so a crashed primary can't replay an old activation, window brought to front the way the plugin callback would have, then emitted through the same validateddeep-link-activatepath.The plugin's own forwarding stays registered — it still covers the soft-fail case where the file lock errors on I/O and the second instance reaches Tauri.
Verification
single_instancetests including 3 new relay tests (roundtrip + consume-once, stale discard, garbage tolerance)fourda://activate?key=<real key>against the running canonical app — the exact scenario that failed — and confirmactivate_licensesuccess via Victauri IPC trace before calling the feature production-ready.🤖 Generated with Claude Code
https://claude.ai/code/session_01EnyENPW2GcG8GU7x1rJ8wN