use std::{
future::pending,
io::Cursor,
process,
sync::{
Arc, Mutex, PoisonError,
atomic::{AtomicU64, Ordering},
},
time::Duration,
};
use anyhow::Result;
use aws_lc_rs::{
aead::{Aad, LessSafeKey, NONCE_LEN, Nonce},
hmac::{Key, sign},
rand,
};
use bincode_next::{config::standard, encode_to_vec};
use bon::Builder;
use bytes::BytesMut;
use tokio::{
select,
sync::mpsc::{Receiver, Sender},
time::{Instant as TokioInstant, sleep_until},
};
use tokio_util::sync::CancellationToken;
use tracing::{error, info, warn};
use uuid::Uuid;
use crate::{
ConnectionReader, ConnectionWriter, Emulator, EncryptedFrame, TerminalMessage, UuidWrapper,
udp::reader::{
ClientRenderCtx, apply_full_state_rendering, decode_all_capped, intercept_queries_core,
process_bytes_with_prediction,
},
};
#[derive(Builder, Debug)]
pub struct TcpTransportSender {
id: Uuid,
rnk: LessSafeKey,
hmac: Key,
writer: ConnectionWriter,
control_rx: Receiver<EncryptedFrame>,
rx: Receiver<EncryptedFrame>,
#[builder(default)]
send_seq: u64,
}
impl TcpTransportSender {
pub async fn frame_loop(&mut self, token: CancellationToken) -> Result<()> {
let mut control_active = true;
loop {
select! {
biased;
() = token.cancelled() => break,
frame_opt = self.control_rx.recv(), if control_active => {
match frame_opt {
Some(frame) => {
let frame = match frame {
EncryptedFrame::Keepalive(_) => {
EncryptedFrame::Keepalive(now_micros())
}
other => other,
};
let seq = self.send_seq;
self.send_seq += 1;
let wire = self.encrypt(&frame, seq)?;
self.writer.write_data(&wire).await?;
}
None => control_active = false,
}
},
frame_opt = self.rx.recv() => {
match frame_opt {
Some(frame) => {
let seq = self.send_seq;
self.send_seq += 1;
let wire = self.encrypt(&frame, seq)?;
self.writer.write_data(&wire).await?;
}
None => break,
}
},
}
}
Ok(())
}
fn encrypt(&self, frame: &EncryptedFrame, seq: u64) -> Result<Vec<u8>> {
let data = encode_to_vec(frame, standard())?;
let aad = Aad::from(seq.to_be_bytes());
let mut encrypted_part = self.id.as_bytes().to_vec();
encrypted_part.extend_from_slice(&data);
let mut nonce_bytes = [0u8; NONCE_LEN];
rand::fill(&mut nonce_bytes)?;
let nonce = Nonce::try_assume_unique_for_key(&nonce_bytes)?;
self.rnk
.seal_in_place_append_tag(nonce, aad, &mut encrypted_part)?;
let seq_bytes = seq.to_be_bytes();
let mut to_sign = seq_bytes.to_vec();
to_sign.extend_from_slice(&encrypted_part);
let tag = sign(&self.hmac, &to_sign);
let tag_bytes = tag.as_ref();
let len = encrypted_part.len().to_be_bytes();
let mut packet = nonce_bytes.to_vec();
packet.extend_from_slice(&seq_bytes);
packet.extend_from_slice(tag_bytes);
packet.extend_from_slice(&len);
packet.extend_from_slice(&encrypted_part);
Ok(packet)
}
}
#[derive(Builder, Debug)]
pub struct TcpTransportReader {
id: Uuid,
rnk: LessSafeKey,
hmac: Key,
mac_tag_len: usize,
reader: ConnectionReader,
silence_timeout: Option<Duration>,
nak_out_tx: Option<Sender<EncryptedFrame>>,
reconnect_tx: Option<Sender<()>>,
last_rx_us: Option<Arc<AtomicU64>>,
repaint_tx: Option<Sender<()>>,
client_ack_tx: Option<Sender<u64>>,
#[builder(default)]
passthrough: bool,
}
impl TcpTransportReader {
#[cfg_attr(nightly, allow(clippy::too_many_lines))]
pub async fn client_frame_loop(
&mut self,
token: CancellationToken,
exit_token: CancellationToken,
exit_msg: Arc<Mutex<Option<&'static [u8]>>>,
ctx: ClientRenderCtx,
) {
let stdout_tx = ctx.stdout_tx().clone();
let emulator = ctx.emulator().clone();
let prediction = ctx.prediction().clone();
let renderer = ctx.renderer().clone();
let in_alt_screen = ctx.in_alt_screen().clone();
let mut prev_bytes = BytesMut::with_capacity(1024);
let mut osc_started = false;
let passthrough = self.passthrough;
let mut scrollback_mode = false;
let mut silence_deadline: Option<TokioInstant> =
self.silence_timeout.map(|d| TokioInstant::now() + d);
'session: loop {
select! {
biased;
() = token.cancelled() => break 'session,
() = async {
match silence_deadline {
Some(dl) => sleep_until(dl).await,
None => pending().await,
}
} => {
info!("TCP data channel: server not responding, signalling reconnect");
self.signal_reconnect_or_exit(1);
break 'session;
},
frame_res = self.read_frame() => {
if let Some(timeout) = self.silence_timeout {
silence_deadline = Some(TokioInstant::now() + timeout);
}
match frame_res {
Ok(Some(frame)) => {
match frame {
EncryptedFrame::Bytes((_id, message)) => {
let message = self.intercept_queries(&message, &emulator);
process_bytes_with_prediction(
message,
&mut prev_bytes,
&mut osc_started,
&ctx,
scrollback_mode,
&exit_token,
passthrough,
)
.await;
}
EncryptedFrame::CompressedBytes((_id, compressed)) => {
match decode_all_capped(compressed.as_slice()) {
Ok(decompressed) => {
let message = self.intercept_queries(&decompressed, &emulator);
process_bytes_with_prediction(
message,
&mut prev_bytes,
&mut osc_started,
&ctx,
scrollback_mode,
&exit_token,
passthrough,
)
.await;
}
Err(e) => {
error!("TCP transport: failed to decompress CompressedBytes: {e}");
}
}
}
EncryptedFrame::ScreenState(payload) => {
let repaint = apply_full_state_rendering(
&payload, &emulator, &prediction, &renderer, &in_alt_screen,
);
if !repaint.is_empty()
&& let Err(e) = stdout_tx.send(repaint).await
{
error!("TCP transport: error sending ScreenState repaint: {e}");
}
}
EncryptedFrame::ScreenStateCompressed(compressed) => {
match decode_all_capped(compressed.as_slice()) {
Ok(payload) => {
let repaint = apply_full_state_rendering(
&payload, &emulator, &prediction, &renderer, &in_alt_screen,
);
if !repaint.is_empty()
&& let Err(e) = stdout_tx.send(repaint).await
{
error!("TCP transport: error sending ScreenStateCompressed repaint: {e}");
}
}
Err(e) => {
error!("TCP transport: failed to decompress ScreenStateCompressed: {e}");
}
}
}
EncryptedFrame::Keepalive(ts) => {
if let Some(ref tx) = self.nak_out_tx
&& let Err(e) = tx.try_send(EncryptedFrame::Keepalive(ts))
{
warn!("TCP transport: failed to echo keepalive: {e}");
}
}
EncryptedFrame::Shutdown => {
info!("TCP transport: server is shutting down, reconnecting");
self.signal_reconnect_or_exit(0);
break 'session;
}
EncryptedFrame::PtyExit => {
*exit_msg
.lock()
.unwrap_or_else(PoisonError::into_inner) =
Some(b"[moshpit] Remote session ended.\r\n");
exit_token.cancel();
break 'session;
}
EncryptedFrame::ScrollbackStart => {
scrollback_mode = true;
}
EncryptedFrame::ScrollbackEnd => {
scrollback_mode = false;
let repaint = {
let emu = emulator.lock().unwrap_or_else(PoisonError::into_inner);
let screen = emu.screen();
let mut rend = renderer.lock().unwrap_or_else(PoisonError::into_inner);
rend.invalidate();
rend.render(screen, &[], None)
};
if !repaint.is_empty()
&& let Err(e) = stdout_tx.send(repaint).await
{
error!("TCP transport: error sending scrollback repaint: {e}");
}
}
EncryptedFrame::StateSyncDiff(_) => {
warn!("TCP transport: StateSyncDiff not supported, ignoring");
}
EncryptedFrame::StateChunk(_) => {
warn!("TCP transport: StateChunk not supported, ignoring");
}
EncryptedFrame::Resize(_)
| EncryptedFrame::Nak(_)
| EncryptedFrame::RepaintRequest
| EncryptedFrame::ClientAck(_) => {}
}
}
Ok(None) => {
info!("TCP data channel closed by server");
self.signal_reconnect_or_exit(1);
break 'session;
}
Err(e) => {
error!("TCP transport read error: {e}");
self.signal_reconnect_or_exit(1);
break 'session;
}
}
},
}
}
}
fn intercept_queries(&self, bytes: &[u8], emulator: &Arc<Mutex<Emulator>>) -> Vec<u8> {
let (out, responses) =
intercept_queries_core(bytes, "rgb:d0d0/d0d0/d0d0", "rgb:1c1c/1c1c/1c1c", emulator);
if let Some(ref tx) = self.nak_out_tx {
for resp in responses {
let frame = EncryptedFrame::Bytes((UuidWrapper::new(self.id), resp));
if let Err(e) = tx.try_send(frame) {
warn!("TCP transport: failed to send query response: {e}");
}
}
}
out
}
pub async fn server_frame_loop(
&mut self,
token: CancellationToken,
term_tx: Sender<TerminalMessage>,
) -> Result<()> {
loop {
select! {
biased;
() = token.cancelled() => break,
frame_res = self.read_frame() => {
match frame_res {
Ok(Some(frame)) => {
if let Some(ref counter) = self.last_rx_us {
counter.store(now_micros(), Ordering::Relaxed);
}
match frame {
EncryptedFrame::Bytes((_id, message)) => {
term_tx.send(TerminalMessage::Input(message)).await?;
}
EncryptedFrame::Resize((_id, columns, rows)) => {
term_tx
.send(TerminalMessage::Resize { rows, columns })
.await?;
}
EncryptedFrame::RepaintRequest => {
if let Some(ref tx) = self.repaint_tx
&& let Err(e) = tx.try_send(())
{
warn!("TCP transport: failed to signal repaint request: {e}");
}
}
EncryptedFrame::Keepalive(ts) => {
if let Some(ref tx) = self.nak_out_tx
&& let Err(e) = tx.try_send(EncryptedFrame::Keepalive(ts))
{
warn!("TCP transport: failed to echo keepalive: {e}");
}
}
EncryptedFrame::ClientAck(diff_id) => {
if let Some(ref tx) = self.client_ack_tx
&& let Err(e) = tx.try_send(diff_id)
{
warn!("TCP transport: failed to forward ClientAck: {e}");
}
}
_ => {}
}
}
Ok(None) => {
info!("TCP data channel: client closed connection");
break;
}
Err(e) => {
error!("TCP transport read error: {e}");
break;
}
}
},
}
}
Ok(())
}
fn signal_reconnect_or_exit(&self, code: i32) {
if let Some(ref tx) = self.reconnect_tx {
let _ = tx.try_send(());
} else {
process::exit(code);
}
}
async fn read_frame(&mut self) -> Result<Option<EncryptedFrame>> {
match self.reader.read_data().await? {
None => Ok(None),
Some(bytes) => {
let mut buf = BytesMut::from(bytes.as_slice());
let mut cursor = Cursor::new(&buf[..]);
match EncryptedFrame::parse(
&mut cursor,
self.id,
&self.hmac,
&self.rnk,
self.mac_tag_len,
) {
Ok(Some((frame, _seq))) => {
buf.clear();
Ok(Some(frame))
}
Ok(None) => {
warn!(
"TCP transport: received data blob that could not be parsed as EncryptedFrame"
);
Ok(None)
}
Err(e) => Err(e),
}
}
}
}
}
fn now_micros() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| u64::try_from(d.as_micros()).unwrap_or(u64::MAX))
}
#[cfg(test)]
mod tests {
use std::{
sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicU64, Ordering},
},
time::Duration,
};
use aws_lc_rs::{
aead::{AES_256_GCM_SIV, LessSafeKey, UnboundKey},
hmac::{HMAC_SHA512, Key},
};
use tokio::{
net::{TcpListener, TcpStream},
sync::mpsc::{Receiver, Sender, channel},
time::timeout,
};
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use super::{TcpTransportReader, TcpTransportSender};
use crate::{
ClientRenderCtx, ConnectionReader, ConnectionWriter, DisplayPreference, Emulator,
EncryptedFrame, PredictionEngine, Renderer, TerminalMessage, UuidWrapper,
};
const MAC_TAG_LEN: usize = 64;
const AEAD_BYTES: [u8; 32] = [7u8; 32];
const HMAC_BYTES: [u8; 64] = [9u8; 64];
fn aead() -> LessSafeKey {
LessSafeKey::new(UnboundKey::new(&AES_256_GCM_SIV, &AEAD_BYTES).expect("test AEAD key"))
}
fn hmac() -> Key {
Key::new(HMAC_SHA512, &HMAC_BYTES)
}
async fn make_link() -> (ConnectionWriter, ConnectionReader) {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind test listener");
let addr = listener.local_addr().expect("local addr");
let (server, client) = tokio::join!(
async { listener.accept().await.map(|(s, _)| s).expect("accept") },
async { TcpStream::connect(addr).await.expect("connect") },
);
let (_, client_w) = client.into_split();
let (server_r, _) = server.into_split();
let writer = ConnectionWriter::builder().writer(client_w).build();
let reader = ConnectionReader::builder().reader(server_r).build();
(writer, reader)
}
fn make_sender(
writer: ConnectionWriter,
id: Uuid,
) -> (
TcpTransportSender,
Sender<EncryptedFrame>,
Sender<EncryptedFrame>,
) {
let (control_tx, control_rx) = channel::<EncryptedFrame>(16);
let (data_tx, rx) = channel::<EncryptedFrame>(16);
let sender = TcpTransportSender::builder()
.id(id)
.rnk(aead())
.hmac(hmac())
.writer(writer)
.control_rx(control_rx)
.rx(rx)
.build();
(sender, control_tx, data_tx)
}
async fn make_reader_with_response_rx() -> (TcpTransportReader, Receiver<EncryptedFrame>) {
let (_writer, reader) = make_link().await;
let (tx, rx) = channel::<EncryptedFrame>(16);
let transport_reader = TcpTransportReader::builder()
.id(Uuid::new_v4())
.rnk(aead())
.hmac(hmac())
.mac_tag_len(MAC_TAG_LEN)
.reader(reader)
.nak_out_tx(tx)
.passthrough(false)
.build();
(transport_reader, rx)
}
fn make_emulator() -> Arc<Mutex<Emulator>> {
Arc::new(Mutex::new(Emulator::new(24, 80)))
}
fn make_render_ctx() -> (ClientRenderCtx, Receiver<Vec<u8>>) {
let (stdout_tx, stdout_rx) = channel::<Vec<u8>>(64);
let emulator = make_emulator();
let prediction = Arc::new(Mutex::new(PredictionEngine::new(DisplayPreference::Never)));
let renderer = Arc::new(Mutex::new(Renderer::new(24, 80)));
let in_alt_screen = Arc::new(AtomicBool::new(false));
let ctx = ClientRenderCtx::new(stdout_tx, emulator, prediction, renderer, in_alt_screen);
(ctx, stdout_rx)
}
#[tokio::test]
async fn intercept_queries_da1_sends_response_to_server() {
let (reader, mut rx) = make_reader_with_response_rx().await;
let emu = make_emulator();
let out = reader.intercept_queries(b"\x1b[c", &emu);
assert!(out.is_empty(), "DA1 query must be stripped from stdout");
let frame = rx
.try_recv()
.expect("a DA1 response frame must be sent back");
let EncryptedFrame::Bytes((_id, resp)) = frame else {
panic!("expected Bytes frame, got {frame:?}");
};
assert_eq!(resp, b"\x1b[?62c", "DA1 response payload");
}
#[tokio::test]
async fn intercept_queries_plain_bytes_pass_through_without_response() {
let (reader, mut rx) = make_reader_with_response_rx().await;
let emu = make_emulator();
let out = reader.intercept_queries(b"hello world", &emu);
assert_eq!(
out, b"hello world",
"non-query bytes pass through unchanged"
);
assert!(
rx.try_recv().is_err(),
"no response should be sent for plain output"
);
}
fn make_plain_reader(reader: ConnectionReader, id: Uuid) -> TcpTransportReader {
TcpTransportReader::builder()
.id(id)
.rnk(aead())
.hmac(hmac())
.mac_tag_len(MAC_TAG_LEN)
.reader(reader)
.build()
}
fn make_server_reader(
reader: ConnectionReader,
id: Uuid,
) -> (
TcpTransportReader,
Receiver<EncryptedFrame>,
Receiver<()>,
Receiver<u64>,
Arc<AtomicU64>,
) {
let (nak_tx, nak_rx) = channel::<EncryptedFrame>(16);
let (repaint_tx, repaint_rx) = channel::<()>(4);
let (ack_tx, ack_rx) = channel::<u64>(4);
let last_rx_us = Arc::new(AtomicU64::new(0));
let r = TcpTransportReader::builder()
.id(id)
.rnk(aead())
.hmac(hmac())
.mac_tag_len(MAC_TAG_LEN)
.reader(reader)
.nak_out_tx(nak_tx)
.repaint_tx(repaint_tx)
.client_ack_tx(ack_tx)
.last_rx_us(Arc::clone(&last_rx_us))
.build();
(r, nak_rx, repaint_rx, ack_rx, last_rx_us)
}
fn make_client_reader(
reader: ConnectionReader,
id: Uuid,
) -> (TcpTransportReader, Receiver<()>, Receiver<EncryptedFrame>) {
let (reconnect_tx, reconnect_rx) = channel::<()>(4);
let (nak_tx, nak_rx) = channel::<EncryptedFrame>(16);
let r = TcpTransportReader::builder()
.id(id)
.rnk(aead())
.hmac(hmac())
.mac_tag_len(MAC_TAG_LEN)
.reader(reader)
.reconnect_tx(reconnect_tx)
.nak_out_tx(nak_tx)
.passthrough(false)
.build();
(r, reconnect_rx, nak_rx)
}
#[tokio::test]
async fn sender_frame_loop_delivers_data_frames_in_order() {
let (writer, reader) = make_link().await;
let id = Uuid::new_v4();
let (mut sender, _control_tx, data_tx) = make_sender(writer, id);
let token = CancellationToken::new();
let loop_token = token.clone();
let handle = tokio::spawn(async move { sender.frame_loop(loop_token).await });
data_tx
.send(EncryptedFrame::Bytes((
UuidWrapper::new(id),
b"one".to_vec(),
)))
.await
.expect("send one");
data_tx
.send(EncryptedFrame::Bytes((
UuidWrapper::new(id),
b"two".to_vec(),
)))
.await
.expect("send two");
let mut rdr = make_plain_reader(reader, id);
let f1 = rdr.read_frame().await.expect("read1").expect("some1");
let f2 = rdr.read_frame().await.expect("read2").expect("some2");
let EncryptedFrame::Bytes((_, b1)) = f1 else {
panic!("expected Bytes, got {f1:?}");
};
let EncryptedFrame::Bytes((_, b2)) = f2 else {
panic!("expected Bytes, got {f2:?}");
};
assert_eq!(b1, b"one");
assert_eq!(b2, b"two");
token.cancel();
drop(data_tx);
let _joined = handle.await;
}
#[tokio::test]
async fn sender_frame_loop_rewrites_keepalive_timestamp() {
let (writer, reader) = make_link().await;
let id = Uuid::new_v4();
let (mut sender, control_tx, _data_tx) = make_sender(writer, id);
let token = CancellationToken::new();
let loop_token = token.clone();
let handle = tokio::spawn(async move { sender.frame_loop(loop_token).await });
control_tx
.send(EncryptedFrame::Keepalive(0))
.await
.expect("send keepalive");
let mut rdr = make_plain_reader(reader, id);
let frame = rdr.read_frame().await.expect("read").expect("some");
let EncryptedFrame::Keepalive(ts) = frame else {
panic!("expected Keepalive, got {frame:?}");
};
assert!(ts > 0, "keepalive timestamp must be rewritten to now");
token.cancel();
drop(control_tx);
let _joined = handle.await;
}
#[tokio::test]
async fn server_frame_loop_forwards_input_and_resize() {
let (writer, reader) = make_link().await;
let id = Uuid::new_v4();
let (mut sender, _control_tx, data_tx) = make_sender(writer, id);
let sender_token = CancellationToken::new();
let st = sender_token.clone();
let sender_handle = tokio::spawn(async move { sender.frame_loop(st).await });
let (mut srv, _nak, _repaint, _ack, last_rx_us) = make_server_reader(reader, id);
let (term_tx, mut term_rx) = channel::<TerminalMessage>(16);
let srv_token = CancellationToken::new();
let srv_t = srv_token.clone();
let srv_handle = tokio::spawn(async move { srv.server_frame_loop(srv_t, term_tx).await });
data_tx
.send(EncryptedFrame::Bytes((
UuidWrapper::new(id),
b"abc".to_vec(),
)))
.await
.expect("send input");
data_tx
.send(EncryptedFrame::Resize((UuidWrapper::new(id), 100, 40)))
.await
.expect("send resize");
let input = timeout(Duration::from_secs(2), term_rx.recv())
.await
.expect("input timeout")
.expect("input frame");
assert_eq!(input, TerminalMessage::Input(b"abc".to_vec()));
let resize = timeout(Duration::from_secs(2), term_rx.recv())
.await
.expect("resize timeout")
.expect("resize frame");
assert_eq!(
resize,
TerminalMessage::Resize {
rows: 40,
columns: 100
}
);
assert!(
last_rx_us.load(Ordering::Relaxed) > 0,
"watchdog counter must advance on receipt"
);
srv_token.cancel();
sender_token.cancel();
srv_handle.await.expect("srv join").expect("srv loop");
let _joined = sender_handle.await;
}
#[tokio::test]
async fn server_frame_loop_echoes_keepalive_and_forwards_control() {
let (writer, reader) = make_link().await;
let id = Uuid::new_v4();
let (mut sender, _control_tx, data_tx) = make_sender(writer, id);
let sender_token = CancellationToken::new();
let st = sender_token.clone();
let sender_handle = tokio::spawn(async move { sender.frame_loop(st).await });
let (mut srv, mut nak_rx, mut repaint_rx, mut ack_rx, _last) =
make_server_reader(reader, id);
let (term_tx, _term_rx) = channel::<TerminalMessage>(16);
let srv_token = CancellationToken::new();
let srv_t = srv_token.clone();
let srv_handle = tokio::spawn(async move { srv.server_frame_loop(srv_t, term_tx).await });
data_tx
.send(EncryptedFrame::Keepalive(42))
.await
.expect("send keepalive");
data_tx
.send(EncryptedFrame::RepaintRequest)
.await
.expect("send repaint");
data_tx
.send(EncryptedFrame::ClientAck(7))
.await
.expect("send ack");
let echoed = timeout(Duration::from_secs(2), nak_rx.recv())
.await
.expect("keepalive echo timeout")
.expect("keepalive echo");
assert!(matches!(echoed, EncryptedFrame::Keepalive(42)));
timeout(Duration::from_secs(2), repaint_rx.recv())
.await
.expect("repaint timeout")
.expect("repaint signal");
let ack = timeout(Duration::from_secs(2), ack_rx.recv())
.await
.expect("ack timeout")
.expect("ack value");
assert_eq!(ack, 7);
srv_token.cancel();
sender_token.cancel();
srv_handle.await.expect("srv join").expect("srv loop");
let _joined = sender_handle.await;
}
#[tokio::test]
async fn server_frame_loop_returns_on_eof() {
let (writer, reader) = make_link().await;
let id = Uuid::new_v4();
let (sender, _control_tx, _data_tx) = make_sender(writer, id);
drop(sender);
let (mut srv, _nak, _repaint, _ack, _last) = make_server_reader(reader, id);
let (term_tx, _term_rx) = channel::<TerminalMessage>(16);
let token = CancellationToken::new();
let res = timeout(
Duration::from_secs(2),
srv.server_frame_loop(token, term_tx),
)
.await
.expect("loop should return promptly on EOF");
assert!(res.is_ok(), "clean EOF returns Ok(())");
}
#[tokio::test]
async fn client_frame_loop_renders_bytes_to_stdout() {
let (writer, reader) = make_link().await;
let id = Uuid::new_v4();
let (mut sender, _control_tx, data_tx) = make_sender(writer, id);
let sender_token = CancellationToken::new();
let st = sender_token.clone();
let sender_handle = tokio::spawn(async move { sender.frame_loop(st).await });
let (mut client, _reconnect_rx, _nak_rx) = make_client_reader(reader, id);
let (ctx, mut stdout_rx) = make_render_ctx();
let token = CancellationToken::new();
let exit_token = CancellationToken::new();
let exit_msg = Arc::new(Mutex::new(None));
let ct = token.clone();
let et = exit_token.clone();
let em = Arc::clone(&exit_msg);
let client_handle =
tokio::spawn(async move { client.client_frame_loop(ct, et, em, ctx).await });
data_tx
.send(EncryptedFrame::Bytes((
UuidWrapper::new(id),
b"hello".to_vec(),
)))
.await
.expect("send bytes");
let out = timeout(Duration::from_secs(2), stdout_rx.recv())
.await
.expect("stdout timeout")
.expect("stdout frame");
assert!(!out.is_empty(), "rendered output must reach stdout");
token.cancel();
sender_token.cancel();
let _joined = timeout(Duration::from_secs(2), client_handle).await;
let _joined = sender_handle.await;
}
#[tokio::test]
async fn client_frame_loop_pty_exit_sets_message_and_cancels() {
let (writer, reader) = make_link().await;
let id = Uuid::new_v4();
let (mut sender, _control_tx, data_tx) = make_sender(writer, id);
let sender_token = CancellationToken::new();
let st = sender_token.clone();
let sender_handle = tokio::spawn(async move { sender.frame_loop(st).await });
let (mut client, _reconnect_rx, _nak_rx) = make_client_reader(reader, id);
let (ctx, _stdout_rx) = make_render_ctx();
let token = CancellationToken::new();
let exit_token = CancellationToken::new();
let exit_msg: Arc<Mutex<Option<&'static [u8]>>> = Arc::new(Mutex::new(None));
let et = exit_token.clone();
let em = Arc::clone(&exit_msg);
let client_handle = tokio::spawn(async move {
client.client_frame_loop(token, et, em, ctx).await;
});
data_tx
.send(EncryptedFrame::PtyExit)
.await
.expect("send pty exit");
timeout(Duration::from_secs(2), client_handle)
.await
.expect("client loop should finish")
.expect("client join");
assert!(exit_token.is_cancelled(), "PtyExit cancels the exit token");
assert!(
exit_msg.lock().unwrap().is_some(),
"PtyExit sets the exit message"
);
sender_token.cancel();
let _joined = sender_handle.await;
}
#[tokio::test]
async fn client_frame_loop_shutdown_signals_reconnect() {
let (writer, reader) = make_link().await;
let id = Uuid::new_v4();
let (mut sender, _control_tx, data_tx) = make_sender(writer, id);
let sender_token = CancellationToken::new();
let st = sender_token.clone();
let sender_handle = tokio::spawn(async move { sender.frame_loop(st).await });
let (mut client, mut reconnect_rx, _nak_rx) = make_client_reader(reader, id);
let (ctx, _stdout_rx) = make_render_ctx();
let token = CancellationToken::new();
let exit_token = CancellationToken::new();
let exit_msg = Arc::new(Mutex::new(None));
let client_handle = tokio::spawn(async move {
client
.client_frame_loop(token, exit_token, exit_msg, ctx)
.await;
});
data_tx
.send(EncryptedFrame::Shutdown)
.await
.expect("send shutdown");
timeout(Duration::from_secs(2), reconnect_rx.recv())
.await
.expect("reconnect timeout")
.expect("reconnect signal");
sender_token.cancel();
let _joined = timeout(Duration::from_secs(2), client_handle).await;
let _joined = sender_handle.await;
}
#[tokio::test]
async fn client_frame_loop_echoes_keepalive() {
let (writer, reader) = make_link().await;
let id = Uuid::new_v4();
let (mut sender, _control_tx, data_tx) = make_sender(writer, id);
let sender_token = CancellationToken::new();
let st = sender_token.clone();
let sender_handle = tokio::spawn(async move { sender.frame_loop(st).await });
let (mut client, _reconnect_rx, mut nak_rx) = make_client_reader(reader, id);
let (ctx, _stdout_rx) = make_render_ctx();
let token = CancellationToken::new();
let exit_token = CancellationToken::new();
let exit_msg = Arc::new(Mutex::new(None));
let ct = token.clone();
let client_handle = tokio::spawn(async move {
client
.client_frame_loop(ct, exit_token, exit_msg, ctx)
.await;
});
data_tx
.send(EncryptedFrame::Keepalive(99))
.await
.expect("send keepalive");
let echoed = timeout(Duration::from_secs(2), nak_rx.recv())
.await
.expect("keepalive echo timeout")
.expect("keepalive echo");
assert!(matches!(echoed, EncryptedFrame::Keepalive(99)));
token.cancel();
sender_token.cancel();
let _joined = timeout(Duration::from_secs(2), client_handle).await;
let _joined = sender_handle.await;
}
#[tokio::test]
async fn signal_reconnect_or_exit_sends_on_reconnect_tx() {
let (_writer, reader) = make_link().await;
let id = Uuid::new_v4();
let (client, mut reconnect_rx, _nak_rx) = make_client_reader(reader, id);
client.signal_reconnect_or_exit(0);
assert!(
reconnect_rx.try_recv().is_ok(),
"reconnect signal must be sent when reconnect_tx is set"
);
}
}