main
15 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9bafd981d7
|
feat(install): write launcher language marker files
Some games include a language.txt marker in the unpacked local tree, similar in spirit to account_name.txt. Installs and updates now carry the launcher language alongside the account name so those game-provided marker files are rewritten before staged files are promoted into local/. The Tauri command boundary keeps the UI setting vocabulary as de/en, then maps it to the file vocabulary expected by games: german or english. Unknown values continue through the existing DEFAULT_LANGUAGE path, so the marker file falls back to english just like script launch arguments fall back to en. The transaction layer deliberately reuses the same first-match traversal helper for both marker files. The searches stay independent, so games may place account_name.txt and language.txt in different directories if their archive layout requires that. Test Plan: - just fmt - just test - just frontend-test - just clippy - deno task build - git diff --check Refs: none |
||
|
|
574acfca45
|
feat(install): stamp username into account_name.txt after install
Some games ship an `account_name.txt` file somewhere under the unpacked `local/` tree (location varies per game). After install or update, write the configured username into the first such file we find so the game launches under the user's account instead of whatever default the archive contains. The search is a deterministic alphabetical DFS rooted at the install staging dir (`.local.installing/`, which becomes `local/` on rename), stopping at the first regular-file match. Symlinks named `account_name.txt` are skipped (`is_file()` is false for symlinks on Linux), so a hostile archive can't redirect the write outside the game tree. If no `account_name.txt` exists anywhere in the install, the step is a no-op. If the write fails, the existing install rollback (cleanup of staging on fresh installs, restore from backup on updates) handles it — no partial state is left behind. The username flows from the Tauri layer, where it is already sanitized by `sanitize_username`, down through `PeerCommand` variants (`InstallGame`, `DownloadGameFiles`, `DownloadGameFilesWithOptions`) into `install`/`update`, which now take an `Option<&str> account_name`. For the "install game that isn't downloaded yet" path the username has to bridge the async gap between the `GetGame` / `FetchLatestFromPeers` request and the eventual `GotGameFiles` event; we park it in a per-game-id map on `LanSpreadState` and pop it when forwarding the download command. The map is also cleared defensively on `cancel_download`, `DownloadGameFilesFailed`, and `DownloadGameFilesAllPeersGone` so a stale entry can't bleed into a subsequent install with a different username. `PeerCommand` is the in-process command channel, not the wire protocol; no on-wire types changed, so the "one wire version" policy is preserved. The peer-cli harness keeps passing `account_name: None` since it tests peer interop, not user-facing settings. # Test Plan Unit tests in `crates/lanspread-peer/src/install/transaction.rs`: - `install_overwrites_first_account_name_file` — unpacker creates `a/account_name.txt` and `z/account_name.txt`; after install with username "Alice", `a/` is overwritten and `z/` is left untouched, pinning the sorted-DFS "first match wins" behavior. - `install_account_name_missing_file_is_noop` — install with a username but no `account_name.txt` anywhere in the archive succeeds and creates no spurious file. Manual GUI check: in Settings, set a username; install a game whose archive contains `account_name.txt`; open `local/` and confirm the file now holds the configured username. Repeat for the update flow (install, change username, click update). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
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 |
||
|
|
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
|
||
|
|
0f10108438
|
perf(peer): widen LAN bulk-transfer windows and buffers
Centralize the bulk-transfer sizing in config.rs and bump the values used
on both ends of a QUIC connection:
- CHUNK_SIZE: 32 MiB -> 128 MiB
- QUIC_CONNECTION_DATA_WINDOW: 64 MiB -> 256 MiB
- QUIC_STREAM_DATA_WINDOW: 32 MiB -> 128 MiB
- QUIC_MAX_SEND_BUFFER_SIZE: 32 MiB -> 128 MiB
- QUIC_INITIAL_CONGESTION_WINDOW: 1 MiB -> 4 MiB
- FILE_TRANSFER_BUFFER_SIZE: 64 KiB -> 1 MiB (new constant)
The previous 32 MiB stream window was already comfortably above the
bandwidth-delay product of a sub-millisecond LAN at 2.5 GbE. The further
bump is deliberately generous: the goal is to push flow control and
per-syscall overhead far enough out of the way that they cannot be the
suspect when isolating the remaining LAN download bottleneck (disk, NIC,
or s2n-quic platform offload on the sending host). Memory pressure from
the larger windows is not observable on a desktop client moving GB-sized
games.
stream_file_bytes previously read the local file in 64 KiB chunks. At
multi-Gbit/s send rates that produced many thousands of disk reads per
second; bumping to 1 MiB keeps the per-file syscall load modest with no
measurable latency cost on streamed bulk transfers. The buffer size lives
in config.rs as FILE_TRANSFER_BUFFER_SIZE so it stays adjustable from one
place.
Also add a started/MiB-per-second log line at info level when a file
finishes streaming. This matches the S37 measurement methodology already
used in the peer-cli harness and makes per-file send throughput visible in
normal operation.
The peer-cli extended-scenarios harness uses CHUNK_SIZE as the tolerance
bound for chunk-boundary variance in its assertions, so its constant is
bumped to match. The multi-chunk planning unit test is rewritten to
reference CHUNK_SIZE symbolically (CHUNK_SIZE * 3 + CHUNK_SIZE / 2)
instead of a hardcoded 120 MiB; the previous literal would silently
degrade into a single-chunk test at the new chunk size and stop
exercising the spread-across-peers code path.
Test Plan:
- just fmt
- just clippy
- just test
- python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S37 \
--build-image
- python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S37
Refs: local LAN download performance investigation on 2026-05-20.
Depends-on:
|
||
|
|
44e0629926
|
refactor(peer-cli): split download measurement event handlers
Extract the download-begin and chunk-finished measurement bookkeeping out of the main peer-cli event reducer. This keeps the S37 throughput reporting behavior unchanged while bringing the reducer back under the pedantic clippy line-count threshold. Test Plan: - just fmt - just clippy - just test Refs: S37 download throughput measurement harness. |
||
|
|
8a9f420a06
|
test(peer-cli): measure single-source download throughput
Add peer-cli accounting for download sessions so terminal download events report bytes, chunks, elapsed time, MiB/s, and Mbit/s. The extended scenario runner now has S37, a focused single-source download benchmark that creates a 2 GiB sparse bf1942 archive, downloads it from one peer with install disabled, and checks the destination archive size and reported byte count. This gives the QUIC performance work a repeatable measurement below the 5 GiB limit from the original request. The source file is sparse, so S37 is aimed at the app, QUIC, and destination-write path rather than raw source-disk reads; the existing correctness scenarios still cover normal game downloads. Baseline S37 before QUIC tuning: - 733.22 MiB/s - 6150.72 Mbit/s - 2.793s for 2.00 GiB plus version.ini - 65 reported chunks Test Plan: - just fmt - python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py S37 --build-image Refs: local LAN download performance investigation on 2026-05-20. |
||
|
|
62ceb063ac
|
feat(peer): remove downloaded game files safely
Downloaded but uninstalled games can still occupy significant disk space. Add a separate removal path for that state instead of overloading uninstall, which is reserved for deleting only `local/` installs. The peer runtime now exposes `RemoveDownloadedGame` with matching lifecycle and active-operation events. The filesystem delete is intentionally strict: the id must be a catalog game and a single path component, the target must be a direct child of the configured game directory, the root must not be a symlink, it must have a regular root-level `version.ini`, and it must not contain `local/`, `.local.installing/`, or `.local.backup/`. Only then do we recursively remove the game root. The Tauri bridge exposes this as `remove_downloaded_game`, the frontend shows a matching danger action only for downloaded-but-uninstalled games, and a confirmation dialog warns that re-downloading can take a long time. Test Plan: - git diff --check - just fmt - RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just test - RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just clippy - RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= just build Refs: user redesign nitpick about removing downloaded uninstalled games |
||
|
|
a8edcd7450
|
test(peer-cli): cover full docker scenario matrix
Merge the S18-S36 scenario ideas into the official peer-cli scenario matrix and add a Docker-backed runner that now exercises S1-S36 with concrete file proofs. The runner creates temporary fixtures under .lanspread-peer-cli, drives JSONL peer containers, checks transferred roots with diff and SHA-256 manifests, and covers startup, discovery, transfer, failure, mutation, concurrency, mesh, lifecycle, and catalog edge cases. The scenarios exposed a few harness/runtime boundary gaps that would otherwise make the contract ambiguous. The peer CLI now rejects self-connects, rejects commands for game IDs outside the receiver catalog, filters unknown remote games from its command/event surface, and reports duplicate active same-game commands as operation-in-progress errors. The peer core also refuses non-catalog download commands before transfer, and PeerGameDB has a unit check that address changes preserve identity and library state. S12 and S28 remain unit-level invariants because the CLI cannot stably race raw serve-gate requests or rebind a live listener without restart. The runner treats those scenarios as covered by just test and checks the expected unit test names appear in the output. Test Plan: - just fmt - python3 -m py_compile crates/lanspread-peer-cli/scripts/run_extended_scenarios.py - RUSTC_WRAPPER= just test - RUSTC_WRAPPER= just clippy - RUSTC_WRAPPER= just peer-cli-build - just peer-cli-image - python3 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py - git diff --check Refs: PEER_CLI_SCENARIOS.md S1-S36 |
||
|
|
41e9a0efc1
|
refactor(peer): split local library and operation UI events
Replace the `a9f9845` local-update dedup cache with explicit peer event semantics. Local scans now emit `LocalLibraryChanged` when the library changes, while operation mutations emit `ActiveOperationsChanged` from the mutation path. Tauri keeps joining those facts into the existing `games-list-updated` payload, so the frontend contract stays stable. This removes the cache/invalidation coupling between scan emission and operation state. The remaining forced local snapshot is explicit: accepted game directory changes can refresh the UI for an equivalent new path without sending a peer library delta. Operation guard cleanup and liveness cancellation now publish the same active operation snapshot as normal command-handler transitions. The peer CLI JSONL events follow the same split with `local-library-changed` and `active-operations-changed`. Test Plan: - `just fmt` - `CARGO_BUILD_RUSTC_WRAPPER= just test` - `CARGO_BUILD_RUSTC_WRAPPER= just clippy` - `git diff --check` Refs: CLEAN_CODE_PLAN_1.md |
||
|
|
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 |
||
|
|
5d58791192
|
fix(peer-cli): fail missing downloads from peer event
The peer core already emits NoPeersHaveGame when a requested game cannot be
served by any known peer. The JSONL harness still waited for the generic file
detail timeout before returning the download command error, which made the
manual scenario slower and less precise.
Correlate the existing no-peers event with the pending CLI download command so
the harness returns a deterministic error immediately. This is harness
bookkeeping only; game availability and peer behavior remain owned by
lanspread-peer.
Test Plan:
- just fmt
- just test
- just clippy
- just peer-cli-build
- just peer-cli-image
- just peer-cli-alpha, just peer-cli-bravo, just peer-cli-charlie
- In charlie, send {"cmd":"download","game_id":"not-a-game"}; observe
no-peers-have-game followed by error "no peers have game not-a-game"
Refs: PEER_CLI_SCENARIOS.md
|
||
|
|
ed007f7844
|
test: add peer CLI game directory fixtures
Add three reusable peer CLI game directory fixtures for local smoke tests. Each fixture is a complete games root that can be passed directly to --games-dir, with catalog-backed game IDs, version.ini sentinels, and real RAR archives renamed to .eti. The fixtures intentionally overlap in two places so multi-peer tests can cover shared availability. Alpha and bravo both contain ggoo, while bravo and charlie both contain cnc4. The archives contain generated random payload files rather than meaningful game data; this keeps the fixtures fake while still exercising RAR-backed ETI handling. The tradeoff is committing roughly 34 MiB of binary fixture data. That is intentional here because the fixtures need real archives for CLI tests instead of synthetic text placeholders. Test Plan: - Ran git diff --check. - Ran unrar t -idq over every .eti file in the fixture tree. Refs: none |
||
|
|
754afd5621
|
refactor(peer): drop --no-mdns toggle, mDNS is always on
The peer runtime previously accepted an `enable_mdns: bool` flag, plumbed
through `PeerStartOptions`, `spawn_peer_runtime`, `run_peer`, `Ctx`, and
`PeerCtx`. The lanspread-peer-cli harness exposed the toggle as
`--no-mdns` so test scenarios could fall back to explicit `connect`
commands when mDNS could not be relied on, in particular when multiple
peers ran inside `--network host` containers and could not advertise
independently.
That host-networking workaround no longer exists: the previous commit
moves harness containers onto a macvlan network, where each peer is a
real LAN device and mDNS just works between them. There is no scenario
left in the codebase where disabling mDNS is desirable. Per the project's
protocol policy in CLAUDE.md ("there is only one wire version, no
compatibility shims, no fallback paths"), an opt-out path with no current
caller is exactly the kind of dead code we should not carry.
Remove the flag and every plumbing point that exists only to support it:
- `PeerStartOptions::enable_mdns` and the custom `Default` impl that set
it to `true`; the struct now derives `Default` and just carries
`state_dir`.
- The `enable_mdns` parameter on `start_peer_with_options`,
`spawn_peer_runtime`, `run_peer`, and `Ctx::new`.
- The `enable_mdns` fields on `Ctx` and `PeerCtx` and the propagation
through `to_peer_ctx`.
- The `if ctx.enable_mdns` guard in `spawn_startup_services`;
`spawn_peer_discovery_service` is now always spawned.
- The `if ctx.enable_mdns { ... } else { ... }` branch in
`run_server_component`: the mDNS advertiser and event monitor are now
unconditionally started, and the no-mDNS-fallback log line that read
"mDNS disabled; direct peer address is ..." is gone. The
`direct_connect_addr` helper is kept because the mDNS-on branch still
uses it as a fallback when `local_peer_addr` has not yet been
populated.
- The internal test helpers in `handlers.rs`, `services/local_monitor.rs`,
and `services/stream.rs` that passed `true` as the trailing
`enable_mdns` arg to `Ctx::new`.
- In `lanspread-peer-cli`: the `--no-mdns` arg parsing, the
`Args::enable_mdns` field, the `mdns` key on the `cli-started` event
payload, and the `--no-mdns` mention in the help text and the crate
README.
The `Args::name` field is wired to the harness identity but is otherwise
untouched. The macvlan network created by `just peer-cli-net` is the
runtime prerequisite for this change to be observable across containers;
on a single workstation, two harness binaries on `127.0.0.1` discover
each other through mDNS on the loopback interface as before.
Test Plan:
- `just fmt`
- `just clippy`
- `just test`
- `just peer-cli-build`
- Two peers on macvlan: `just peer-cli-run alpha` and
`just peer-cli-run beta`; check that each emits `peer-discovered` and
`peer-connected` events without an explicit `connect` JSONL command.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
dc9e13e6a1
|
feat(peer-cli): add JSONL peer test harness
Agents need a way to exercise multiple peers without launching the Tauri GUI.
Add `lanspread-peer-cli` as a workspace crate that starts the core peer runtime,
reads JSON commands from stdin, and writes result, event, and error records as
JSONL on stdout.
The harness supports status, peer listing, game listing, direct connect,
set-game-dir, download, install, uninstall, wait-peers, and shutdown commands.
It can seed tiny fixture archives that use a fixture unpacker, or delegate real
archives to an external `unrar` program when one is supplied.
Add a Dockerfile, `.dockerignore`, and `just` recipes for building the binary,
building the image, and running named harness containers with state and games
mounted under `target/peer-cli/`. The documentation now lists the crate and the
new test harness commands in the project map, with a crate-local README for the
JSONL protocol.
This commit depends on the non-GUI peer hooks introduced in the previous commit:
startup options, local-ready events, direct connects, snapshots, and explicit
post-download install policy. It does not add old-peer compatibility paths.
Test Plan:
- `git diff --check`
- `just fmt`
- `just clippy`
- `just test`
- `just peer-cli-build`
- Not run: `just peer-cli-image` requires a Docker daemon and base image access.
Depends-on:
|