This commit is contained in:
2026-02-26 20:12:25 +01:00
parent 4318927060
commit 86d0f93ede
9 changed files with 576 additions and 758 deletions
+5
View File
@@ -78,15 +78,18 @@ LanSpread follows a layered, modular architecture with clear separation of conce
The project is organized as a Cargo workspace with 7 crates: The project is organized as a Cargo workspace with 7 crates:
**Foundation Layer:** **Foundation Layer:**
- **lanspread-utils**: Utility macros and helpers used across all crates - **lanspread-utils**: Utility macros and helpers used across all crates
- **lanspread-db**: Core data structures (`Game`, `GameFileDescription`, game metadata) - **lanspread-db**: Core data structures (`Game`, `GameFileDescription`, game metadata)
**Protocol & Communication:** **Protocol & Communication:**
- **lanspread-proto**: P2P communication protocol definitions - **lanspread-proto**: P2P communication protocol definitions
- Message types: `Request` (Ping, ListGames, GetGame, etc.), `Response`, `Message` trait - Message types: `Request` (Ping, ListGames, GetGame, etc.), `Response`, `Message` trait
- Serialization: JSON with `serde` - Serialization: JSON with `serde`
**Network Discovery & Compatibility:** **Network Discovery & Compatibility:**
- **lanspread-mdns**: mDNS service discovery and advertisement - **lanspread-mdns**: mDNS service discovery and advertisement
- Advertises and discovers "_lanspread._udp.local." services on LAN - Advertises and discovers "_lanspread._udp.local." services on LAN
- Uses `mdns-sd` crate - Uses `mdns-sd` crate
@@ -96,6 +99,7 @@ The project is organized as a Cargo workspace with 7 crates:
- Converts `EtiGame` structs to modern `Game` struct - Converts `EtiGame` structs to modern `Game` struct
**Core P2P Engine:** **Core P2P Engine:**
- **lanspread-peer**: Central orchestration for all P2P functionality - **lanspread-peer**: Central orchestration for all P2P functionality
- Entry point: `start_peer()` in `lib.rs` - Entry point: `start_peer()` in `lib.rs`
- Communicates via unbounded channels (`PeerEvent`, `PeerCommand`) - Communicates via unbounded channels (`PeerEvent`, `PeerCommand`)
@@ -108,6 +112,7 @@ The project is organized as a Cargo workspace with 7 crates:
- `path_validation.rs`: Security for file operations - `path_validation.rs`: Security for file operations
**Application Layer:** **Application Layer:**
- **lanspread-tauri-deno-ts**: Tauri desktop application - **lanspread-tauri-deno-ts**: Tauri desktop application
- Binary + Library crate for the UI layer - Binary + Library crate for the UI layer
- IPC commands: `request_games`, `install_game`, etc. - IPC commands: `request_games`, `install_game`, etc.
Generated
+501 -693
View File
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -1,14 +1,14 @@
[workspace] [workspace]
members = [
"crates/lanspread-compat",
"crates/lanspread-db",
"crates/lanspread-utils",
"crates/lanspread-mdns",
"crates/lanspread-proto",
"crates/lanspread-peer",
"crates/lanspread-tauri-deno-ts/src-tauri",
]
resolver = "2" resolver = "2"
members = [
"crates/lanspread-compat",
"crates/lanspread-db",
"crates/lanspread-mdns",
"crates/lanspread-peer",
"crates/lanspread-proto",
"crates/lanspread-tauri-deno-ts/src-tauri",
"crates/lanspread-utils",
]
[workspace.dependencies] [workspace.dependencies]
base64 = "0.22" base64 = "0.22"
@@ -18,24 +18,24 @@ clap = { version = "4", features = ["derive"] }
eyre = "0.6" eyre = "0.6"
futures = "0.3" futures = "0.3"
gethostname = "1" gethostname = "1"
if-addrs = "0.11" if-addrs = "0.15"
itertools = "0.14" itertools = "0.14"
log = "0.4" log = "0.4"
mdns-sd = "0.17" mdns-sd = "0.18"
mimalloc = { version = "0.1", features = ["secure"] } mimalloc = { version = "0.1", features = ["secure"] }
s2n-quic = { version = "1", features = ["provider-event-tracing"] } s2n-quic = { version = "1", features = ["provider-event-tracing"] }
semver = "1" semver = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
sqlx = { version = "0.8", default-features = false, features = [ sqlx = { version = "0.8", default-features = false, features = [
"derive", "derive",
"runtime-tokio", "runtime-tokio",
"sqlite", "sqlite",
] } ] }
tauri = { version = "2", features = [] } tauri = { version = "2", features = [] }
tauri-plugin-dialog = "2"
tauri-plugin-log = "2" tauri-plugin-log = "2"
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
tauri-plugin-dialog = "2"
tauri-plugin-store = "2" tauri-plugin-store = "2"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["codec"] } tokio-util = { version = "0.7", features = ["codec"] }
@@ -44,25 +44,25 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1", features = ["v7"] } uuid = { version = "1", features = ["v7"] }
walkdir = "2" walkdir = "2"
windows = { version = "0.62", features = [ windows = { version = "0.62", features = [
"Win32", "Win32",
"Win32_UI", "Win32_UI",
"Win32_UI_Shell", "Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging", "Win32_UI_WindowsAndMessaging",
] } ] }
[profile.release] [profile.release]
debug = true debug = true
strip = false
debug-assertions = true debug-assertions = true
overflow-checks = true overflow-checks = true
strip = false
lto = false lto = false
panic = "unwind" panic = "unwind"
codegen-units = 1 codegen-units = 1
[profile.release-lto] [profile.release-lto]
inherits = "release" inherits = "release"
lto = true
debug = false debug = false
strip = true
debug-assertions = false debug-assertions = false
overflow-checks = false overflow-checks = false
strip = true lto = true
-15
View File
@@ -1,15 +0,0 @@
# Wrong decisions
- **tauri-leptos** adds unnecessary complexity to the project.
Tauri is built to transfer backend Rust stuff to the frontend JavaScript world.
But with leptos, the frontend becomes Rust so you have to transfer everything again back into the Rust world.
# Good decisions
- **Tauri** is strong 💪
- Easy project setup with `cargo install create-tauri-app --locked` and `cargo create-tauri-app`
- Easy testing with `cargo tauri dev`
- Easy bundling (with installers and everything) with `cargo tauri build`
- Final binary size of my tauri-leptos project is 13MB (which seems small to me if you think that a whole WASM / Web stack is in there)
# Open questions
- **logging**: I don't understand the relationship (or lack thereof) between `tracing` and `log`.
I had to refactor logging in the client away from `tracing`.
+4 -2
View File
@@ -4,10 +4,10 @@
Simple server and GUI for LAN parties. Simple server and GUI for LAN parties.
## Development ## Development
### Prerequisites ### Prerequisites
```bash ```bash
# install Tauri CLI # install Tauri CLI
cargo install tauri-cli cargo install tauri-cli
@@ -15,9 +15,10 @@ cargo install tauri-cli
# install Deno with a package manager or from https://deno.land/ # install Deno with a package manager or from https://deno.land/
``` ```
### Build ### Build
#### Frontend #### Frontend
```bash ```bash
# Development # Development
cargo tauri dev # prefix with RUST_LOG=your_module=debug or similary for more verbose output cargo tauri dev # prefix with RUST_LOG=your_module=debug or similary for more verbose output
@@ -36,6 +37,7 @@ deno outdated --update --latest
``` ```
#### Backend #### Backend
```bash ```bash
# Development # Development
./server.sh [options...] # prefix with RUST_LOG=your_module=debug or similary for more verbose output ./server.sh [options...] # prefix with RUST_LOG=your_module=debug or similary for more verbose output
+30 -17
View File
@@ -95,29 +95,42 @@ Most scans become O(number of game dirs), with full recursion only when needed.
- Any mismatch or missing delta falls back to `LibrarySnapshot`. - Any mismatch or missing delta falls back to `LibrarySnapshot`.
- Loss of goodbye is harmless; stale timeout is authoritative. - Loss of goodbye is harmless; stale timeout is authoritative.
## TODO: roadmap from current design to this one ## Roadmap from current design to this one
1. Protocol updates in `lanspread-proto`: 1. Protocol updates in `lanspread-proto`:
- Add `Hello`, `HelloAck`, `LibrarySummary`, `LibrarySnapshot`, - Define `Hello`, `HelloAck`, `LibrarySummary`, `LibrarySnapshot`,
`LibraryDelta`, and optional `Goodbye`. `LibraryDelta`, and optional `Goodbye` messages.
- Add `peer_id`, `library_rev`, and `manifest_hash` to relevant types. - Thread `peer_id`, `library_rev`, and `manifest_hash` through all
library and manifest-bearing types.
- Make `HelloAck` carry the remote `library_rev` and `manifest_hash`
so the client can immediately select `LibraryDelta` vs `LibrarySnapshot`.
2. Peer identity: 2. Peer identity:
- Introduce stable `peer_id` in `PeerInfo` and `PeerGameDB`. - Persist a stable `peer_id` (UUID) in the peer config and inject it into
- Map `peer_id` to current `SocketAddr` and update on IP changes. `PeerInfo` and `PeerGameDB` at startup.
- Track `peer_id -> SocketAddr` in the discovery table and update the
address on any incoming handshake or mDNS refresh.
3. Discovery handshake: 3. Discovery handshake:
- Advertise TXT records in mDNS. - Publish `peer_id` and `library_rev` in mDNS TXT records to avoid
- Add handshake in `run_peer_discovery` or connection setup. immediate TCP/QUIC roundtrips when nothing changed.
- Keep compatibility fallback to `ListGames` for older peers. - Add a lightweight handshake in `run_peer_discovery` that exchanges
`Hello`/`HelloAck` before any library sync.
- Keep a fallback path that uses `ListGames` when `Hello` is unsupported.
4. Library revisioning: 4. Library revisioning:
- Track `library_rev` locally. - Store a monotonic `library_rev` locally and increment only after a
- Apply `LibraryDelta` and reject stale revisions. successful index refresh completes.
- Use `LibrarySnapshot` for first sync or delta mismatch. - 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.
5. Local index + scan optimizations: 5. Local index + scan optimizations:
- Add cached index storage (e.g., `.lanspread/index.json`). - Introduce a cached index file (e.g., `.lanspread/index.json`) that stores
- Implement filesystem watchers with debounce. per-root fingerprints and computed manifests.
- Add a low-frequency full scan as a safety net. - Use filesystem watchers with a debounce window to collect changes and
incrementally update the cache.
- Schedule a low-frequency full scan to reconcile missed watcher events.
6. Announce updates: 6. Announce updates:
- Replace broad `AnnounceGames` with deltas. - Replace `AnnounceGames` with `LibraryDelta` broadcasts keyed by
- Send `LibrarySummary` on new connections. `library_rev`.
- Send `LibrarySummary` on new connections to seed the delta flow.
7. File manifest caching: 7. File manifest caching:
- Store per-game `manifest_hash` and only fetch details when changed. - Store per-game `manifest_hash` and only fetch details when changed.
8. Liveness: 8. Liveness:
+7 -8
View File
@@ -11,10 +11,7 @@ use crate::{
download::download_game_files, download::download_game_files,
identity::FEATURE_LIBRARY_DELTA, identity::FEATURE_LIBRARY_DELTA,
local_games::{ local_games::{
LocalLibraryScan, LocalLibraryScan, get_game_file_descriptions, local_download_available, scan_local_library,
get_game_file_descriptions,
local_download_available,
scan_local_library,
}, },
network::{announce_games_to_peer, request_game_details_from_peer, send_library_delta}, network::{announce_games_to_peer, request_game_details_from_peer, send_library_delta},
peer_db::{PeerGameDB, PeerId}, peer_db::{PeerGameDB, PeerId},
@@ -218,10 +215,13 @@ pub async fn handle_download_game_files_command(
return; return;
} }
let downloading = ctx.downloading_games.read().await; let local_dl_available = {
let downloading = ctx.downloading_games.read().await;
local_download_available(&games_folder, &id, &downloading).await
};
if peer_whitelist.is_empty() { if peer_whitelist.is_empty() {
if local_download_available(&games_folder, &id, &downloading).await { if local_dl_available {
drop(downloading);
log::info!("Using locally downloaded files for game {id}; skipping peer transfer"); log::info!("Using locally downloaded files for game {id}; skipping peer transfer");
if let Err(e) = tx_notify_ui.send(PeerEvent::DownloadGameFilesBegin { id: id.clone() }) if let Err(e) = tx_notify_ui.send(PeerEvent::DownloadGameFilesBegin { id: id.clone() })
{ {
@@ -237,7 +237,6 @@ pub async fn handle_download_game_files_command(
} }
return; return;
} }
drop(downloading);
{ {
let mut in_progress = ctx.downloading_games.write().await; let mut in_progress = ctx.downloading_games.write().await;
@@ -280,6 +280,8 @@ async fn get_game_thumbnail(
tauri::path::BaseDirectory::Resource, tauri::path::BaseDirectory::Resource,
)?; )?;
dbg!(&resource_path);
let image_data = std::fs::read(&resource_path)?; let image_data = std::fs::read(&resource_path)?;
let base64_data = base64::engine::general_purpose::STANDARD.encode(&image_data); let base64_data = base64::engine::general_purpose::STANDARD.encode(&image_data);
Ok(format!("data:image/jpeg;base64,{base64_data}")) Ok(format!("data:image/jpeg;base64,{base64_data}"))
@@ -854,7 +856,7 @@ pub fn run() {
.path() .path()
.resolve("game.db", tauri::path::BaseDirectory::Resource) .resolve("game.db", tauri::path::BaseDirectory::Resource)
{ {
Ok(path) => path, Ok(path) => { dbg!(&path); path },
Err(e) => { Err(e) => {
log::error!("Failed to resolve game.db resource: {e}"); log::error!("Failed to resolve game.db resource: {e}");
panic!("game.db resource is mandatory - cannot continue"); panic!("game.db resource is mandatory - cannot continue");
+4
View File
@@ -1,4 +1,5 @@
export RUSTFLAGS := "-C target-cpu=native" export RUSTFLAGS := "-C target-cpu=native"
export WEBKIT_DISABLE_COMPOSITING_MODE := "1"
server: server:
cargo build -p lanspread-server cargo build -p lanspread-server
@@ -6,6 +7,9 @@ server:
client: client:
cargo tauri dev cargo tauri dev
buildclient:
cargo tauri build --no-bundle -- --profile dev
fmt: fmt:
cargo +nightly fmt cargo +nightly fmt