[omg] GUI, mDNS, list games on client start

This commit is contained in:
2024-11-10 15:11:22 +01:00
parent 70c327ff03
commit 89af1f9176
46 changed files with 5790 additions and 206 deletions

View File

@ -0,0 +1,183 @@
use std::{net::SocketAddr, time::Duration};
use lanspread_db::db::Game;
use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr;
use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient};
use tokio::{
io::AsyncWriteExt as _,
sync::mpsc::{UnboundedReceiver, UnboundedSender},
};
// use tracing_subscriber::EnvFilter;
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
#[derive(Debug)]
pub enum ClientEvent {
ListGames(Vec<Game>),
}
#[derive(Debug)]
pub enum ClientCommand {
ListGames,
GetGame(u64),
ServerAddr(SocketAddr),
}
/// # Errors
pub async fn run(
mut rx_control: UnboundedReceiver<ClientCommand>,
tx_event: UnboundedSender<ClientEvent>,
) -> eyre::Result<()> {
// tracing_subscriber::fmt()
// .with_env_filter(EnvFilter::from_default_env())
// .init();
// blocking wait for remote address
tracing::debug!("waiting for server address");
let server_addr = loop {
if let Some(ClientCommand::ServerAddr(addr)) = rx_control.recv().await {
tracing::info!("got server address: {addr}");
break addr;
}
};
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(server_addr).with_server_name("localhost");
let mut conn = client.connect(conn).await?;
conn.keep_alive(true)?;
tracing::info!(
"connected: (server: {}) (client: {})",
maybe_addr!(conn.remote_addr()),
maybe_addr!(conn.local_addr())
);
// tx
while let Some(cmd) = rx_control.recv().await {
let request = match cmd {
ClientCommand::ListGames => Request::ListGames,
ClientCommand::GetGame(id) => Request::GetGame { id },
ClientCommand::ServerAddr(_) => Request::Invalid(
[].into(),
"invalid control message (ServerAddr), should not happen".into(),
),
};
let data = request.encode();
tracing::trace!("encoded data: {}", String::from_utf8_lossy(&data));
let stream = conn.open_bidirectional_stream().await?;
let (mut rx, mut tx) = stream.split();
if let Err(e) = tx.write_all(&data).await {
tracing::error!(?e, "failed to send request to server");
}
if let Ok(Some(data)) = rx.receive().await {
tracing::trace!("server response (raw): {}", String::from_utf8_lossy(&data));
let response = Response::decode(&data);
tracing::trace!(
"server response (decoded): {}",
String::from_utf8_lossy(&data)
);
match response {
Response::Games(games) => {
for game in &games {
tracing::debug!(?game);
}
if let Err(e) = tx_event.send(ClientEvent::ListGames(games)) {
tracing::error!(?e, "failed to send ClientEvent::ListGames to client");
}
}
Response::Game(game) => tracing::debug!(?game, "game received"),
Response::GameNotFound(id) => tracing::debug!(?id, "game not found"),
Response::InvalidRequest(request_bytes, err) => tracing::error!(
"server says our request was invalid (error: {}): {}",
err,
String::from_utf8_lossy(&request_bytes)
),
Response::EncodingError(err) => {
tracing::error!("server encoding error: {err}");
}
Response::DecodingError(data, err) => {
tracing::error!(
"response decoding error: {} (data: {})",
err,
String::from_utf8_lossy(&data)
);
}
}
if let Err(err) = tx.close().await {
tracing::error!("failed to close stream: {err}");
}
}
}
tracing::info!("server closed connection");
Ok(())
}
// #[derive(Debug, Parser)]
// struct Cli {
// /// Server IP address.
// #[clap(long, default_value = "127.0.0.1")]
// ip: IpAddr,
// /// Server port.
// #[clap(long, default_value = "13337")]
// port: u16,
// }
// #[tokio::main]
// async fn main() -> eyre::Result<()> {
// let cli = Cli::parse();
// let (tx_control, rx_control) = tokio::sync::mpsc::unbounded_channel::<ControlMessage>();
// // Spawn client in a separate task
// let client_handle = tokio::spawn(async move {
// let remote_addr = SocketAddr::from((cli.ip, cli.port));
// Client::run(remote_addr, rx_control).await
// });
// // Handle stdin commands in the main task
// let mut stdin = BufReader::new(tokio::io::stdin());
// let mut line = String::new();
// loop {
// line.clear();
// if stdin.read_line(&mut line).await? == 0 {
// break; // EOF reached
// }
// // Trim whitespace and handle commands
// match line.trim() {
// "list" => {
// tx_control.send(ControlMessage::ListGames)?;
// }
// cmd if cmd.starts_with("get ") => {
// if let Ok(id) = cmd[4..].trim().parse::<u64>() {
// tx_control.send(ControlMessage::GetGame(id))?;
// } else {
// println!("Invalid game ID");
// }
// }
// "quit" | "exit" => break,
// "" => continue,
// _ => println!("Unknown command. Available commands: list, get <id>, quit"),
// }
// }
// client_handle.await??;
// Ok(())
// }

View File

@ -1,166 +0,0 @@
use std::{
net::{IpAddr, SocketAddr},
time::Duration,
};
use clap::Parser;
use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr;
use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient};
use tokio::{
io::{AsyncBufReadExt as _, AsyncWriteExt as _, BufReader},
sync::mpsc::UnboundedReceiver,
};
use tracing_subscriber::EnvFilter;
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
#[derive(Debug)]
enum ControlMessage {
ListGames,
GetGame(u64),
}
struct Client;
impl Client {
pub(crate) async fn run(
remote_addr: SocketAddr,
mut rx_control: UnboundedReceiver<ControlMessage>,
) -> eyre::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
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(remote_addr).with_server_name("localhost");
let mut conn = client.connect(conn).await?;
conn.keep_alive(true)?;
tracing::info!(
"connected: (server: {}) (client: {})",
maybe_addr!(conn.remote_addr()),
maybe_addr!(conn.local_addr())
);
// tx
while let Some(cmd) = rx_control.recv().await {
let request = match cmd {
ControlMessage::ListGames => Request::ListGames,
ControlMessage::GetGame(id) => Request::GetGame { id },
};
let data = request.encode();
tracing::trace!("encoded data: {}", String::from_utf8_lossy(&data));
let stream = conn.open_bidirectional_stream().await?;
let (mut rx, mut tx) = stream.split();
if let Err(e) = tx.write_all(&data).await {
tracing::error!(?e, "failed to send request to server");
}
if let Ok(Some(data)) = rx.receive().await {
tracing::trace!("server response (raw): {}", String::from_utf8_lossy(&data));
let response = Response::decode(&data);
tracing::trace!(
"server response (decoded): {}",
String::from_utf8_lossy(&data)
);
match response {
Response::Games(games) => {
for game in games {
tracing::debug!(?game);
}
}
Response::Game(game) => tracing::debug!(?game, "game received"),
Response::GameNotFound(id) => tracing::debug!(?id, "game not found"),
Response::InvalidRequest(request_bytes, err) => tracing::error!(
"server says our request was invalid (error: {}): {}",
err,
String::from_utf8_lossy(&request_bytes)
),
Response::EncodingError(err) => {
tracing::error!("server encoding error: {err}");
}
Response::DecodingError(data, err) => {
tracing::error!(
"response decoding error: {} (data: {})",
err,
String::from_utf8_lossy(&data)
);
}
}
if let Err(err) = tx.close().await {
tracing::error!("failed to close stream: {err}");
}
}
}
tracing::info!("server closed connection");
Ok(())
}
}
#[derive(Debug, Parser)]
struct Cli {
/// Server IP address.
#[clap(long, default_value = "127.0.0.1")]
ip: IpAddr,
/// Server port.
#[clap(long, default_value = "13337")]
port: u16,
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
let cli = Cli::parse();
let (tx_control, rx_control) = tokio::sync::mpsc::unbounded_channel::<ControlMessage>();
// Spawn client in a separate task
let client_handle = tokio::spawn(async move {
let remote_addr = SocketAddr::from((cli.ip, cli.port));
Client::run(remote_addr, rx_control).await
});
// Handle stdin commands in the main task
let mut stdin = BufReader::new(tokio::io::stdin());
let mut line = String::new();
loop {
line.clear();
if stdin.read_line(&mut line).await? == 0 {
break; // EOF reached
}
// Trim whitespace and handle commands
match line.trim() {
"list" => {
tx_control.send(ControlMessage::ListGames)?;
}
cmd if cmd.starts_with("get ") => {
if let Ok(id) = cmd[4..].trim().parse::<u64>() {
tx_control.send(ControlMessage::GetGame(id))?;
} else {
println!("Invalid game ID");
}
}
"quit" | "exit" => break,
"" => continue,
_ => println!("Unknown command. Available commands: list, get <id>, quit"),
}
}
client_handle.await??;
Ok(())
}