Commit Graph

280 Commits

Author SHA1 Message Date
ddidderr e7bb14dc7c deps: deno update --latest 2026-05-15 11:14:41 +02:00
ddidderr 5ec47d6c47 deps: upgrade Rust dependencies 2026-05-15 11:10:53 +02:00
ddidderr 2e3d6a9abb update CLAUDE.md, README.md and justfile 2026-05-15 11:07:26 +02:00
ddidderr 2bbd2ac869 refactor(peer): adopt structured concurrency with supervised shutdown
Replace the detached tokio::spawn pattern in the peer runtime with a
supervised model built on tokio_util's CancellationToken and TaskTracker.
Long-lived services and child tasks now have an explicit parent, a
cancellation path, and a join point. Tauri can request a clean shutdown
on app exit instead of leaking work into process termination.

Background
~~~~~~~~~~

start_peer() previously returned only a command sender. The four startup
services (QUIC server, mDNS discovery, peer liveness, local library
monitor) and their child tasks (ping workers, handshake jobs, download
workers, announcement fan-outs, connection/stream handlers) were spawned
with raw tokio::spawn and detached. Closing the command channel sent
Goodbye notifications but did not stop those services. The mDNS blocking
worker had no cancellation path at all. Active downloads were stored as
JoinHandle<()> and force-aborted, which could interrupt file writes
mid-chunk.

Supervisor
~~~~~~~~~~

The runtime now owns a CancellationToken and a TaskTracker, threaded
through Ctx and PeerCtx. Each long-lived service is spawned through a
small supervisor (spawn_supervised_service) that wraps the service in
catch_unwind and enforces an explicit SupervisionPolicy:

  QuicServer:    Required     (fatal; cancels the runtime if it dies)
  Discovery:     Restart(5s)  (matches the prior self-restart loop)
  Liveness:      Restart(5s)
  LocalMonitor:  BestEffort   (logs and exits, no restart)

A Required failure emits a new RuntimeFailed { component, error } event
to the UI and cancels the runtime; the command loop and goodbye
notifications still run to completion. The Tauri layer forwards the
event as "peer-runtime-failed" so a future UI can surface it.

mDNS cancellation
~~~~~~~~~~~~~~~~~

MdnsBrowser previously blocked on receiver.recv() forever. It now
exposes next_service_timeout(Duration) returning an MdnsServicePoll
enum (Service/Timeout/Closed) via recv_timeout(). The discovery worker
polls at 250ms and checks the shutdown flag between ticks, so
cancellation reaches the blocking thread within one poll interval
instead of waiting for the next mDNS event.

Downloads
~~~~~~~~~

active_downloads is now HashMap<String, CancellationToken>. Each
download gets a child token of the runtime shutdown, checked at chunk
and peer-attempt boundaries (never inside file writes). When all peers
with a game disappear, liveness cancels the token and emits
DownloadGameFilesAllPeersGone; the download exits Ok(()) without
emitting a duplicate Failed event.

DownloadStateGuard (context.rs) is held inside the download task and
clears downloading_games + active_downloads on Drop, covering the happy
path, error returns, cancellation, and task abort. Drop falls back to
spawning the cleanup if write-lock contention prevents try_write.

Public API and Tauri integration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

start_peer() now returns PeerRuntimeHandle exposing:

  fn sender(&self) -> UnboundedSender<PeerCommand>
  fn shutdown(&self)
  async fn wait_stopped(&mut self)

The Tauri layer stores the handle in managed state and switches its
main loop from .run(ctx) to .build(ctx).run(|h, e| ...). On
RunEvent::Exit it calls handle.shutdown() and blocks up to 2s on
wait_stopped(), giving services time to cancel and Goodbye packets time
to flush over a healthy LAN while staying short enough not to delay
process exit noticeably on a dead network.

The command loop distinguishes graceful shutdown from unexpected
channel closure: if recv() returns None and shutdown.is_cancelled() is
set, the loop returns Ok(()) silently. Only an unexpected close (no
cancellation observed) still emits RuntimeFailed. This avoids a
spurious failure event on every normal app close.

User-visible behavior changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

- Closing the app no longer leaks services into process termination;
  Goodbye notifications are reliably attempted before exit.
- Downloads cancel cleanly (between chunks) instead of force-aborting
  mid-write.
- A new "peer-runtime-failed" Tauri event fires when a Required service
  cannot recover. No frontend handler exists yet — that is a follow-up.

Tradeoffs
~~~~~~~~~

- Workspace tokio-util now requires the "rt" feature for TaskTracker.
- The mDNS worker still runs in spawn_blocking and may stay parked
  briefly between 250ms polls — acceptable for a desktop app.
- The 2s shutdown timeout on app exit is a deliberate compromise.

Tests
~~~~~

New unit tests:
  - DownloadStateGuard clears tracking on completion, cancellation, and
    parent-task abort (context.rs).
  - Required failure cancels the runtime and emits RuntimeFailed
    (startup.rs).
  - Restart policy restarts until shutdown is requested (startup.rs).
  - PeerRuntimeHandle.shutdown() observable via wait_stopped()
    (startup.rs).
  - Peers-gone cancellation emits only PeersGone, no duplicate Failed
    (services/liveness.rs).

Test plan
~~~~~~~~~

  cargo test --workspace
  cargo clippy --workspace --all-targets

Manual smoke test on two peers on the same LAN:
  1. Start a download, verify chunks transfer.
  2. Close the receiving app mid-download — verify the sending peer
     logs a Goodbye, not a connection-reset error.
  3. Stop the sending peer mid-download — verify the receiver emits
     DownloadGameFilesAllPeersGone, not Failed.

Follow-ups
~~~~~~~~~~

- Frontend handler for "peer-runtime-failed".
- Consider exposing the runtime handle's stopped watch to the frontend
  for a reconnecting indicator on Required failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:53:51 +02:00
ddidderr 84665cacf0 deno update --latest 2026-05-08 12:13:41 +02:00
ddidderr 87d00e7df6 refactor(peer): make startup directory-driven
Peer startup used to bootstrap itself by spawning the runtime and immediately
sending a SetGameDir command back through its own control channel. The Tauri
integration then polled shared state until a directory appeared and waited two
seconds before asking peers for games. That made startup ordering implicit and
left a race-prone sleep in the UI bridge.

Install the initial game directory directly into the peer context instead. The
runtime now attempts the initial local-library scan before starting discovery,
then launches the server, discovery, liveness, and local monitor services from
that initialized context. Later directory changes still use SetGameDir, so the
existing UI command surface stays intact.

Use PathBuf and Path references across peer filesystem boundaries so directory
state is represented as a path rather than an optional string. The Tauri layer
now validates a selected game directory before storing it, loads the bundled
catalog on first use, and starts or updates the peer runtime from one helper.
Peer event fan-out is split into named handlers so the Tauri setup closure only
wires state and starts the event loop.

Shutdown goodbye notifications are still best-effort, but they are now awaited
with a short timeout instead of being spawned and forgotten. The tradeoff is a
small bounded wait during peer runtime shutdown in exchange for clearer task
ownership.

Test Plan:
- cargo test -p lanspread-peer
- cargo clippy
- cargo clippy --benches
- cargo clippy --tests
- cargo +nightly fmt
- git diff --check

Refs: none
2026-05-02 17:09:00 +02:00
ddidderr 5480d1bdd4 refactor: extract bundled game database loading
Move the required game.db resource resolution and ETI catalog loading out of
Tauri setup into small helpers. The setup closure now describes the startup
flow instead of carrying resource-resolution and conversion details inline.

This keeps the existing fail-fast behavior for a missing or unreadable bundled
catalog, while giving the required resource path and in-memory GameDB conversion
clear names. There is no intended user-visible behavior change.

Test Plan:
- cargo clippy
- cargo clippy --benches
- cargo clippy --tests
- cargo +nightly fmt

Refs: none
2026-05-02 16:34:19 +02:00
ddidderr 8f35a197a9 refactor(peer): extract peer startup task spawning
The peer runtime used to spawn each long-running service inline inside
run_peer. That made the startup path harder to scan because service names,
clone setup, and task error handling were interleaved with the command loop.

Move the task wrappers into a startup module and leave run_peer as the
lifecycle overview: create shared context, start services, handle commands,
then send shutdown goodbyes. The spawned services and their error handling are
unchanged; only the ownership plumbing moved into named helpers.

Test Plan:
- cargo clippy
- cargo clippy --benches
- cargo clippy --tests
- cargo +nightly fmt

Refs: none
2026-05-02 16:02:37 +02:00
ddidderr 3fb516af2b refactor(tauri): use default managed runtime state
LanSpreadState now owns its empty initialization through Default. This keeps
the root runtime state construction in one place instead of building each
Arc<RwLock<_>> value inline before registering it with Tauri.

The setup hook now retrieves peer_game_db from the managed state and clones the
Arc before spawning async peer initialization. That preserves the existing
lifetime boundary while removing the separate outer peer_game_db binding.

There is no user-visible behavior change. The peer database, game list,
download tracking, games folder, and peer control channel still start empty and
are populated through the same setup and command paths.

Test Plan:
- cargo clippy
- cargo clippy --benches
- cargo clippy --tests
- cargo +nightly fmt

Refs: none
2026-05-02 15:48:11 +02:00
ddidderr 047cb72905 deno update --latest 2026-05-02 15:33:14 +02:00
ddidderr b4585b663a ChatGPT Codex 5.5 xhigh refactored even more 2026-05-02 15:31:37 +02:00
ddidderr 86d0f93ede asd 2026-02-26 20:12:25 +01:00
ddidderr 4318927060 deps: deno update --latest 2026-01-14 08:57:30 +01:00
ddidderr b60dcef471 ChatGPT Codex 5.2 xhigh refactored > 45min 2026-01-13 18:59:12 +01:00
ddidderr f76d59265c Plan to cleanup everything by Codex 5.2 (xhigh) 2026-01-03 22:21:29 +01:00
ddidderr 0ba4ff3acb deno update, cargo update 2026-01-03 22:20:59 +01:00
ddidderr feee9229fa claude: updated CLAUDE.md 2025-12-07 08:18:25 +01:00
ddidderr 05411ce8b8 CLAUDE.md: --workspace instead of --all 2025-11-30 10:50:53 +01:00
ddidderr 53c7fe10ba refactor (Opus 4.5): modularize and split 2025-11-28 21:10:42 +01:00
ddidderr df01131f8d refactor: Centralize local game database updates and announcements, and add retry logic for requesting games from peers. 2025-11-18 21:42:47 +01:00
ddidderr f9923bd61e feat: Implement length-delimited framing for QUIC stream communication using tokio-util and futures. 2025-11-18 20:39:38 +01:00
ddidderr 68594740a5 Tauri app identifier 2025-11-18 19:52:04 +01:00
ddidderr ddbdb33cd6 Merge remote-tracking branch 'gh/p2p' into p2p 2025-11-18 19:49:21 +01:00
ddidderr 84eeebb633 feat: Exclude .sync, .softlan_first_start_done, and local directories from root size calculation. 2025-11-18 19:47:41 +01:00
ddidderr 04fe2bfcbe clippy 2025-11-18 19:26:05 +01:00
ddidderr e2f0dfa792 feat: Enable peers to announce and synchronize local game libraries. 2025-11-18 19:08:29 +01:00
ddidderr 25bac734e2 unsafe 2025-11-14 11:32:56 +01:00
ddidderr 293ede96ed ugly 2025-11-14 11:28:55 +01:00
ddidderr f88fa5794c skip descending into local 2025-11-14 11:08:37 +01:00
ddidderr 75be55d255 remember game dir 2025-11-14 10:44:02 +01:00
ddidderr 86cf3e87f7 skip .sync and .softlan_game_installed 2025-11-14 10:34:45 +01:00
ddidderr 67f99f4a0a dont do heavy size calc 2025-11-14 10:28:55 +01:00
ddidderr ec2a2ef44d windows paths 2025-11-14 10:11:11 +01:00
ddidderr e435be4d94 unsafe for the win 2025-11-14 09:39:59 +01:00
ddidderr cc42bc6f4b Reapply "build system windows"
This reverts commit d9dc040927.
2025-11-14 09:33:38 +01:00
ddidderr d9dc040927 Revert "build system windows"
This reverts commit da758d770a.
2025-11-14 09:32:22 +01:00
ddidderr da758d770a build system windows 2025-11-14 09:30:47 +01:00
ddidderr 5fc6505474 update 2025-11-14 09:28:47 +01:00
ddidderr b53f249512 minor clippy 2025-11-14 09:05:53 +01:00
ddidderr 833c8afedf game thumbnails 2025-11-14 09:03:05 +01:00
ddidderr 567d293455 game sizes? 2025-11-14 08:12:09 +01:00
ddidderr 6eec74f0f6 unavailable games red button 2025-11-14 02:24:18 +01:00
ddidderr 2952b596e2 peers gone... 2025-11-14 02:16:53 +01:00
ddidderr fe7444be4f wip 2025-11-14 01:59:07 +01:00
ddidderr da8457edfc wip 2025-11-14 01:44:39 +01:00
ddidderr f209653842 dead peer discovery and available games updates 2025-11-14 01:24:42 +01:00
ddidderr 8432030292 detect if a game is deleted, added, modified locally 2025-11-14 01:12:01 +01:00
ddidderr 4764bb9fd3 log chunks 2025-11-14 01:02:49 +01:00
ddidderr 1b2b2cf8c0 file transfer: improve / fix 2025-11-14 00:47:02 +01:00
ddidderr 4e9707dd51 mdns improved peer discovery 2025-11-14 00:24:04 +01:00