lanspread: Game Distribution on LAN parties (WIP)
This commit is contained in:
commit
70e3aaea17
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
1475
Cargo.lock
generated
Normal file
1475
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
Normal file
23
Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "lanspread"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "server"
|
||||||
|
path = "src/bin/server.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "client"
|
||||||
|
path = "src/bin/client.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bytes = "1.7.2"
|
||||||
|
clap = "4.5.18"
|
||||||
|
eyre = "0.6.12"
|
||||||
|
itertools = "0.13"
|
||||||
|
s2n-quic = "1.47.0"
|
||||||
|
semver = "1.0"
|
||||||
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
|
serde_json = "1.0.128"
|
||||||
|
tokio = { version = "1.40.0", features = ["full"] }
|
35
cert.pem
Normal file
35
cert.pem
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
xFR/TSuDnG5jObpzew==
|
||||||
|
-----END CERTIFICATE-----
|
52
key.pem
Normal file
52
key.pem
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
4g8cERBIkBP2otGtuFoA4oIQ6qsh
|
||||||
|
-----END PRIVATE KEY-----
|
3
rustfmt.toml
Normal file
3
rustfmt.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
imports_layout = "HorizontalVertical"
|
64
src/bin/client.rs
Normal file
64
src/bin/client.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use lanspread::db::{Game, GameDB};
|
||||||
|
use s2n_quic::{client::Connect, Client as QuicClient};
|
||||||
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
use tokio::{io::AsyncWriteExt as _, sync::Mutex};
|
||||||
|
|
||||||
|
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/cert.pem"));
|
||||||
|
|
||||||
|
const SERVER_ADDR: &str = "127.0.0.1";
|
||||||
|
const SERVER_PORT: u16 = 13337;
|
||||||
|
|
||||||
|
struct Client {
|
||||||
|
db: Arc<Mutex<GameDB>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Client {
|
||||||
|
db: Arc::new(Mutex::new(GameDB::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn run(&mut self, addr: SocketAddr) -> eyre::Result<()> {
|
||||||
|
let client = QuicClient::builder()
|
||||||
|
.with_tls(CERT_PEM)?
|
||||||
|
.with_io("0.0.0.0:0")?
|
||||||
|
.start()?;
|
||||||
|
|
||||||
|
let connect1 = Connect::new(addr).with_server_name("localhost");
|
||||||
|
let mut connection1 = client.connect(connect1).await?;
|
||||||
|
connection1.keep_alive(true)?;
|
||||||
|
|
||||||
|
let stream = connection1.open_bidirectional_stream().await?;
|
||||||
|
let (mut rx, mut tx) = stream.split();
|
||||||
|
|
||||||
|
let buf = b"get_games";
|
||||||
|
tx.write_all(&buf[..]).await?;
|
||||||
|
|
||||||
|
while let Ok(Some(data)) = rx.receive().await {
|
||||||
|
let games: Vec<Game> = serde_json::from_slice(&data)?;
|
||||||
|
self.db = Arc::new(Mutex::new(GameDB::from(games)));
|
||||||
|
tx.close().await.unwrap();
|
||||||
|
let db = self.db.lock().await;
|
||||||
|
|
||||||
|
eprintln!("received GameDB:");
|
||||||
|
for game in db.games.values() {
|
||||||
|
eprintln!("{:#?}", game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("server closed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> eyre::Result<()> {
|
||||||
|
let mut client = Client::new();
|
||||||
|
|
||||||
|
client
|
||||||
|
.run(format!("{SERVER_ADDR}:{SERVER_PORT}").parse().unwrap())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
117
src/bin/server.rs
Normal file
117
src/bin/server.rs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
use std::{net::SocketAddr, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use itertools::Itertools as _;
|
||||||
|
use s2n_quic::Server as QuicServer;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use lanspread::db::GameDB;
|
||||||
|
|
||||||
|
static KEY_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/key.pem"));
|
||||||
|
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/cert.pem"));
|
||||||
|
|
||||||
|
const SERVER_ADDR: &str = "0.0.0.0";
|
||||||
|
const SERVER_PORT: u16 = 13337;
|
||||||
|
|
||||||
|
pub(crate) struct Server {
|
||||||
|
pub(crate) db: Arc<Mutex<GameDB>>,
|
||||||
|
db_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
pub(crate) fn new<S: Into<PathBuf>>(db_path: S) -> Self {
|
||||||
|
let db_path = db_path.into();
|
||||||
|
let db = Arc::new(Mutex::new(GameDB::load_from_file(&db_path).unwrap()));
|
||||||
|
Server { db, db_path }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn run(&mut self, addr: SocketAddr) -> eyre::Result<()> {
|
||||||
|
let mut server = QuicServer::builder()
|
||||||
|
.with_tls((CERT_PEM, KEY_PEM))?
|
||||||
|
.with_io(addr)?
|
||||||
|
.start()?;
|
||||||
|
|
||||||
|
let db = self.db.clone();
|
||||||
|
while let Some(mut connection) = server.accept().await {
|
||||||
|
// spawn a new task for the connection
|
||||||
|
let db = db.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
eprintln!("Connection accepted from {:?}", connection.remote_addr());
|
||||||
|
|
||||||
|
while let Ok(Some(mut stream)) = connection.accept_bidirectional_stream().await {
|
||||||
|
// spawn a new task for the stream
|
||||||
|
let db = db.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
eprintln!("Stream opened from {:?}", stream.connection().remote_addr());
|
||||||
|
|
||||||
|
// echo any data back to the stream
|
||||||
|
while let Ok(Some(data)) = stream.receive().await {
|
||||||
|
eprintln!("got data from client: {data:?}");
|
||||||
|
if data.as_ref() == b"get_games" {
|
||||||
|
let games_vec: Vec<_> =
|
||||||
|
db.lock().await.games.values().cloned().collect();
|
||||||
|
let json = serde_json::to_string(&games_vec).unwrap();
|
||||||
|
stream.send(Bytes::from(json)).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_test_db<P: Into<PathBuf>>(db_path: P) {
|
||||||
|
let db_path = db_path.into();
|
||||||
|
|
||||||
|
let mut db = GameDB::new();
|
||||||
|
db.add_game(
|
||||||
|
"Call of Duty 3",
|
||||||
|
"A shooter game in war.",
|
||||||
|
"call_of_duty.tar.zst",
|
||||||
|
64,
|
||||||
|
semver::Version::new(1, 0, 0),
|
||||||
|
);
|
||||||
|
db.add_game(
|
||||||
|
"Counter-Strike Source",
|
||||||
|
"Valve's iconic shooter.",
|
||||||
|
"cstrike.tar.zst",
|
||||||
|
32,
|
||||||
|
semver::Version::new(1, 0, 0),
|
||||||
|
);
|
||||||
|
db.add_game(
|
||||||
|
"Factorio",
|
||||||
|
"Best game of all time, seriously.",
|
||||||
|
"factorio.tar.zst",
|
||||||
|
128,
|
||||||
|
semver::Version::new(1, 0, 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
db.update_game(1, Some("Call of Duty 4"), None, None);
|
||||||
|
db.save_to_file(&db_path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
const GAME_DB_PATH: &str = "/home/pfs/shm/game.db";
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
generate_test_db(GAME_DB_PATH);
|
||||||
|
|
||||||
|
let mut server = Server::new(GAME_DB_PATH);
|
||||||
|
|
||||||
|
server
|
||||||
|
.db
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.list_games()
|
||||||
|
.iter()
|
||||||
|
.sorted()
|
||||||
|
.for_each(|game| println!("{game:?}"));
|
||||||
|
|
||||||
|
server
|
||||||
|
.run(format!("{SERVER_ADDR}:{SERVER_PORT}").parse().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
251
src/db.rs
Normal file
251
src/db.rs
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::{File, OpenOptions},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
mod version_serde {
|
||||||
|
use semver::Version;
|
||||||
|
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize<S>(version: &Version, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&version.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Version, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
Version::parse(&s).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A game
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Game {
|
||||||
|
/// example: 1
|
||||||
|
pub id: u64,
|
||||||
|
/// example: Call of Duty 3
|
||||||
|
pub name: String,
|
||||||
|
/// example: A shooter game in war.
|
||||||
|
pub description: String,
|
||||||
|
/// example: call_of_duty.tar.zst
|
||||||
|
pub install_archive: String,
|
||||||
|
/// example: 8
|
||||||
|
pub max_players: u32,
|
||||||
|
/// example: 1.0.0
|
||||||
|
#[serde(with = "version_serde")]
|
||||||
|
pub version: semver::Version,
|
||||||
|
|
||||||
|
/// size (bytes) (not serialized)
|
||||||
|
#[serde(skip)]
|
||||||
|
pub size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Game {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}: {} {} ({} players) ({}: {} MB)\n {}",
|
||||||
|
self.id,
|
||||||
|
self.name,
|
||||||
|
self.version,
|
||||||
|
self.max_players,
|
||||||
|
self.install_archive,
|
||||||
|
self.size,
|
||||||
|
self.description,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Game {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Game {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.name == other.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Game {}
|
||||||
|
|
||||||
|
impl PartialOrd for Game {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.name.cmp(&other.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Game {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.name.cmp(&other.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GameDB {
|
||||||
|
pub games: HashMap<u64, Game>,
|
||||||
|
next_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for GameDB {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
use serde::ser::SerializeSeq;
|
||||||
|
let mut seq = serializer.serialize_seq(Some(self.games.len()))?;
|
||||||
|
for game in self.games.values() {
|
||||||
|
seq.serialize_element(game)?;
|
||||||
|
}
|
||||||
|
seq.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for GameDB {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct GameDBVisitor;
|
||||||
|
|
||||||
|
impl<'de> serde::de::Visitor<'de> for GameDBVisitor {
|
||||||
|
type Value = GameDB;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a sequence of Game objects")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||||
|
where
|
||||||
|
A: serde::de::SeqAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut games = HashMap::new();
|
||||||
|
let mut next_id = 0;
|
||||||
|
while let Some(game) = seq.next_element()? {
|
||||||
|
let game: Game = game;
|
||||||
|
next_id = next_id.max(game.id + 1);
|
||||||
|
games.insert(game.id, game);
|
||||||
|
}
|
||||||
|
Ok(GameDB { games, next_id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_seq(GameDBVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameDB {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
GameDB {
|
||||||
|
games: HashMap::new(),
|
||||||
|
next_id: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from(games: Vec<Game>) -> Self {
|
||||||
|
let mut db = GameDB::new();
|
||||||
|
for game in games {
|
||||||
|
let id = game.id;
|
||||||
|
db.games.insert(game.id, game);
|
||||||
|
db.next_id = db.next_id.max(id + 1);
|
||||||
|
}
|
||||||
|
db
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_game<S: Into<String>>(
|
||||||
|
&mut self,
|
||||||
|
name: S,
|
||||||
|
description: S,
|
||||||
|
install_archive: S,
|
||||||
|
max_players: u32,
|
||||||
|
version: semver::Version,
|
||||||
|
) -> u64 {
|
||||||
|
let id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
let game = Game {
|
||||||
|
id,
|
||||||
|
name: name.into(),
|
||||||
|
description: description.into(),
|
||||||
|
install_archive: install_archive.into(),
|
||||||
|
max_players,
|
||||||
|
version,
|
||||||
|
size: 0,
|
||||||
|
};
|
||||||
|
self.games.insert(id, game);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_game(&self, id: u64) -> Option<&Game> {
|
||||||
|
self.games.get(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_game<S: Into<String>>(
|
||||||
|
&mut self,
|
||||||
|
id: u64,
|
||||||
|
name: Option<S>,
|
||||||
|
description: Option<S>,
|
||||||
|
install_archive: Option<S>,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(game) = self.games.get_mut(&id) {
|
||||||
|
if let Some(new_name) = name {
|
||||||
|
game.name = new_name.into();
|
||||||
|
}
|
||||||
|
if let Some(new_description) = description {
|
||||||
|
game.description = new_description.into();
|
||||||
|
}
|
||||||
|
if let Some(archive) = install_archive {
|
||||||
|
game.install_archive = archive.into();
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_game(&mut self, id: u64) -> bool {
|
||||||
|
self.games.remove(&id).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_games(&self) -> Vec<&Game> {
|
||||||
|
self.games.values().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_game(&self, name: &str) -> Option<&Game> {
|
||||||
|
self.games.values().find(|game| game.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_to_file(&self, path: &Path) -> eyre::Result<()> {
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(path)?;
|
||||||
|
|
||||||
|
let games: Vec<&Game> = self.games.values().collect();
|
||||||
|
serde_json::to_writer(file, &games)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_from_file(path: &Path) -> eyre::Result<Self> {
|
||||||
|
let rdr = File::open(path)?;
|
||||||
|
let games: Vec<Game> = serde_json::from_reader(rdr)?;
|
||||||
|
let db = GameDB::from(games);
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GameDB {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
1
src/lib.rs
Normal file
1
src/lib.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod db;
|
Loading…
Reference in New Issue
Block a user