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