asd
This commit is contained in:
@@ -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
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -1,14 +1,14 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/lanspread-compat",
|
"crates/lanspread-compat",
|
||||||
"crates/lanspread-db",
|
"crates/lanspread-db",
|
||||||
"crates/lanspread-utils",
|
|
||||||
"crates/lanspread-mdns",
|
"crates/lanspread-mdns",
|
||||||
"crates/lanspread-proto",
|
|
||||||
"crates/lanspread-peer",
|
"crates/lanspread-peer",
|
||||||
|
"crates/lanspread-proto",
|
||||||
"crates/lanspread-tauri-deno-ts/src-tauri",
|
"crates/lanspread-tauri-deno-ts/src-tauri",
|
||||||
|
"crates/lanspread-utils",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
@@ -18,10 +18,10 @@ 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"
|
||||||
@@ -33,9 +33,9 @@ sqlx = { version = "0.8", default-features = false, features = [
|
|||||||
"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"] }
|
||||||
@@ -52,17 +52,17 @@ windows = { version = "0.62", features = [
|
|||||||
|
|
||||||
[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
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 local_dl_available = {
|
||||||
let downloading = ctx.downloading_games.read().await;
|
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");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user