From d7b1dea0d3aa793b6e928fff6188e260db0fc67e Mon Sep 17 00:00:00 2001 From: Miha Krajnc Date: Mon, 30 Mar 2026 15:53:50 +0200 Subject: [PATCH] fix: persist deep link to file before launcher self-update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, `update.install()` calls `std::process::exit(0)` inside the Tauri updater after launching the NSIS installer. This means the deep link preservation code added in dee45b5 (which pushes the URL into `env.args_os` and calls `tauri::process::restart`) never executes — it is dead code on Windows. The NSIS installer does pass current args via `/ARGS`, but URL-like deep links containing `?`, `&`, `=` may get mangled when passed unquoted through the NSIS command line. Fix: write the deep link URL to a plain text file (`deeplink-state.txt`) **before** calling `update.install()`, and read it back on startup as a fallback when no deep link is found in command-line args. The file is cleaned up after the deep link is consumed in the launch flow. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 66 +++++++++++++++++++++++++++++++++++++++++++ core/Cargo.lock | 2 +- core/src/flow.rs | 9 ++++-- core/src/installs.rs | 4 +++ core/src/protocols.rs | 33 +++++++++++++++++++++- src-tauri/Cargo.lock | 4 +-- src-tauri/src/lib.rs | 13 +++++++++ 7 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..6a8b2920 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,66 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Decentraland Launcher — a cross-platform (macOS/Windows) desktop app built with **Tauri v2** that manages installation, updating, and launching of the Decentraland Explorer client. Rust backend, React/TypeScript frontend, Tauri IPC bridge. + +## Build & Development Commands + +```bash +npm install # Install all dependencies +npm run tauri dev # Run in development mode +npm run tauri build # Build for distribution +npm run format # Format JS/TS (Prettier) + check Rust formatting +npm run analyze # Run clippy on all three Rust crates with -D warnings +``` + +**Rust-only commands** (run from `core/`, `src-tauri/`, or `src-auto-auth/`): +```bash +cargo fmt # Format Rust code +cargo check # Type-check +cargo clippy --all-targets --all-features -- -D warnings +cargo test # Run tests for that crate +``` + +The pre-commit hook (`.githooks/pre-commit`) runs fmt, check, clippy, and test on all three Rust crates. + +## Architecture + +``` +Frontend (React/TS) ←—Tauri IPC Channel—→ Backend (Rust) + src/ core/src/ + components/Home/Home.tsx flow.rs (orchestration) + installs.rs → downloads.rs → compression.rs + types.rs (Status/Step enums serialized to TS) +``` + +**Three Rust crates** (no workspace — independent Cargo.toml files): +- **`core/`** (`dcl-launcher-core`) — Main business logic: launch flow orchestration, downloads, installation, S3 version fetching, analytics, error handling, deep linking, auto-auth +- **`src-tauri/`** (`Decentraland-Launcher`) — Tauri app shell: IPC command handlers (`launch`, `retry`), self-update logic, deep link setup +- **`src-auto-auth/`** — Platform-specific token extraction (macOS DMG xattr, Windows PE tail) + +**Frontend** (`src/`): React + Vite. Main UI in `components/Home/Home.tsx`. TypeScript types in `components/Home/types.ts` mirror Rust enums in `core/src/types.rs` — these must stay in sync. + +**Core flow** (`core/src/flow.rs`): Fetch → Download → Install → Launch, with retry logic and status reporting over Tauri channels. + +## Rust Conventions + +**Rust edition 2024** for `core` and `src-auto-auth`, **2021** for `src-tauri`. Minimum Rust version: **1.88.0**. + +**Strict clippy enforcement** (see `core/src/lib.rs`): +- `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::indexing_slicing, clippy::arithmetic_side_effects, clippy::todo, clippy::dbg_macro)]` +- `#![warn(clippy::all, clippy::pedantic, clippy::nursery)]` +- Use `?` operator and proper error handling instead of unwrap/expect/panic +- Use `.get()` for safe indexing instead of `[]` + +**Error handling**: Custom `StepError` enum in `core/src/errors.rs` with error codes (E0000–E3002). Use `anyhow` for generic errors, `thiserror` for enum derives. + +**Serde**: Use `#[serde(rename_all = "camelCase")]` for types crossing the IPC boundary to match JavaScript conventions. + +**Platform-specific code**: Use `#[cfg(target_os = "macos")]` and `#[cfg(target_os = "windows")]` for platform-conditional logic. + +## Commit Convention + +Conventional Commits: `feat:`, `fix:`, `chore:`, `style:` with optional scopes like `(release)`, `(windows)`, `(macos)`, `(auto-auth)`. diff --git a/core/Cargo.lock b/core/Cargo.lock index 04e6c2dc..4bccff0b 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -261,7 +261,7 @@ dependencies = [ [[package]] name = "dcl-launcher-core" -version = "1.12.11" +version = "1.12.19" dependencies = [ "anyhow", "core-foundation 0.10.1", diff --git a/core/src/flow.rs b/core/src/flow.rs index 864258f5..b1f16bd5 100644 --- a/core/src/flow.rs +++ b/core/src/flow.rs @@ -472,7 +472,7 @@ impl WorkflowStep for AppLaunchStep { let any_is_running = self.is_any_instance_running().await?; let is_local_scene = deeplink.has_true_value(ARG_LOCAL_SCENE) || args.local_scene; - if !open_new_instance && any_is_running && !is_local_scene { + let result = if !open_new_instance && any_is_running && !is_local_scene { channel.send(Status::State { step: Step::DeeplinkOpening, })?; @@ -505,7 +505,12 @@ impl WorkflowStep for AppLaunchStep { .launch_explorer(Some(deeplink), None) .await?; StepResult::Ok(()) - } + }; + + // Clear persisted deep link file after consumption + Protocol::clear_file(); + + result } None => { //TODO passed version if specified manually from upper flow diff --git a/core/src/installs.rs b/core/src/installs.rs index 93bef655..310bee76 100644 --- a/core/src/installs.rs +++ b/core/src/installs.rs @@ -90,6 +90,10 @@ pub fn deeplink_bridge_path() -> PathBuf { explorer_path().join("deeplink-bridge.json") } +pub fn deeplink_state_path() -> PathBuf { + explorer_path().join("deeplink-state.txt") +} + // There is no point to recovery if the app failed to create working directory #[allow(clippy::expect_used)] fn get_app_base_path() -> PathBuf { diff --git a/core/src/protocols.rs b/core/src/protocols.rs index 1ac85b08..1c0b2e4d 100644 --- a/core/src/protocols.rs +++ b/core/src/protocols.rs @@ -3,7 +3,7 @@ use std::result::Result; use std::sync::Mutex; use url::form_urlencoded; -use log::{error, warn}; +use log::{error, info, warn}; static PROTOCOL_STATE: Mutex> = Mutex::new(None); const PROTOCOL_PREFIX: &str = "decentraland://"; @@ -122,6 +122,37 @@ impl Protocol { } } + pub fn save_to_file(deeplink: &DeepLink) { + let path = crate::installs::deeplink_state_path(); + if let Err(e) = std::fs::write(&path, deeplink.original()) { + error!("Failed to persist deep link to file: {}", e); + } else { + info!("Persisted deep link to {}", path.display()); + } + } + + pub fn load_from_file() -> Option { + let path = crate::installs::deeplink_state_path(); + match std::fs::read_to_string(&path) { + Ok(content) if !content.is_empty() => { + info!("Loaded persisted deep link from {}", path.display()); + Some(content) + } + Ok(_) | Err(_) => None, + } + } + + pub fn clear_file() { + let path = crate::installs::deeplink_state_path(); + if path.exists() { + if let Err(e) = std::fs::remove_file(&path) { + error!("Failed to remove persisted deep link file: {}", e); + } else { + info!("Cleared persisted deep link file"); + } + } + } + pub fn try_assign_value_from_vec(&self, value: &Vec) { for v in value { if let Ok(deeplink) = DeepLink::new(v.to_owned()) { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index eb2baaad..e3eb350f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "Decentraland-Launcher" -version = "1.12.6" +version = "1.12.19" dependencies = [ "anyhow", "dcl-launcher-core", @@ -663,7 +663,7 @@ dependencies = [ [[package]] name = "dcl-launcher-core" -version = "1.12.6" +version = "1.12.19" dependencies = [ "anyhow", "core-foundation 0.10.1", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c7854fbf..9afb2f4c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -210,6 +210,12 @@ async fn update_if_needed_and_restart( .await?; channel.send_silent(LauncherUpdate::InstallingUpdate.into()); + + // Persist deeplink to file BEFORE install (install may call exit(0) on Windows) + if let Some(deeplink) = Protocol::value() { + Protocol::save_to_file(&deeplink); + } + update.install(content)?; info!("update installed"); @@ -237,6 +243,13 @@ fn setup_deeplink(a: &App, protocol: &Protocol) { let args: Vec = AppEnvironment::raw_cmd_args().collect(); protocol.try_assign_value_from_vec(&args); + // Fallback: load from persisted file (survives Windows NSIS restart) + if Protocol::value().is_none() { + if let Some(saved) = Protocol::load_from_file() { + protocol.try_assign_value(saved); + } + } + #[cfg(target_os = "macos")] { let protocol = protocol.clone();