[omg] GUI, mDNS, list games on client start
This commit is contained in:
183
crates/lanspread-client/src/lib.rs
Normal file
183
crates/lanspread-client/src/lib.rs
Normal 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(())
|
||||
// }
|
@ -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(())
|
||||
}
|
Reference in New Issue
Block a user