37420e26edad797d58b173439e76f335e16a9bd6
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
37420e26ed |
fix(peer): coalesce full UI view publications
Keep one queued and one replaceable pending snapshot for remote-library and Call-to-Play views while preserving lifecycle-event FIFO delivery. Generation barriers and fenced drains prevent stale nonempty views from crossing disable or acknowledgement boundaries. Test Plan: - just test - just clippy - focused burst, lifecycle ordering, fence, repeated-barrier, and stale-view tests - independent ordering review - git diff --check |
||
|
|
60fd7ba0c2
|
feat(peer)!: cut over to authenticated catalog sharing
Replace address-only trust and pushed peer state with installation identities, SPKI-pinned QUIC, candidate-only discovery, and bounded responder-owned protocol-8 pulls. The runtime now owns each network generation and all admitted work through shutdown. Add exact bundled content identities, reproducible manifest publishing, capability-confined downloads, streaming BLAKE3 verification, quarantine and retry, and crash-recoverable download and install transactions. Ship generated fixture catalogs and fail closed when production manifests are absent. The Tauri backend exposes durable sharing policy, redacted identity state, and attempt-keyed transfer snapshots. Frontend consumption follows in the next commit. Repository-wide test certificates and protocol-7 paths are removed. BREAKING CHANGE: peers must use protocol 8 and exact catalog content artifacts; protocol-7 frames and shared-certificate identities are no longer accepted. Test Plan: - `just test` -- passed on the completed stack (708 workspace tests) - `just clippy` -- passed on the completed stack - `just build` -- passed with fixture catalogs on the completed stack - `just catalog-check-production` -- failed closed because the external production manifest corpus is absent - `git diff --cached --check` -- passed |
||
|
|
5bb4a8b611
|
fix(peer): drain cancelled download workers
Keep initial peer transfers, retry attempts, and chunk receivers structurally owned until they quiesce. Cancellation now stops opening new streams, flushes accepted file writes, and drains active work before ownership rollback can begin. Leave operation admission owned by the running download task when liveness detects that every source disappeared, and emit the peers-gone notification only once. Test Plan: - just fmt (Rust and configured formatters completed; 39 pre-existing rumdl findings remain) - just clippy - just test - git diff --cached --check |
||
|
|
691176e1d5
|
fix(peer): confine download mutations to game root handles
Remote manifests were validated before mutation, but preparation, chunk writes, sentinel transactions, and ownership recovery later reopened ambient paths. A link or reparse-point swap between those steps could redirect a mutation outside the validated game root. Introduce a retained ConfinedGameRoot capability backed by cap-primitives. Carry typed validated destinations into chunk plans, walk every component without following links, and perform payload, sentinel, stale-file, abort, and recovery mutations relative to the retained handle. File writes and verification use the same opened handle, while final durability syncs payload files and unique parent directories before committing version.ini. Make ownership-record publication phase-aware as well. A directory-sync failure after record rename now stops before payload mutation without performing an unsafe old-sentinel rollback. Record the capability-root, bounded-handle, hard-link, and unproven Windows durability tradeoffs in the decision log. Test Plan: - `just clippy` -- passed - `just test` -- passed; 185 peer tests and the full workspace are green - `just fmt` -- Rust, TOML, and Prettier completed; command remains nonzero on 39 pre-existing rumdl issues outside this change - `git diff --cached --check` -- passed |
||
|
|
62cd9306bd
|
feat(peer): journal download file ownership
Track the exact regular files owned by each completed and in-flight peer download instead of sweeping every non-reserved path after cancellation. Bind the record to the canonical games directory, publish pending ownership before payload mutation, and use the final version.ini rename as the recovery commit point. Make replacement, cancellation, and startup recovery preserve unknown files and install state while removing stale or partial downloader-owned bytes. Add a new-format baseline so legacy discarded sentinels cannot make partially modified payloads ready, sync payload and journal state in transaction order, and serialize startup recovery against operation admission. Document ambiguous legacy target adoption, portable alias transitions, and the other ownership tradeoffs in the refactor decision log. Test Plan: - `just clippy` -- passed - `just test` -- passed (182 peer-core tests plus the full workspace) - `just fmt` -- Rust, TOML, and Prettier formatting completed; the command then stopped on 40 pre-existing rumdl findings in unrelated Markdown content - `git diff --cached --check` -- passed |
||
|
|
a6ed60a538
|
feat(peer): validate manifests before download mutation
Why: - Remote and UI-echoed file descriptions could reach transaction and storage code one entry at a time, so a hostile late path could mutate earlier files. - Per-file consensus also accepted malformed peer lists and let duplicate rows inflate a source's vote. What: - Add a complete protocol-7 manifest adapter with catalog-root confinement, portable path and alias rules, reserved-path protection, shape and size caps, symlink/reparse inspection, and zero-mutation tests. - Keep download selection in the peer core, validate every peer manifest before consensus, and pass only the validated manifest into storage/orchestration. - Canonicalize locally advertised paths, cap exact chunk receives, and preserve the local-only install fast path. - Record the chosen safety limits and follow-up ownership/catalog decisions. Test Plan: - just clippy - just test - just frontend-test - just build - just fmt (Rust/TOML/Prettier completed; rumdl reports 39 pre-existing issues) - git diff --cached --check |
||
|
|
9835e77e8d
|
feat: store launcher state outside game dirs
Move launcher-owned metadata from game roots into the configured peer state area. Peer identity, the local library index, install intent logs, and setup markers now live under app/CLI state instead of being written beside games. The Tauri shell passes its app data directory into the peer, and the peer CLI runs the same path through its explicit --state-dir. Add a dedicated pre-start migration phase for legacy files. It migrates the old global library index, per-game install intents, and the old first-start marker into app state, then deletes legacy files only after the replacement write succeeds. Normal scan, install, recovery, and transfer paths no longer read legacy state files. Rename the old first-start meaning to setup_done and only set it after launching game_setup.cmd. Start/setup scripts keep the shared argument shape, while server_start.cmd now uses cmd /k and a visible window so server logs stay open for inspection. While validating the Docker scenario matrix, make download terminal events come from the handler after local state refresh and operation cleanup. This makes download-finished/download-failed safe points for immediate follow-up CLI commands. Also update the multi-peer chunking scenario to use a sparse archive large enough to actually span multiple production chunks. Test Plan: - just fmt - just test - just frontend-test - just build - just clippy - git diff --check - python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py Refs: local app-state migration discussion |
||
|
|
c3800461a4
|
fix(peer): delete partial files when a download is cancelled
Cancelling an in-flight download via `PeerCommand::CancelDownload` previously
torn down the network transfer and cleared `active_downloads`, but left the
partial `.eti` archive(s) sitting in the game root forever. The next library
scan still picked up the half-written files as a "downloaded" game, and the
only escape was the `Remove files` action. This is the symmetric fix to
`62ceb06 feat(peer): remove downloaded game files safely`: the cancel path
must clean up after itself the same way an explicit remove does.
The fix introduces a dedicated `download/storage.rs` module that owns both the
existing pre-allocation step (`prepare_game_storage`, moved out of
`planning.rs` because pure file I/O has no business sitting next to chunk
planning) and a new `discard_cancelled_download` sweep. The orchestrator
calls the sweep at every cancellation exit point, immediately after
`rollback_version_ini_transaction` so the version sentinel transients are
gone before the bulk deletion runs.
The sweep deliberately preserves a known set of names so a cancelled update
of an installed game does not destroy user-extracted files:
- `local/` committed install directory
- `.local.installing/`,
`.local.backup/` in-flight install transaction state, needed by
`install::recover_game_root` on next startup
- `.lanspread.json` per-game install intent log
- `.softlan_game_installed` external softlan installer marker
- `.sync/` external sync tooling
Everything else under the game root (the `.eti` archives, any nested payload
directories, partial chunk files) is removed, and the game root itself is
removed if it ends up empty. The set matches `should_ignore_game_child` in
`services/local_monitor.rs` minus the version.ini transients (which the
rollback step removes itself just before the discard runs).
Tradeoff worth knowing: this does NOT restore the pre-update `version.ini`
sentinel. `begin_version_ini_transaction` parks the existing sentinel as
`.version.ini.discarded`, and `rollback_version_ini_transaction` deletes
that file rather than renaming it back. The user-visible consequence is
that cancelling a mid-flight update of an installed game leaves the local
install playable but no longer flagged as "downloaded" — the documented
"settles as local-only" behaviour now recorded in
`crates/lanspread-peer/ARCHITECTURE.md` and `README.md`. Restoring the
sentinel on cancel was considered, but it would mean a cancelled update
keeps advertising the OLD version as Ready, which is worse than the
current outcome.
Two unrelated correctness issues that surfaced while threading cancellation
through the orchestrator are bundled in here because they belong to the
same user-visible "Cancel button works" story:
1. `download_from_peer` now races `connect_to_peer` against
`cancel_token.cancelled()` (`download/transport.rs:314-322`). Previously
a cancel arriving while QUIC was still in its connect handshake had to
wait for the connect timeout to elapse before the cleanup could run.
2. The download task in `handlers.rs` now calls
`refresh_local_game_for_ending_operation` on every terminal branch —
success-without-install, install-handoff-failure, and the `Err(e)` /
cancel branch — before `end_download_operation` clears
`active_downloads`. Without this, the UI's settled snapshot on the
cancel path could lag behind the actual file system state because the
active-operation snapshot was cleared while the discard was still
running, leaving a brief window where the card showed the pre-cancel
state.
What this does NOT fix: a crash (process kill, power loss) during a
download still leaves orphan `.eti` files because `recover_download_transients`
in `install/transaction.rs` only sweeps the version.ini transients. Closing
that gap would mean calling the same discard from startup recovery for any
game root whose install intent is None and whose `version.ini` is absent.
Tracked in `FINDINGS.md` as a follow-up.
Test Plan:
- `just clippy && just test` — 102 unit tests pass, no new warnings.
- Two new storage tests:
- `discard_cancelled_download_removes_peer_owned_payload` exercises the
fresh-download cancel (no `local/`, root sweeps clean).
- `discard_cancelled_download_preserves_local_install_state` exercises
the update cancel (`local/`, `.lanspread.json`, `.local.backup/`
survive; `version.ini` and `.eti` go away).
- Manual GUI smoke (operator): start a fresh download of a multi-archive
game from a peer, click Cancel from the detail modal while the progress
bar is between 5% and 95%. Expect the game root to be empty (or absent)
afterwards and no orphan `.eti` files. Repeat against an installed game
by clicking Update, then Cancel mid-download; expect `local/` contents
intact and the card to drop back to Play (or Update if the newer-version
peer is still around).
- `lanspread-peer-cli` has no `cancel` command yet, so the headless
`PEER_CLI_SCENARIOS.md` matrix does not cover this end-to-end. Adding a
CLI cancel command + scenario is the natural follow-up.
Refs:
|
||
|
|
01712f248b
|
feat(ui): show download progress and speed in the action button
Previously the action button only said "Downloading…" with no indication of
how far along the transfer was or how fast it was going. With multi-gigabyte
game payloads on a LAN this gave the user no signal whether the download had
stalled, was hitting the wire fast, or was about to finish.
Wire a sampled byte-level progress channel from the download pipeline up to
the action button:
- New `DownloadProgressTracker` in `crates/lanspread-peer/src/download/progress.rs`
holds the total expected bytes plus two atomic counters: `downloaded_bytes`
(deduplicated per `(relative_path, offset)` chunk key, used for the bar) and
`transferred_bytes` (raw cumulative, used for the speed sample). The dedup
prevents a retried chunk from double-counting toward completion while still
letting speed reflect actual wire activity including retry waste, which is
the more useful metric for "is the link doing anything right now?".
- `sample_download_progress` wraps the transfer future, emits an initial 0 B/s
snapshot, then samples on a 500 ms interval (`MissedTickBehavior::Skip` so a
stalled downloader does not generate a thundering herd of catch-up ticks)
and emits one final snapshot when the future resolves, so the UI sees the
closing state before `DownloadGameFilesFinished` arrives.
- New `PeerEvent::DownloadGameFilesProgress(DownloadProgress)` variant carries
`{ id, downloaded_bytes, total_bytes, bytes_per_second }`. The Tauri shell
forwards it as `game-download-progress`; the JSONL harness emits it as
`download-progress`.
- Orchestrator and retry paths refactored to thread a single shared
`Arc<DownloadProgressTracker>` through both the initial transfer and any
retry attempts. New `TransferContext`, `RetryContext`, and `ChunkPlanContext`
structs absorb the parameter-list growth that came with adding the tracker.
Frontend rendering honors the snapshot-is-authoritative decision from commit
`5df82aa` ("fix(ui): derive operation status from snapshots"):
- `Game.download_progress` is an ephemeral overlay carried alongside the card,
not a status field. `mergeGameUpdate` preserves it only while
`install_status === Downloading` and otherwise clears it on the next
snapshot, so the games-list snapshot remains the single authority for when
the bar should disappear.
- The `game-download-progress` listener writes ONLY `download_progress` — it
does not touch `install_status`, `status_message`, or `status_level`. This
preserves the rule that lifecycle events never mutate card status.
- No `game-download-finished` listener; snapshot reconciliation clears the
overlay automatically when status leaves Downloading.
- `ActionButton` renders a percentage fill behind the icon/label via a
`--download-progress` CSS custom property; the existing `.act-busy` spinner
is layered above the fill with `z-index: 1`. `act-downloading` widens the
button to avoid label jitter as the speed number changes (tabular-nums).
- `actionLabel` for the Downloading status now appends a formatted speed
("Downloading… 12.5 MB/s") via the new `formatBytesPerSecond` helper.
Test Plan:
- `just test` — Rust workspace tests including new progress tracker unit tests
(`tracker_counts_only_new_bytes_for_a_retried_chunk`,
`tracker_clamps_reported_bytes_to_total`).
- `just frontend-test` — Deno tests including
`download progress is preserved only while actively downloading` and
`downloading action label includes current speed`.
- `just clippy` — clean.
- Manual: download a multi-GB game from a peer and watch the action button
fill, speed update on the half-second, and reset cleanly on completion.
Refs: download progress visibility, snapshot-authoritative UI architecture
|
||
|
|
274b9d2fd4
|
test(peer-cli): add large exact-transfer coverage
Add deeper peer CLI coverage for file-transfer integrity and multi-peer chunking. The alpha fixture now carries a real renamed RAR archive larger than 100 MB for alienswarm, which gives the chunk planner enough work to split a single game archive across multiple peers. Expose completed chunk source details as a peer event and have the CLI print that event as JSONL. This keeps transfer behavior in lanspread-peer while the CLI remains a harness that reports what the peer runtime did. The Tauri shell logs the event at debug level so the shared PeerEvent enum stays exhaustive. Document the new S13/S14 scenarios and record the manual run evidence, including SHA-256 manifests and the per-peer byte split for the large archive. Test Plan: - just fmt - just test - just peer-cli-build - just clippy - just peer-cli-image - unrar t -idq crates/lanspread-peer-cli/fixtures/fixture-alpha/alienswarm/alienswarm.eti - Manual peer CLI: bravo -> deep-small-client bfbc2 download with matching SHA-256 manifests - Manual peer CLI: alpha -> deep-stage-b alienswarm download with matching SHA-256 manifests - Manual peer CLI: alpha + deep-stage-b -> deep-stage-c alienswarm download with chunk events from both peers and matching SHA-256 manifests Refs: PEER_CLI_SCENARIOS.md S13 S14 |
||
|
|
a251233653
|
refactor(peer): split download pipeline into modules
The download pipeline had grown into one large file that mixed sentinel transaction handling, peer planning, transport, retry, and top-level orchestration. Split it into a download/ module tree with one file per concern so future lifecycle changes can be reviewed at the right boundary. The public crate surface remains download::download_game_files. Helper types and functions are kept pub(super) or private so the refactor does not widen the API or encourage new callers to depend on internals. The version.ini transaction helpers stay local to version_ini.rs; the proposed fs_util extraction is intentionally left for the later atomic-index work, where a second caller exists. There is no intended runtime behavior change. Test Plan: - just fmt - just test - just clippy - just build Refs: none |