markdown formatting
This commit is contained in:
@@ -17,13 +17,14 @@ Useful flags:
|
||||
|
||||
- `--games-dir PATH` stores local archives and installs.
|
||||
- `--state-dir PATH` stores the generated peer identity.
|
||||
- `--fixture GAME_ID` seeds a tiny archive that the fixture unpacker can install.
|
||||
- `--fixture GAME_ID` seeds a tiny archive that the fixture unpacker can
|
||||
install.
|
||||
|
||||
## Fixture Game Directories
|
||||
|
||||
`fixtures/fixture-alpha`, `fixtures/fixture-bravo`, and
|
||||
`fixtures/fixture-charlie` are ready-to-use game directories for local CLI
|
||||
smoke tests. Point `--games-dir` at one of them to start a peer with several
|
||||
`fixtures/fixture-charlie` are ready-to-use game directories for local CLI smoke
|
||||
tests. Point `--games-dir` at one of them to start a peer with several
|
||||
catalog-backed fake games. Each game includes `version.ini` and a real RAR
|
||||
archive renamed to `.eti`; `fixture-alpha` and `fixture-bravo` share `ggoo`,
|
||||
while `fixture-bravo` and `fixture-charlie` share `cnc4`.
|
||||
@@ -44,6 +45,6 @@ echoed back on the result or error line.
|
||||
{"id":"q1","cmd":"shutdown"}
|
||||
```
|
||||
|
||||
The `status` result includes receiver-side `active_operations` and
|
||||
sender-side `active_outbound_transfers` counts by game ID, which the scenario
|
||||
runner uses to verify transfer lifecycle cleanup.
|
||||
The `status` result includes receiver-side `active_operations` and sender-side
|
||||
`active_outbound_transfers` counts by game ID, which the scenario runner uses to
|
||||
verify transfer lifecycle cleanup.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# lanspread-peer proposed protocol and architecture
|
||||
|
||||
This document proposes a tighter, more fault-tolerant protocol while keeping
|
||||
the current idea: mDNS discovery, QUIC transport, on-demand metadata, and
|
||||
chunked file transfers.
|
||||
This document proposes a tighter, more fault-tolerant protocol while keeping the
|
||||
current idea: mDNS discovery, QUIC transport, on-demand metadata, and chunked
|
||||
file transfers.
|
||||
|
||||
## Goals (unchanged)
|
||||
|
||||
@@ -26,14 +26,15 @@ chunked file transfers.
|
||||
|
||||
When a peer is discovered:
|
||||
|
||||
1. Connect and send `Hello { peer_id, proto_ver, listen_addr, library_rev,
|
||||
library_digest, features }`. `listen_addr` is mandatory; the QUIC source port
|
||||
is only a temporary transport port and must not be recorded as the peer's
|
||||
listener.
|
||||
2. Receive `HelloAck { peer_id, proto_ver, listen_addr, library_rev,
|
||||
library_digest, features }`.
|
||||
1. Connect and send
|
||||
`Hello { peer_id, proto_ver, listen_addr, library_rev, library_digest, features }`.
|
||||
`listen_addr` is mandatory; the QUIC source port is only a temporary
|
||||
transport port and must not be recorded as the peer's listener.
|
||||
2. Receive
|
||||
`HelloAck { peer_id, proto_ver, listen_addr, library_rev, library_digest, features }`.
|
||||
3. If the remote `peer_id` is already known but the address changed, update it.
|
||||
4. If protocol versions are incompatible, drop the peer (and keep mDNS watching).
|
||||
4. If protocol versions are incompatible, drop the peer (and keep mDNS
|
||||
watching).
|
||||
5. If library digests match, do nothing else.
|
||||
6. If digests differ:
|
||||
- If we have a known `library_rev` for that peer, request `LibraryDelta`.
|
||||
@@ -50,16 +51,16 @@ When a peer is discovered:
|
||||
|
||||
Call to Play is transient peer-session state rather than database state. The
|
||||
peer keeps a bounded event history, deduplicated by event ID. Every event and
|
||||
chat message remains in the snapshot for the full lifetime of an active call,
|
||||
so a peer joining mid-call receives the complete context. A creator's `Start`
|
||||
or `Cancel` makes the call terminal and read-only, but its complete roster and
|
||||
chat history remain in snapshots for 15 minutes so late joiners can see the
|
||||
outcome. After that display window the history compacts to the Start or Cancel
|
||||
tombstone for the rest of the peer session. Active and recently terminal calls
|
||||
are never partially trimmed. If genuinely active history reaches the bound,
|
||||
local publishes return an error to the caller instead of appearing to succeed;
|
||||
terminal histories and tombstones do not consume that active-history capacity.
|
||||
A call whose deadline elapses remains available for five minutes so the creator
|
||||
chat message remains in the snapshot for the full lifetime of an active call, so
|
||||
a peer joining mid-call receives the complete context. A creator's `Start` or
|
||||
`Cancel` makes the call terminal and read-only, but its complete roster and chat
|
||||
history remain in snapshots for 15 minutes so late joiners can see the outcome.
|
||||
After that display window the history compacts to the Start or Cancel tombstone
|
||||
for the rest of the peer session. Active and recently terminal calls are never
|
||||
partially trimmed. If genuinely active history reaches the bound, local
|
||||
publishes return an error to the caller instead of appearing to succeed;
|
||||
terminal histories and tombstones do not consume that active-history capacity. A
|
||||
call whose deadline elapses remains available for five minutes so the creator
|
||||
can start or extend it, then its unresolved history is evicted as a unit. A
|
||||
local action is applied to that history, sent to the UI, and broadcast to every
|
||||
currently known peer. An incoming live event is applied once and sent to the UI
|
||||
@@ -89,8 +90,8 @@ deterministically and derives deadlines and check-in phases from timestamps.
|
||||
Those phases compare creator-supplied wall-clock timestamps with each viewer's
|
||||
local wall clock, so LAN machines are assumed to be synchronized closely enough
|
||||
for human-scale minute countdowns; clock skew shifts the displayed boundary by
|
||||
the same amount.
|
||||
There is deliberately no compatibility path for older protocol versions.
|
||||
the same amount. There is deliberately no compatibility path for older protocol
|
||||
versions.
|
||||
|
||||
### 4) Shutdown
|
||||
|
||||
@@ -137,8 +138,8 @@ There is deliberately no compatibility path for older protocol versions.
|
||||
|
||||
1. Maintain a persistent on-disk index (per game):
|
||||
- `manifest_hash`, total size, file list (optional), and a fingerprint
|
||||
(root-level `version.ini` mtime, root-level `.eti` mtime/size, and
|
||||
`local/` directory presence).
|
||||
(root-level `version.ini` mtime, root-level `.eti` mtime/size, and `local/`
|
||||
directory presence).
|
||||
2. Use filesystem watchers to update only changed games.
|
||||
3. Keep a 300-second fallback scan to recover from missed events.
|
||||
|
||||
@@ -164,8 +165,8 @@ Downloaded and installed are independent predicates:
|
||||
`local/` are user-owned and are skipped by manifests, fingerprints, and file
|
||||
serving.
|
||||
- Install and update transactions unpack into staging, then overwrite the first
|
||||
discovered game-provided `account_name.txt` and `language.txt` files under
|
||||
the staged tree from launcher settings before promoting it to `local/`.
|
||||
discovered game-provided `account_name.txt` and `language.txt` files under the
|
||||
staged tree from launcher settings before promoting it to `local/`.
|
||||
|
||||
Reserved per-game paths:
|
||||
|
||||
@@ -239,8 +240,8 @@ Most scans become O(number of game dirs), with full recursion only when needed.
|
||||
1. Protocol updates in `lanspread-proto`:
|
||||
- Define `Hello`, `HelloAck`, `LibrarySummary`, `LibrarySnapshot`,
|
||||
`LibraryDelta`, and optional `Goodbye` messages.
|
||||
- Thread `peer_id`, `library_rev`, and `manifest_hash` through all
|
||||
library and manifest-bearing types.
|
||||
- Thread `peer_id`, `library_rev`, and `manifest_hash` through all library
|
||||
and manifest-bearing types.
|
||||
- Make `Hello` and `HelloAck` carry the sender's `listen_addr`,
|
||||
`library_rev`, and `library_digest` so both sides can record stable
|
||||
listener addresses and immediately select `LibraryDelta` vs
|
||||
@@ -248,11 +249,11 @@ Most scans become O(number of game dirs), with full recursion only when needed.
|
||||
2. Peer identity:
|
||||
- Persist a stable `peer_id` (UUID) in the peer config and inject it into
|
||||
`PeerInfo` and `PeerGameDB` at startup.
|
||||
- Track `peer_id -> SocketAddr` in the discovery table and update the
|
||||
address on any incoming handshake or mDNS refresh.
|
||||
- Track `peer_id -> SocketAddr` in the discovery table and update the address
|
||||
on any incoming handshake or mDNS refresh.
|
||||
3. Discovery handshake:
|
||||
- Publish `peer_id` and `library_rev` in mDNS TXT records to avoid
|
||||
immediate TCP/QUIC roundtrips when nothing changed.
|
||||
- Publish `peer_id` and `library_rev` in mDNS TXT records to avoid immediate
|
||||
TCP/QUIC roundtrips when nothing changed.
|
||||
- Add a lightweight handshake in `run_peer_discovery` that exchanges
|
||||
`Hello`/`HelloAck` before any library sync.
|
||||
- Ignore peers that do not advertise the current protocol version.
|
||||
@@ -261,8 +262,8 @@ Most scans become O(number of game dirs), with full recursion only when needed.
|
||||
successful index refresh completes.
|
||||
- Apply `LibraryDelta` when `library_rev` matches; reject stale or future
|
||||
revisions and request `LibrarySnapshot` instead.
|
||||
- Cache the last accepted `manifest_hash` per peer to short-circuit
|
||||
manifest requests when unchanged.
|
||||
- Cache the last accepted `manifest_hash` per peer to short-circuit manifest
|
||||
requests when unchanged.
|
||||
5. Local index + scan optimizations:
|
||||
- Use the cached `local_library/index.json` file in the configured state
|
||||
directory to store per-root fingerprints and computed manifests.
|
||||
|
||||
@@ -23,19 +23,20 @@ It is designed to run headless – other crates (most notably
|
||||
`Game` definitions, tracks the latest ETI version per title, and keeps the
|
||||
last seen list of `GameFileDescription` entries for each peer.
|
||||
|
||||
Internally the peer runtime owns four long-lived tasks that run for the
|
||||
lifetime of the process:
|
||||
Internally the peer runtime owns four long-lived tasks that run for the lifetime
|
||||
of the process:
|
||||
|
||||
1. **Server component** (`run_server_component`) – listens for QUIC connections,
|
||||
advertises via mDNS, and serves `Request::ListGames`, `Request::GetGame`,
|
||||
`Request::GetGameFileData`, `Request::GetGameFileChunk`, and
|
||||
`Request::StreamInstall` by reading from the local game directory.
|
||||
2. **Discovery loop** (`run_peer_discovery`) – uses the `lanspread-mdns`
|
||||
helper to discover other peers. The blocking mDNS work is executed on a
|
||||
dedicated thread via `tokio::task::spawn_blocking` so that the Tokio runtime
|
||||
remains responsive.
|
||||
3. **Ping service** (`run_ping_service`) – periodically issues QUIC ping requests
|
||||
to keep peer liveness up to date and prunes stale entries from `PeerGameDB`.
|
||||
2. **Discovery loop** (`run_peer_discovery`) – uses the `lanspread-mdns` helper
|
||||
to discover other peers. The blocking mDNS work is executed on a dedicated
|
||||
thread via `tokio::task::spawn_blocking` so that the Tokio runtime remains
|
||||
responsive.
|
||||
3. **Ping service** (`run_ping_service`) – periodically issues QUIC ping
|
||||
requests to keep peer liveness up to date and prunes stale entries from
|
||||
`PeerGameDB`.
|
||||
4. **Local game monitor** (`run_local_game_monitor`) – watches the configured
|
||||
game directory and each game root non-recursively, gates per-ID rescans while
|
||||
operations are active, emits local-library changes separately from active
|
||||
@@ -61,8 +62,9 @@ When the UI asks to download a game:
|
||||
|
||||
1. The UI first issues `PeerCommand::GetGame` for a new download, or
|
||||
`PeerCommand::FetchLatestFromPeers` for an update that must bypass local
|
||||
archives. The selected peers are queried via `request_game_details_from_peer`,
|
||||
and their file manifests are merged inside `PeerGameDB`.
|
||||
archives. The selected peers are queried via
|
||||
`request_game_details_from_peer`, and their file manifests are merged inside
|
||||
`PeerGameDB`.
|
||||
2. Once the UI receives `PeerEvent::GotGameFiles`, it forwards the selected file
|
||||
list back with `PeerCommand::DownloadGameFiles`.
|
||||
3. `download_game_files` starts a version-sentinel transaction, parks any old
|
||||
@@ -84,8 +86,8 @@ When the UI asks to download a game:
|
||||
sweep `.version.ini.tmp` and `.version.ini.discarded` without restoring the
|
||||
previous sentinel. Cancelled downloads also discard the peer-owned download
|
||||
payload while preserving `local/` and install transaction metadata.
|
||||
7. After a successful sentinel commit, `PeerEvent::DownloadGameFilesFinished`
|
||||
is emitted and the peer auto-runs the install transaction.
|
||||
7. After a successful sentinel commit, `PeerEvent::DownloadGameFilesFinished` is
|
||||
emitted and the peer auto-runs the install transaction.
|
||||
|
||||
### Streamed Install Pipeline
|
||||
|
||||
@@ -108,25 +110,24 @@ renamed to `local/`, post-promote intent or launch-settings cleanup failures are
|
||||
logged for startup recovery rather than reported as a failed install.
|
||||
|
||||
`PeerCommand::CancelDownload` cancels the tracked download token for an active
|
||||
transfer. The transfer task remains responsible for clearing `active_operations`,
|
||||
discarding partial payload files, and refreshing the settled local snapshot, so
|
||||
the UI continues to treat active-operation snapshots as the single source of
|
||||
truth for whether a download is still running.
|
||||
transfer. The transfer task remains responsible for clearing
|
||||
`active_operations`, discarding partial payload files, and refreshing the
|
||||
settled local snapshot, so the UI continues to treat active-operation snapshots
|
||||
as the single source of truth for whether a download is still running.
|
||||
|
||||
### Install Transactions
|
||||
|
||||
Install, update, uninstall, downloaded-file removal, and startup recovery live
|
||||
under `src/install/`.
|
||||
Install-side operation intent is stored atomically under the configured peer
|
||||
state directory, at `games/<game_id>/install_intent.json`. Game roots still use
|
||||
Lanspread-owned `.local.installing/` and `.local.backup/` directories marked by
|
||||
`.lanspread_owned`. Startup recovery combines the recorded intent with the
|
||||
observed filesystem state and only deletes reserved directories when intent or
|
||||
marker ownership proves they belong to Lanspread.
|
||||
Downloaded-file removal is deliberately separate from uninstall: it only accepts
|
||||
catalog IDs that are direct children of the configured game directory, refuses
|
||||
installed or in-flight roots, and deletes the whole game root only after finding
|
||||
a regular root-level `version.ini` sentinel.
|
||||
under `src/install/`. Install-side operation intent is stored atomically under
|
||||
the configured peer state directory, at `games/<game_id>/install_intent.json`.
|
||||
Game roots still use Lanspread-owned `.local.installing/` and `.local.backup/`
|
||||
directories marked by `.lanspread_owned`. Startup recovery combines the recorded
|
||||
intent with the observed filesystem state and only deletes reserved directories
|
||||
when intent or marker ownership proves they belong to Lanspread. Downloaded-file
|
||||
removal is deliberately separate from uninstall: it only accepts catalog IDs
|
||||
that are direct children of the configured game directory, refuses installed or
|
||||
in-flight roots, and deletes the whole game root only after finding a regular
|
||||
root-level `version.ini` sentinel.
|
||||
|
||||
Legacy launcher-owned files in game directories are migrated by a dedicated
|
||||
pre-start phase. Normal install, recovery, scan, and transfer paths use only the
|
||||
@@ -142,11 +143,10 @@ The Tauri application embeds this crate in
|
||||
game directory.
|
||||
- The Tauri commands (`request_games`, `install_game`, `update_game`,
|
||||
`remove_downloaded_game`, and `update_game_directory`) translate UI actions
|
||||
into `PeerCommand`s. In
|
||||
particular, `update_game_directory` validates the filesystem path before
|
||||
storing it, loads the bundled catalog on first use, kicks off the peer runtime
|
||||
on demand, and mirrors the installed/uninstalled state into the UI-facing
|
||||
database.
|
||||
into `PeerCommand`s. In particular, `update_game_directory` validates the
|
||||
filesystem path before storing it, loads the bundled catalog on first use,
|
||||
kicks off the peer runtime on demand, and mirrors the installed/uninstalled
|
||||
state into the UI-facing database.
|
||||
- A background task consumes `PeerEvent`s and fans them out to the front-end via
|
||||
Tauri publish/subscribe events (`games-list-updated`, `game-download-*`,
|
||||
`game-install-*`, `game-uninstall-*`, `peer-*`). The Tauri crate now only
|
||||
|
||||
Reference in New Issue
Block a user