Skip to content
This repository was archived by the owner on Jun 25, 2026. It is now read-only.

Commit b9cac05

Browse files
committed
add level logging and client implementation for relay communication
1 parent 18e4cbc commit b9cac05

2 files changed

Lines changed: 248 additions & 0 deletions

File tree

src/client.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use tokio::io::{BufReader, BufWriter};
2+
use tokio::net::TcpStream;
3+
4+
use crate::protocol;
5+
use crate::types::{ClientWire, ServerEvent, ServerWire, SyncError};
6+
7+
/// A connected relay client.
8+
pub struct Client {
9+
reader: BufReader<tokio::net::tcp::OwnedReadHalf>,
10+
writer: BufWriter<tokio::net::tcp::OwnedWriteHalf>,
11+
max_payload: usize,
12+
}
13+
14+
impl Client {
15+
/// Connect to a relay server.
16+
pub async fn connect(addr: &str) -> Result<Self, SyncError> {
17+
Self::builder().connect(addr).await
18+
}
19+
20+
/// Start building a client.
21+
pub fn builder() -> ClientBuilder {
22+
ClientBuilder::new()
23+
}
24+
25+
/// Join a room. If room does not exist, returns [`SyncError::RoomNotFound`]
26+
pub async fn join(&mut self, room_id: &str) -> Result<(), SyncError> {
27+
let msg = ClientWire::JoinRoom {
28+
room_id: room_id.into(),
29+
};
30+
self.send(&msg).await
31+
}
32+
33+
/// Leave the current room.
34+
pub async fn leave(&mut self) -> Result<(), SyncError> {
35+
self.send(&ClientWire::LeaveRoom).await
36+
}
37+
38+
/// Send a ping (keep-alive).
39+
pub async fn ping(&mut self) -> Result<(), SyncError> {
40+
self.send(&ClientWire::Ping).await
41+
}
42+
43+
/// Broadcast raw bytes to all peers in the current room.
44+
pub async fn broadcast(&mut self, data: &[u8]) -> Result<(), SyncError> {
45+
let msg = ClientWire::Broadcast {
46+
data: data.to_vec(),
47+
};
48+
self.send(&msg).await
49+
}
50+
51+
/// Receive the next server event.
52+
/// Ping/pong keepalive is handled internally — the user never sees it.
53+
/// Returns `Ok(None)` on clean disconnect.
54+
pub async fn recv(&mut self) -> Result<Option<ServerEvent>, SyncError> {
55+
loop {
56+
let payload = match protocol::read_frame_raw(&mut self.reader, self.max_payload).await {
57+
Ok(p) => p,
58+
Err(ref e) if e.is_connection_closed() => {
59+
return Ok(None);
60+
}
61+
Err(e) => return Err(e),
62+
};
63+
64+
let wire: ServerWire = wincode::deserialize(&payload)
65+
.map_err(|e| SyncError::Protocol(format!("deserialize failed: {:?}", e)))?;
66+
67+
// Internal: auto-respond to server pings
68+
if matches!(wire, ServerWire::Ping) {
69+
self.send(&ClientWire::Pong).await?;
70+
continue;
71+
}
72+
73+
// Internal: swallow pong responses
74+
if matches!(wire, ServerWire::Pong) {
75+
continue;
76+
}
77+
78+
return Ok(Some(Self::wire_to_event(wire)));
79+
}
80+
}
81+
82+
async fn send(&mut self, msg: &ClientWire) -> Result<(), SyncError> {
83+
protocol::write_frame(&mut self.writer, msg).await
84+
}
85+
86+
fn wire_to_event(wire: ServerWire) -> ServerEvent {
87+
match wire {
88+
ServerWire::Joined { client_id, room_id } => ServerEvent::Joined { client_id, room_id },
89+
ServerWire::PlayerJoined { client_id } => ServerEvent::PlayerJoined { client_id },
90+
ServerWire::PlayerLeft { client_id } => ServerEvent::PlayerLeft { client_id },
91+
ServerWire::Error(msg) => ServerEvent::Error(msg),
92+
ServerWire::Broadcast { sender_id, data } => ServerEvent::Broadcast { sender_id, data },
93+
// Ping/Pong are handled internally before reaching here
94+
_ => unreachable!("ping/pong should be handled in recv()"),
95+
}
96+
}
97+
}
98+
99+
/// Builder for configuring a [`Client`].
100+
pub struct ClientBuilder {
101+
max_payload: usize,
102+
}
103+
104+
impl ClientBuilder {
105+
pub fn new() -> Self {
106+
Self {
107+
max_payload: 64 * 1024,
108+
}
109+
}
110+
}
111+
112+
impl Default for ClientBuilder {
113+
fn default() -> Self {
114+
Self::new()
115+
}
116+
}
117+
118+
impl ClientBuilder {
119+
/// Set max payload size for incoming frames.
120+
pub fn max_payload(mut self, n: usize) -> Self {
121+
self.max_payload = n;
122+
self
123+
}
124+
125+
/// Connect to the server.
126+
pub async fn connect(self, addr: &str) -> Result<Client, SyncError> {
127+
let stream = TcpStream::connect(addr).await.map_err(|e| {
128+
if e.kind() == std::io::ErrorKind::ConnectionRefused {
129+
SyncError::ConnectionRefused
130+
} else {
131+
SyncError::Io(e)
132+
}
133+
})?;
134+
let (read_half, write_half) = stream.into_split();
135+
136+
Ok(Client {
137+
reader: BufReader::new(read_half),
138+
writer: BufWriter::new(write_half),
139+
max_payload: self.max_payload,
140+
})
141+
}
142+
}

src/log.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use colored::{ColoredString, Colorize};
2+
use std::env;
3+
use std::sync::LazyLock;
4+
5+
static GHOSTSYNC_LOG_LEVEL: LazyLock<Level> = LazyLock::new(Level::from_env);
6+
7+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
8+
pub enum Level {
9+
Trace = 0,
10+
Debug = 1,
11+
Info = 2,
12+
Warn = 3,
13+
Error = 4,
14+
}
15+
16+
impl Level {
17+
fn from_env() -> Self {
18+
dotenv::dotenv().ok();
19+
20+
env::var("LOG_LEVEL")
21+
.ok()
22+
.and_then(|s| match s.to_lowercase().as_str() {
23+
"trace" => Some(Level::Trace),
24+
"debug" => Some(Level::Debug),
25+
"info" => Some(Level::Info),
26+
"warn" => Some(Level::Warn),
27+
"error" => Some(Level::Error),
28+
_ => None,
29+
})
30+
.unwrap_or(Level::Info)
31+
}
32+
}
33+
34+
#[inline]
35+
fn should_log(level: Level) -> bool {
36+
level >= *GHOSTSYNC_LOG_LEVEL
37+
}
38+
39+
#[inline]
40+
pub fn log(level: Level, msg: ColoredString) {
41+
if !should_log(level) {
42+
return;
43+
}
44+
45+
let now = chrono::Local::now();
46+
47+
let level_str = match level {
48+
Level::Trace => "TRACE".dimmed(),
49+
Level::Debug => "DEBUG".blue().bold(),
50+
Level::Info => "INFO".green().bold(),
51+
Level::Warn => "WARN".yellow().bold(),
52+
Level::Error => "ERROR".red().bold(),
53+
};
54+
55+
println!("[{}][{}] {}", now.format("%H:%M:%S"), level_str, msg);
56+
}
57+
58+
#[macro_export]
59+
macro_rules! trace {
60+
($($arg:tt)*) => {
61+
{
62+
use colored::Colorize;
63+
$crate::log::log($crate::log::Level::Trace, format!($($arg)*).dimmed())
64+
}
65+
};
66+
}
67+
68+
#[macro_export]
69+
macro_rules! debug {
70+
($($arg:tt)*) => {
71+
{
72+
use colored::Colorize;
73+
$crate::log::log($crate::log::Level::Debug, format!($($arg)*).blue())
74+
}
75+
};
76+
}
77+
78+
#[macro_export]
79+
macro_rules! info {
80+
($($arg:tt)*) => {
81+
{
82+
use colored::Colorize;
83+
$crate::log::log($crate::log::Level::Info, format!($($arg)*).green())
84+
}
85+
};
86+
}
87+
88+
#[macro_export]
89+
macro_rules! warn {
90+
($($arg:tt)*) => {
91+
{
92+
use colored::Colorize;
93+
$crate::log::log($crate::log::Level::Warn, format!($($arg)*).yellow())
94+
}
95+
};
96+
}
97+
98+
#[macro_export]
99+
macro_rules! error {
100+
($($arg:tt)*) => {
101+
{
102+
use colored::Colorize;
103+
$crate::log::log($crate::log::Level::Error, format!($($arg)*).red())
104+
}
105+
};
106+
}

0 commit comments

Comments
 (0)