wip
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::{fs::File, io::Write, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use lanspread_db::db::{Game, GameFileDescription};
|
||||
use lanspread_proto::{Message, Request, Response};
|
||||
use s2n_quic::{
|
||||
Client as QuicClient,
|
||||
Connection,
|
||||
Server,
|
||||
client::Connect,
|
||||
provider::limits::Limits,
|
||||
stream::BidirectionalStream,
|
||||
};
|
||||
use tokio::{
|
||||
io::AsyncWriteExt,
|
||||
sync::{
|
||||
RwLock,
|
||||
mpsc::{UnboundedReceiver, UnboundedSender},
|
||||
},
|
||||
};
|
||||
|
||||
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
|
||||
static KEY_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../key.pem"));
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PeerEvent {
|
||||
ListGames(Vec<Game>),
|
||||
GotGameFiles {
|
||||
id: String,
|
||||
file_descriptions: Vec<GameFileDescription>,
|
||||
},
|
||||
DownloadGameFilesBegin {
|
||||
id: String,
|
||||
},
|
||||
DownloadGameFilesFinished {
|
||||
id: String,
|
||||
},
|
||||
DownloadGameFilesFailed {
|
||||
id: String,
|
||||
},
|
||||
PeerConnected(SocketAddr),
|
||||
PeerDisconnected(SocketAddr),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PeerCommand {
|
||||
ListGames,
|
||||
GetGame(String),
|
||||
DownloadGameFiles {
|
||||
id: String,
|
||||
file_descriptions: Vec<GameFileDescription>,
|
||||
},
|
||||
SetGameDir(String),
|
||||
ConnectToPeer(SocketAddr),
|
||||
}
|
||||
|
||||
async fn initial_peer_alive_check(conn: &mut Connection) -> bool {
|
||||
let stream = match conn.open_bidirectional_stream().await {
|
||||
Ok(stream) => stream,
|
||||
Err(e) => {
|
||||
log::error!("failed to open stream: {e}");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let (mut rx, mut tx) = stream.split();
|
||||
|
||||
// send ping
|
||||
if let Err(e) = tx.send(Request::Ping.encode()).await {
|
||||
log::error!("failed to send ping to peer: {e}");
|
||||
return false;
|
||||
}
|
||||
let _ = tx.close().await;
|
||||
|
||||
// receive pong
|
||||
if let Ok(Some(response)) = rx.receive().await {
|
||||
let response = Response::decode(response);
|
||||
match response {
|
||||
Response::Pong => {
|
||||
log::info!("peer is alive");
|
||||
return true;
|
||||
}
|
||||
_ => {
|
||||
log::error!("peer sent invalid response to ping: {response:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
async fn receive_game_file(
|
||||
conn: &mut Connection,
|
||||
desc: &GameFileDescription,
|
||||
games_folder: &str,
|
||||
) -> eyre::Result<()> {
|
||||
log::info!("downloading: {desc:?}");
|
||||
|
||||
let stream = conn.open_bidirectional_stream().await?;
|
||||
let (mut rx, mut tx) = stream.split();
|
||||
|
||||
let request = Request::GetGameFileData(desc.clone());
|
||||
|
||||
// request file
|
||||
tx.write_all(&request.encode()).await?;
|
||||
|
||||
// create file
|
||||
let path = PathBuf::from(&games_folder).join(&desc.relative_path);
|
||||
let mut file = File::create(&path)?;
|
||||
|
||||
// receive file contents
|
||||
while let Some(data) = rx.receive().await? {
|
||||
file.write_all(&data)?;
|
||||
}
|
||||
log::debug!("file download complete: {}", path.display());
|
||||
|
||||
tx.close().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_game_files(
|
||||
game_id: &str,
|
||||
game_file_descs: Vec<GameFileDescription>,
|
||||
games_folder: String,
|
||||
peer_addr: SocketAddr,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
) -> eyre::Result<()> {
|
||||
let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?;
|
||||
|
||||
let client = QuicClient::builder()
|
||||
.with_tls(CERT_PEM)?
|
||||
.with_io("0.0.0.0:0")?
|
||||
.with_limits(limits)?
|
||||
.start()?;
|
||||
|
||||
let conn = Connect::new(peer_addr).with_server_name("localhost");
|
||||
let mut conn = client.connect(conn).await?;
|
||||
conn.keep_alive(true)?;
|
||||
|
||||
let game_files = game_file_descs
|
||||
.iter()
|
||||
.filter(|desc| !desc.is_dir)
|
||||
.filter(|desc| !desc.is_version_ini())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if game_files.is_empty() {
|
||||
eyre::bail!("game_file_descs empty: no game files to download");
|
||||
}
|
||||
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesBegin {
|
||||
id: game_id.to_string(),
|
||||
})?;
|
||||
|
||||
// receive all game files
|
||||
for file_desc in game_files {
|
||||
receive_game_file(&mut conn, file_desc, &games_folder).await?;
|
||||
}
|
||||
|
||||
let version_file_desc = game_file_descs
|
||||
.iter()
|
||||
.find(|desc| desc.is_version_ini())
|
||||
.ok_or_else(|| eyre::eyre!("version.ini not found"))?;
|
||||
|
||||
// receive version.ini
|
||||
receive_game_file(&mut conn, version_file_desc, &games_folder).await?;
|
||||
|
||||
log::info!("all files downloaded for game: {game_id}");
|
||||
tx_notify_ui.send(PeerEvent::DownloadGameFilesFinished {
|
||||
id: game_id.to_string(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Ctx {
|
||||
game_dir: Arc<RwLock<Option<String>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct PeerCtx {
|
||||
game_dir: Arc<RwLock<Option<String>>>,
|
||||
}
|
||||
|
||||
pub async fn run_peer(
|
||||
mut rx_control: UnboundedReceiver<PeerCommand>,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
) -> eyre::Result<()> {
|
||||
// peer context
|
||||
let ctx = Ctx {
|
||||
game_dir: Arc::new(RwLock::new(None)),
|
||||
};
|
||||
|
||||
let peer_ctx = PeerCtx {
|
||||
game_dir: ctx.game_dir.clone(),
|
||||
};
|
||||
|
||||
// Start server component
|
||||
let server_addr = "0.0.0.0:0".parse::<SocketAddr>()?;
|
||||
let tx_notify_ui_clone = tx_notify_ui.clone();
|
||||
let peer_ctx_clone = peer_ctx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = run_server_component(server_addr, peer_ctx_clone, tx_notify_ui_clone).await
|
||||
{
|
||||
log::error!("Server component error: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
// Handle client commands
|
||||
loop {
|
||||
let Some(cmd) = rx_control.recv().await else {
|
||||
break;
|
||||
};
|
||||
|
||||
match cmd {
|
||||
PeerCommand::ListGames => {
|
||||
// TODO: Implement peer discovery and game listing
|
||||
log::info!("ListGames command received");
|
||||
}
|
||||
PeerCommand::GetGame(id) => {
|
||||
log::info!("Requesting game from peer: {id}");
|
||||
// TODO: Implement game fetching from peers
|
||||
}
|
||||
PeerCommand::DownloadGameFiles {
|
||||
id,
|
||||
file_descriptions: _,
|
||||
} => {
|
||||
log::info!("Got PeerCommand::DownloadGameFiles");
|
||||
let games_folder = { ctx.game_dir.read().await.clone() };
|
||||
if let Some(_games_folder) = games_folder {
|
||||
// TODO: Implement peer file downloading
|
||||
log::info!("Would download game files for {id}");
|
||||
} else {
|
||||
log::error!("Cannot handle game file descriptions: games_folder is not set");
|
||||
}
|
||||
}
|
||||
PeerCommand::SetGameDir(game_dir) => {
|
||||
*ctx.game_dir.write().await = Some(game_dir.clone());
|
||||
log::info!("Game directory set to: {game_dir}");
|
||||
}
|
||||
PeerCommand::ConnectToPeer(peer_addr) => {
|
||||
log::info!("Connecting to peer: {peer_addr}");
|
||||
// TODO: Implement peer connection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_server_component(
|
||||
addr: SocketAddr,
|
||||
ctx: PeerCtx,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
) -> eyre::Result<()> {
|
||||
let limits = Limits::default()
|
||||
.with_max_handshake_duration(Duration::from_secs(3))?
|
||||
.with_max_idle_timeout(Duration::from_secs(3))?;
|
||||
|
||||
let mut server = Server::builder()
|
||||
.with_tls((CERT_PEM, KEY_PEM))?
|
||||
.with_io(addr)?
|
||||
.with_limits(limits)?
|
||||
.start()?;
|
||||
|
||||
let server_addr = server.local_addr()?;
|
||||
log::info!("Peer server listening on {server_addr}");
|
||||
|
||||
// TODO: Implement mDNS advertising for peer discovery
|
||||
|
||||
while let Some(connection) = server.accept().await {
|
||||
let ctx = ctx.clone();
|
||||
let tx_notify_ui = tx_notify_ui.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_peer_connection(connection, ctx, tx_notify_ui).await {
|
||||
log::error!("Peer connection error: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_peer_connection(
|
||||
mut connection: Connection,
|
||||
ctx: PeerCtx,
|
||||
tx_notify_ui: UnboundedSender<PeerEvent>,
|
||||
) -> eyre::Result<()> {
|
||||
let remote_addr = connection.remote_addr()?;
|
||||
log::info!("{remote_addr} peer connected");
|
||||
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerConnected(remote_addr)) {
|
||||
log::error!("Failed to send PeerConnected event: {e}");
|
||||
}
|
||||
|
||||
// handle streams
|
||||
while let Ok(Some(stream)) = connection.accept_bidirectional_stream().await {
|
||||
let ctx = ctx.clone();
|
||||
let remote_addr = Some(remote_addr);
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_peer_stream(stream, ctx, remote_addr).await {
|
||||
log::error!("{remote_addr:?} peer stream error: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(e) = tx_notify_ui.send(PeerEvent::PeerDisconnected(remote_addr)) {
|
||||
log::error!("Failed to send PeerDisconnected event: {e}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_peer_stream(
|
||||
stream: BidirectionalStream,
|
||||
ctx: PeerCtx,
|
||||
remote_addr: Option<SocketAddr>,
|
||||
) -> eyre::Result<()> {
|
||||
let (mut rx, mut tx) = stream.split();
|
||||
|
||||
log::trace!("{remote_addr:?} peer stream opened");
|
||||
|
||||
// handle streams
|
||||
loop {
|
||||
match rx.receive().await {
|
||||
Ok(Some(data)) => {
|
||||
log::trace!(
|
||||
"{remote_addr:?} msg: (raw): {}",
|
||||
String::from_utf8_lossy(&data)
|
||||
);
|
||||
|
||||
let request = Request::decode(data);
|
||||
log::debug!("{remote_addr:?} msg: {request:?}");
|
||||
|
||||
match request {
|
||||
Request::Ping => {
|
||||
// Respond with pong
|
||||
if let Err(e) = tx.send(Response::Pong.encode()).await {
|
||||
log::error!("Failed to send pong: {e}");
|
||||
}
|
||||
}
|
||||
Request::ListGames => {
|
||||
// TODO: Return list of games from this peer
|
||||
log::info!("Received ListGames request from peer");
|
||||
}
|
||||
Request::GetGame { id } => {
|
||||
log::info!("Received GetGame request for {id} from peer");
|
||||
// TODO: Handle game request
|
||||
}
|
||||
Request::GetGameFileData(desc) => {
|
||||
log::info!(
|
||||
"Received GetGameFileData request for {} from peer",
|
||||
desc.relative_path
|
||||
);
|
||||
// TODO: Handle file data request
|
||||
}
|
||||
Request::Invalid(_, _) => {
|
||||
log::error!("Received invalid request from peer");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
log::trace!("{remote_addr:?} peer stream closed");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{remote_addr:?} peer stream error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user