A native WM_DELETE_WINDOW reproduction completed the frontend drain and then failed with "window.destroy not allowed". All three windows lacked the destroy permission used by Tauri's onCloseRequested wrapper. Removing the listener had previously let a second click bypass that denied IPC; retaining the listener made every click fail. The frontend was not stuck waiting on its own listener. Grant window destruction to the three configured application windows. Keep the existing frontend drains, early peer cancellation, and final runtime/task joins. Test the application's actual generated RuntimeAuthority, including expanded plugin defaults, instead of assuming a mocked successful destroy proves access. Explicitly include generated capabilities and ACL manifests in the context constructor's rustc dependencies. The configured compiler cache reused the old library after a permission-only edit because Tauri's macro reads these files without recording compiler dependencies. A remove/restore native probe now rebuilds with the correct permission in both directions. Document the complete ownership chain and failure evidence. Add a PID-checked X11 WM_DELETE_WINDOW helper for repeatable close-button probes without killing the process or bypassing the frontend boundary. Test Plan: - Native baseline: destroy denied and process remained alive after one request. - Resolved-ACL regression failed before the permission fix and passes afterward. - Native main/companion close probes: one request per window; normal process exit. - Permission-only rebuild with compiler cache: normal exit in 197 ms. - Production executable: normal exit in 43 ms; QUIC port released and rebound. - just test: 797 passed on unchanged rerun after one initial subprocess fixture startup-marker timeout, before that test exercised cancellation. - just frontend-test: 99 passed. - just fmt, just clippy, just build, and git diff --cached --check: passed. - Native helper compiled with -Wall -Wextra -Werror. - Native probes ran on Linux X11/XWayland; Windows/macOS were not measured.
lanspread
Peer-to-peer game library sharing for LAN parties. Peers discover each other on the local network via mDNS, exchange library metadata over QUIC, and let users browse and download games from each other. Ships as a Tauri desktop app.
Peer identity and protocol status
Each desktop installation creates and reuses one Ed25519 identity in its Tauri
app-data directory. Its PeerId is lowercase unpadded RFC 4648 base32 of BLAKE3
over the certificate's complete canonical DER SubjectPublicKeyInfo; certificates
and private keys are never distributed from the repository. Outbound QUIC
connections authenticate the responder against the exact expected PeerId. Wire
protocol 8 (lanspread/8) is the only supported protocol. Peers exchange
responder-owned, revisioned snapshots over identity-pinned endpoints; file and
streamed-install requests name the catalog's exact ContentId. There are no
legacy wire fallbacks or repository-shared identity keys.
First-time setup
Install Rust, Deno, and just first, then bootstrap the project:
just setup
That installs the Tauri CLI and the locked frontend dependencies. All routine
repository work should go through just; the recipes carry the catalog mode and
Tauri resource configuration needed to keep fixture authority out of production
builds.
Daily development
Run the desktop app with the already-generated production catalog:
just run
Plain just (or the j shell alias) does the same. It checks the existing
catalog and starts the launcher; no source package directory or environment
variables are needed. Select the folder containing your games in the launcher.
To test directly with a local game folder and a separate catalog:
just run-local /srv/lanspread/test-games
This selects that game folder and generates or reuses test authority under
crates/lanspread-tauri-deno-ts/src-tauri/local-catalog/, with its own cache in
.lanspread/catalog-cache/local.json. Missing catalog games and differing
version.ini values are allowed; directory names must still match known game
IDs. The console identifies this as local test mode. Its opt-in is checked at
build time and cannot enable local authority in a production build. The local
launcher uses separate test settings and a window title marked LOCAL TEST.
Build a launcher from the already-published production catalog without creating an installer:
just build
Generate the production catalog from a package tree when necessary, then build the same no-bundle launcher:
just build-production /srv/lanspread/packages
To regenerate or reuse production authority from a package tree and then run:
just run-production /srv/lanspread/packages
The fixture-only paths remain explicit:
just run-fixture
just build-fixture
Run the standard checks before committing:
just fmt
just clippy
just test
just frontend-test
See launcher shutdown ownership for the cancellation/drain sequence and a native close-button regression probe.
just build always uses the production resource map and production profile; use
just build-fixture when a fixture build is intended. Tauri's build script
rejects fixture and local resources unless their explicit development opt-in is
present, and the production profile rejects both modes. Before Tauri copies
catalog resources, the build script clears the generated game.db and
manifests/ in the active Cargo profile directory. This keeps
fixture/production switches and reduced production catalogs free of stale
manifests. Cargo also watches these generated copies, so deleting them causes
the next build to recreate them even when the source catalog is unchanged.
Catalog checksums and production manifests
Lanspread does not trust file descriptions received from peers. The release catalog contains the expected bytes for each included game version. The catalog publisher derives that authority from the canonical game packages and writes:
- one
<game_id>.jsonmanifest containing every ordinary file's size, full-file BLAKE3, and BLAKE3 for each 128 MiB transfer chunk; - BLAKE3 and size for every file obtained by extracting the root-level
.etiarchives, which authorizes Stream Install output; and catalog-content-index-v1.jsonl, which binds every catalog game/version to its complete manifestContentIdand Stream Install capability.
The ContentId is a digest of the canonical manifest transcript, not merely a
hash of one archive. Never calculate or edit these values by hand.
Prepare the package tree
Place the canonical packages under one directory, with each direct child named
exactly like the game_id in the production game.db:
/srv/lanspread/packages/
├── alienswarm/
│ ├── version.ini
│ ├── alienswarm.eti
│ └── ...
├── bf1942/
│ ├── version.ini
│ ├── bf1942.eti
│ └── ...
└── ...
Each version.ini must match that game's game_version in game.db. Package
entries must be regular files/directories with canonical portable names; links,
reparse points, special files, aliases, unsafe archive output, or changing bytes
make generation fail closed.
The default publisher uses the checked-in Linux unrar sidecar. Select another
trusted executable when generating on a different supported host:
LANSPREAD_UNRAR=/absolute/path/to/unrar \
just catalog-generate-production /srv/lanspread/packages
Generate and verify checksums
Generate the complete production catalog:
just catalog-generate-production /srv/lanspread/packages
The default remains strict: every game in the checked-in source game.db must
have a package directory. To build a reduced catalog from only the package
directories currently available, pass the explicit optional mode:
just catalog-generate-production /srv/lanspread/packages allow-missing-games
allow-missing-games omits absent package directories and reports their count;
it still rejects a package whose version.ini differs from the source catalog.
Version mismatches have their own independent opt-in:
just catalog-generate-production /srv/lanspread/packages allow-version-mismatch
That mode reports mismatched game IDs and uses each package's actual
version.ini value in the generated database copy, while still requiring every
package directory. Combine both options for an older, incomplete package corpus:
just catalog-generate-production /srv/lanspread/packages \
allow-missing-games allow-version-mismatch
Invalid directories and unreadable or non-UTF-8 version files always fail. The
generator copies and filters game.db to exactly the included games, then
generates and validates their complete manifest/index set. At least one matching
source-catalog game must be present. The checked-in full database is never
modified; the coherent generated output is installed under
crates/lanspread-tauri-deno-ts/src-tauri/production-catalog/.
The mode can also be used while running or building real data:
just run-production /srv/lanspread/packages \
allow-missing-games allow-version-mismatch
just build-production /srv/lanspread/packages \
allow-missing-games allow-version-mismatch
The recipe keeps a local metadata-only stamp in
.lanspread/catalog-cache/production.json. It compares the package tree's
paths, entry types, sizes, and nanosecond mtimes, plus the source and generated
catalog databases, generation mode, and unrar metadata. If those values and
the published output still match, the expensive package hashing and extraction
step is skipped. This is only a performance cache: catalog-check-production
and every production build still validate the catalog authority. Force a refresh
when needed:
LANSPREAD_CATALOG_FORCE=1 just catalog-generate-production /srv/lanspread/packages
The publisher first checks every included package's version.ini against the
generated game.db, then independently reads and hashes each package twice
before publication. It creates the complete database, manifest, and index set in
staging and validates it with the application loader before atomically replacing
the previous production catalog directory.
Verify the published database/index/manifest set without rereading the package corpus:
just catalog-check-production
This is a packaging integrity check, not a substitute for retaining the
canonical packages and the successful generation log as provenance. The output
prints the game_id, game_version, and content_id for every checked game.
Regenerate one changed game
After a complete manifest set exists, update one game without rehashing every package:
just catalog-generate-production-game /srv/lanspread/packages alienswarm
just catalog-check-production
Incremental generation refuses to bootstrap a catalog. Under its durable publication marker it revalidates every unselected manifest/index identity, rebuilds the complete generated index, and then publishes the selected game. Use the main generation command for a new catalog or any intentionally broad catalog change.
Build a production bundle
Only after the production check succeeds, create the installer/bundle:
just bundle
just bundle repeats the production catalog gate before Tauri packaging. It
never falls back to fixture manifests. This checkout does not include generated
production authority, so the command intentionally fails until an external
package corpus has been processed into
crates/lanspread-tauri-deno-ts/src-tauri/production-catalog/.
Fixture catalogs and peer-CLI acceptance
The committed fixture profiles are test authority only:
just fixture-catalog-check # explicit fixture GUI/CLI profile
just fixture-catalogs-check # default + solid + multi + unknown profiles
just fixture-catalogs # explicitly regenerate all committed profiles
The solid, multi-archive, and unknown-game profiles intentionally describe
different package bytes, so the peer-CLI matrix checks all of them before
building its Docker image. just run-fixture uses the default fixture profile.
Build or run the JSONL peer harness:
just peer-cli-build
just peer-cli-image
just peer-cli-tests S1 S2 S3
just peer-cli-run alpha
Run the entire S1-S49 acceptance matrix by omitting scenario arguments:
just peer-cli-tests
For an interactive three-peer session, start these in separate terminals:
just peer-cli-alpha
just peer-cli-bravo
just peer-cli-charlie
Important just commands
| Command | Purpose |
|---|---|
just setup |
Install the Tauri CLI and frontend dependencies. |
just / just run |
Check the existing production catalog and run the GUI. |
just run-local GAMES_DIR |
Generate/reuse separate local test authority and run with that folder. |
just run-production PACKAGES_DIR [MODE...] |
Generate/reuse production authority and run the GUI. |
just run-fixture |
Check the default fixture catalog and run the GUI. |
just build |
Validate production authority and build without bundling. |
just build-production PACKAGES_DIR [MODE] |
Generate/reuse production authority and build without bundling. |
just build-fixture |
Check the default fixture catalog and build without bundling. |
just bundle |
Check production authority and build production bundles. |
just catalog-generate-production DIR [MODE...] |
Publish packages with explicit missing/version mismatch opt-ins. |
just catalog-generate-production-game DIR GAME_ID |
Safely regenerate one game in an already complete catalog. |
just catalog-check-production |
Validate all production database/index/manifest artifacts. |
just fixture-catalog-check |
Validate the default development fixture catalog. |
just fixture-catalogs-check |
Validate every peer-CLI fixture profile. |
just fixture-catalogs |
Regenerate all committed fixture profiles from their packages. |
just fmt |
Format Rust, TOML, Markdown, and the Justfile. |
just clippy |
Lint every Rust workspace target with warnings denied. |
just test |
Run every Rust workspace test target. |
just frontend-test |
Run the frontend reducer/lifecycle tests. |
just peer-cli-build |
Build the JSONL peer harness. |
just peer-cli-image |
Check fixture authority and build the harness Docker image. |
just peer-cli-tests [SCENARIO ...] |
Run selected scenarios, or the complete matrix when no IDs are supplied. |
just peer-cli-run NAME |
Start one persistent-state interactive harness peer. |