use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
use std::time::{Duration, Instant};
use rust_socketio::ClientBuilder;
use serde_json::{json, Value};
use crate::error::{Error, Result};
use crate::nets::debug::RttOptions;
use crate::nets::sio::{hex_decode, hex_encode, payload_json};
const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
enum RttEvent {
Connected { backend: String },
Data(Vec<u8>),
Error(String),
Stopped,
}
fn start_payload(netname: &str, opts: &RttOptions) -> Value {
let mut body = json!({ "netname": netname, "channel": opts.channel });
if let Some(a) = opts.search_addr {
body["search_addr"] = json!(a);
}
if let Some(s) = opts.search_size {
body["search_size"] = json!(s);
}
if let Some(c) = opts.chunk_size {
body["chunk_size"] = json!(c);
}
body
}
pub struct RttSession {
socket: Option<rust_socketio::client::Client>,
rx: Receiver<RttEvent>,
netname: String,
channel: u32,
backend: String,
buf: Vec<u8>,
}
impl RttSession {
pub(crate) fn open(
base_url: &str,
netname: String,
opts: &RttOptions,
bearer_token: Option<String>,
) -> Result<Self> {
let (tx, rx) = std::sync::mpsc::channel::<RttEvent>();
let socket = {
let tx_connected: Sender<RttEvent> = tx.clone();
let tx_data = tx.clone();
let tx_error = tx.clone();
let tx_stopped = tx;
let mut builder = ClientBuilder::new(base_url).namespace("/rtt");
if let Some(token) = &bearer_token {
builder = builder.opening_header("Authorization", format!("Bearer {token}"));
}
builder
.on("rtt_connected", move |payload, _| {
let info = payload_json(payload).unwrap_or(Value::Null);
let backend = info
.get("backend")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let _ = tx_connected.send(RttEvent::Connected { backend });
})
.on("rtt_data", move |payload, _| {
if let Some(bytes) = payload_json(payload)
.as_ref()
.and_then(|v| v.get("data"))
.and_then(Value::as_str)
.and_then(hex_decode)
{
let _ = tx_data.send(RttEvent::Data(bytes));
}
})
.on("error", move |payload, _| {
let message = payload_json(payload)
.as_ref()
.and_then(|v| v.get("message"))
.and_then(Value::as_str)
.unwrap_or("unknown RTT error")
.to_string();
let _ = tx_error.send(RttEvent::Error(message));
})
.on("rtt_stopped", move |_, _| {
let _ = tx_stopped.send(RttEvent::Stopped);
})
.connect()
.map_err(|e| Error::Connection(format!("Socket.IO connect failed: {e}")))?
};
socket
.emit("start_rtt", start_payload(&netname, opts))
.map_err(|e| Error::Stream(format!("could not start RTT session: {e}")))?;
let deadline = Instant::now() + CONNECT_TIMEOUT;
let mut session = RttSession {
socket: Some(socket),
rx,
netname,
channel: opts.channel,
backend: String::new(),
buf: Vec::new(),
};
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(Error::Timeout(format!(
"box did not confirm RTT session for '{}' within {CONNECT_TIMEOUT:?}",
session.netname
)));
}
match session.rx.recv_timeout(remaining) {
Ok(RttEvent::Connected { backend }) => {
session.backend = backend;
return Ok(session);
}
Ok(RttEvent::Data(bytes)) => session.buf.extend_from_slice(&bytes),
Ok(RttEvent::Error(msg)) => return Err(Error::Stream(msg)),
Ok(RttEvent::Stopped) => {}
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => {
return Err(Error::Stream("RTT session closed unexpectedly".to_string()))
}
}
}
}
pub fn netname(&self) -> &str {
&self.netname
}
pub fn channel(&self) -> u32 {
self.channel
}
pub fn backend(&self) -> &str {
&self.backend
}
pub fn write(&self, data: &[u8]) -> Result<()> {
let socket = self
.socket
.as_ref()
.ok_or_else(|| Error::Stream("RTT session already stopped".to_string()))?;
socket
.emit("rtt_write", json!({ "data": hex_encode(data) }))
.map_err(|e| Error::Stream(format!("RTT write failed: {e}")))
}
pub fn write_str(&self, s: &str) -> Result<()> {
self.write(s.as_bytes())
}
fn pump(&mut self, wait: Duration) -> Result<()> {
let mut wait = wait;
loop {
match self.rx.recv_timeout(wait) {
Ok(RttEvent::Data(bytes)) => {
self.buf.extend_from_slice(&bytes);
wait = Duration::ZERO;
}
Ok(RttEvent::Error(msg)) => return Err(Error::Stream(msg)),
Ok(_) => {}
Err(RecvTimeoutError::Timeout) => return Ok(()),
Err(RecvTimeoutError::Disconnected) => {
return Err(Error::Stream("RTT session closed unexpectedly".to_string()))
}
}
}
}
pub fn read(&mut self, timeout: Duration) -> Result<Vec<u8>> {
self.pump(timeout)?;
Ok(std::mem::take(&mut self.buf))
}
pub fn try_read(&mut self) -> Result<Vec<u8>> {
self.pump(Duration::ZERO)?;
Ok(std::mem::take(&mut self.buf))
}
pub fn wait_for(&mut self, needle: &[u8], timeout: Duration) -> Result<Vec<u8>> {
if needle.is_empty() {
return Ok(Vec::new());
}
let deadline = Instant::now() + timeout;
loop {
if let Some(pos) = self.buf.windows(needle.len()).position(|w| w == needle) {
let mut rest = self.buf.split_off(pos + needle.len());
std::mem::swap(&mut self.buf, &mut rest);
return Ok(rest);
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(Error::Timeout(format!(
"'{}' did not appear on RTT '{}' within {timeout:?}",
String::from_utf8_lossy(needle),
self.netname
)));
}
self.pump(remaining)?;
}
}
pub fn stop(mut self) -> Result<()> {
self.shutdown();
Ok(())
}
fn shutdown(&mut self) {
if let Some(socket) = self.socket.take() {
let _ = socket.emit("stop_rtt", json!({}));
let _ = socket.disconnect();
}
}
}
impl Drop for RttSession {
fn drop(&mut self) {
self.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn start_payload_omits_unset_options() {
let body = start_payload("dbg", &RttOptions::default());
assert_eq!(body, json!({ "netname": "dbg", "channel": 0 }));
}
#[test]
fn start_payload_carries_explicit_options() {
let body = start_payload(
"dbg",
&RttOptions {
channel: 1,
search_addr: Some(0x2002_0000),
search_size: Some(0x4000),
chunk_size: Some(4096),
},
);
assert_eq!(
body,
json!({
"netname": "dbg",
"channel": 1,
"search_addr": 0x2002_0000u64,
"search_size": 0x4000,
"chunk_size": 4096,
})
);
}
}