[code] restructured into different crates

This commit is contained in:
2024-11-08 10:05:24 +01:00
parent 70e3aaea17
commit 04a39790b8
11 changed files with 202 additions and 301 deletions

View File

@ -0,0 +1,13 @@
[package]
name = "lanspread-client"
version = "0.1.0"
edition = "2021"
[dependencies]
# local
lanspread-db = { path = "../lanspread-db" }
# external
eyre = { workspace = true }
s2n-quic = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }

View File

@ -0,0 +1,65 @@
use std::{net::SocketAddr, sync::Arc};
use lanspread_db::{Game, GameDB};
use s2n_quic::{client::Connect, Client as QuicClient};
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(())
}

View File

@ -0,0 +1,11 @@
[package]
name = "lanspread-db"
version = "0.1.0"
edition = "2021"
[dependencies]
# external
eyre = { workspace = true}
semver = { workspace = true}
serde = { workspace = true}
serde_json = { workspace = true}

View File

@ -0,0 +1,204 @@
use std::{
collections::HashMap,
fmt,
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, Serialize, Deserialize)]
pub struct GameDB {
pub games: HashMap<u64, Game>,
next_id: u64,
}
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()
}
}

View File

@ -0,0 +1,16 @@
[package]
name = "lanspread-server"
version = "0.1.0"
edition = "2021"
[dependencies]
# local
lanspread-db = { path = "../lanspread-db" }
# external
bytes = { workspace = true }
eyre = { workspace = true }
itertools = { workspace = true }
s2n-quic = { workspace = true }
serde_json = { workspace = true }
semver = { workspace = true }
tokio = { workspace = true }

View File

@ -0,0 +1,116 @@
use std::{net::SocketAddr, path::PathBuf, sync::Arc};
use bytes::Bytes;
use itertools::Itertools as _;
use lanspread_db::GameDB;
use s2n_quic::Server as QuicServer;
use tokio::sync::Mutex;
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();
}