use serde::Serialize;
use std::borrow::Cow;
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter},
net::{
TcpStream,
tcp::{OwnedReadHalf, OwnedWriteHalf},
},
sync::{mpsc, oneshot},
task::JoinHandle,
time::{Instant, sleep},
};
use super::Error;
use super::protocol::Packet;
use log::{debug, error, info, trace, warn};
use serde::de::DeserializeOwned;
use std::time::Duration;
const CHANNEL_BUFFER_SIZE: usize = 32;
const INACTIVITY_TIMEOUT_SECS: u64 = 7;
const PING_RESPONSE_TIMEOUT_SECS: u64 = 3;
const MAX_PING_RETRIES: u8 = 3;
const PING_PACKET: &str = "ping!";
const PONG_PACKET: &str = "pong!";
const FOREVER_DURATION: Duration = Duration::from_secs(u64::MAX);
pub struct TcpStreamExt {
tx_outgoing: mpsc::Sender<String>,
rx_incoming: mpsc::Receiver<Result<String, Error>>,
_task_handle: JoinHandle<()>,
close_tx: Option<oneshot::Sender<()>>,
}
impl TcpStreamExt {
pub fn new(stream: TcpStream) -> Self {
let (reader_half, writer_half) = stream.into_split();
let reader = BufReader::new(reader_half);
let writer = BufWriter::new(writer_half);
let (tx_outgoing, rx_outgoing) = mpsc::channel::<String>(CHANNEL_BUFFER_SIZE);
let (tx_incoming, rx_incoming) =
mpsc::channel::<Result<String, Error>>(CHANNEL_BUFFER_SIZE);
let (close_tx, close_rx) = oneshot::channel::<()>();
let task_handle = tokio::spawn(run_connection(
reader,
writer,
rx_outgoing,
tx_incoming.clone(),
close_rx,
));
Self {
tx_outgoing,
rx_incoming,
_task_handle: task_handle,
close_tx: Some(close_tx),
}
}
pub async fn send<T: Serialize + Packet>(&mut self, msg: T) -> Result<(), Error> {
let body = serde_json::to_string(&msg)?;
let packet_str = format!("{}!{}", T::kind(), body);
self.tx_outgoing.send(packet_str).await.map_err(|_| {
Error::Channel(Cow::Borrowed("Failed to send packet to background task"))
})?;
Ok(())
}
pub async fn recv<R: DeserializeOwned + Packet>(&mut self) -> Result<R, Error> {
let received_result = self
.rx_incoming
.recv()
.await
.ok_or(Error::ConnectionClosed)?;
let buf = received_result?;
let mut split = buf.splitn(2, '!');
let kind = split
.next()
.ok_or(Error::Message(Cow::Borrowed("Received packet has no kind")))?;
let body = split
.next()
.ok_or(Error::Message(Cow::Borrowed("Received packet has no body")))?;
if kind != R::kind() {
return Err(Error::Message(Cow::Owned(format!(
"Expected response kind {}, got {}",
R::kind(),
kind
))));
}
log::info!("Recieved: {body}");
let response: R = serde_json::from_str(body)?;
Ok(response)
}
}
impl Drop for TcpStreamExt {
fn drop(&mut self) {
if let Some(sender) = self.close_tx.take() {
let _ = sender.send(());
}
}
}
async fn run_connection(
mut reader: BufReader<OwnedReadHalf>,
mut writer: BufWriter<OwnedWriteHalf>,
mut rx_outgoing: mpsc::Receiver<String>, tx_incoming: mpsc::Sender<Result<String, Error>>, mut close_rx: oneshot::Receiver<()>, ) {
info!("Connection task started.");
let mut line_buf = String::new();
let inactivity_timeout = Duration::from_secs(INACTIVITY_TIMEOUT_SECS);
let ping_response_timeout = Duration::from_secs(PING_RESPONSE_TIMEOUT_SECS);
let inactivity_timer = sleep(inactivity_timeout);
let ping_timer = sleep(FOREVER_DURATION);
tokio::pin!(inactivity_timer);
tokio::pin!(ping_timer);
let mut pings_sent_without_response = 0;
loop {
tokio::select! {
biased;
_ = &mut close_rx => {
info!("Received shutdown signal.");
break;
}
read_result = reader.read_line(&mut line_buf) => {
match read_result {
Ok(0) => { info!("Connection closed by peer (EOF).");
let _ = tx_incoming.send(Err(Error::ConnectionClosed)).await;
break;
}
Ok(bytes_read) => { trace!("Read {bytes_read} bytes.");
inactivity_timer.as_mut().reset(Instant::now() + inactivity_timeout);
if pings_sent_without_response > 0 {
debug!("Activity detected, resetting ping retry count.");
}
pings_sent_without_response = 0;
let received_line = line_buf.trim_end();
trace!("Received line: '{received_line}'");
if received_line == PING_PACKET {
debug!("Received PING, sending PONG.");
if let Err(e) = writer.write_all(format!("{PONG_PACKET}\n").as_bytes()).await {
error!("Failed to send PONG: {e}");
let _ = tx_incoming.send(Err(e.into())).await; break;
}
if let Err(e) = writer.flush().await {
error!("Failed to flush PONG: {e}");
let _ = tx_incoming.send(Err(e.into())).await; break;
}
trace!("PONG sent successfully.");
} else {
if received_line == PONG_PACKET {
debug!("Received PONG.");
} else {
debug!("Received data packet, forwarding upstream.");
let owned_line = received_line.to_string();
if tx_incoming.send(Ok(owned_line)).await.is_err() {
info!("Upstream receiver closed, shutting down connection task.");
break;
}
trace!("Forwarded packet upstream.");
}
}
line_buf.clear();
}
Err(e) => { error!("TCP read error: {e}");
let _ = tx_incoming.send(Err(e.into())).await; break;
}
}
}
Some(packet_str) = rx_outgoing.recv() => {
trace!("Received packet from upstream to send: '{packet_str}'");
let packet_with_newline = format!("{packet_str}\n");
if let Err(e) = writer.write_all(packet_with_newline.as_bytes()).await {
error!("TCP write error: {e}");
let _ = tx_incoming.send(Err(e.into())).await; break;
}
if let Err(e) = writer.flush().await {
error!("TCP flush error: {e}");
let _ = tx_incoming.send(Err(e.into())).await; break;
}
debug!("Successfully sent packet: '{packet_str}'");
inactivity_timer.as_mut().reset(Instant::now() + inactivity_timeout);
pings_sent_without_response = 0; }
_ = &mut inactivity_timer => {
if pings_sent_without_response == 0 {
debug!("Inactivity detected, sending PING (Attempt 1/{MAX_PING_RETRIES})");
if let Err(e) = writer.write_all(format!("{PING_PACKET}\n").as_bytes()).await {
error!("Failed to send PING (Attempt 1): {e}");
let _ = tx_incoming.send(Err(e.into())).await;
break;
}
if let Err(e) = writer.flush().await {
error!("Failed to flush PING (Attempt 1): {e}");
let _ = tx_incoming.send(Err(e.into())).await;
break;
}
trace!("PING (Attempt 1) sent successfully.");
pings_sent_without_response = 1;
ping_timer.as_mut().reset(Instant::now() + ping_response_timeout);
inactivity_timer.as_mut().reset(Instant::now() + inactivity_timeout);
}
else {
trace!("Inactivity timer fired while waiting for PONG, deferring to ping timer.");
}
}
_ = &mut ping_timer, if pings_sent_without_response > 0 => {
warn!("No response received after PING (Attempt {pings_sent_without_response}/{MAX_PING_RETRIES})");
if pings_sent_without_response >= MAX_PING_RETRIES {
error!("Ping timeout after {MAX_PING_RETRIES} retries. Closing connection.");
let _ = tx_incoming.send(Err(Error::Timeout)).await;
break;
}
let next_attempt = pings_sent_without_response + 1;
debug!("Sending PING (Attempt {next_attempt}/{MAX_PING_RETRIES})");
if let Err(e) = writer.write_all(format!("{PING_PACKET}\n").as_bytes()).await {
error!("Failed to send PING (Attempt {next_attempt}): {e}");
let _ = tx_incoming.send(Err(e.into())).await;
break;
}
if let Err(e) = writer.flush().await {
error!("Failed to flush PING (Attempt {next_attempt}): {e}");
let _ = tx_incoming.send(Err(e.into())).await;
break;
}
trace!("PING (Attempt {next_attempt}) sent successfully.");
pings_sent_without_response += 1;
ping_timer.as_mut().reset(Instant::now() + ping_response_timeout);
inactivity_timer.as_mut().reset(Instant::now() + inactivity_timeout);
}
else => {
info!("Select loop yielded no active branch, likely due to channel closure. Shutting down.");
break;
}
}
}
info!("Connection task finished.");
let _ = tx_incoming.send(Err(Error::ConnectionClosed)).await;
}