use {
anyhow::{anyhow, Context, Result},
std::{
io::{BufRead, BufReader, Read, Write},
net::TcpStream,
os::unix::net::{UnixListener, UnixStream},
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
thread,
time::{Duration, Instant},
},
};
const REG_COUNT: usize = 12;
const REG_BYTES: usize = REG_COUNT * 8;
const REG_HEX_LEN: usize = REG_BYTES * 2;
pub const SOCKET_ENV: &str = "ANCHOR_GDB_SOCKET";
pub struct GdbDriver {
profile_dir: PathBuf,
sock_path: PathBuf,
stop: Arc<AtomicBool>,
accept: Option<thread::JoinHandle<Result<()>>>,
captures: Arc<Mutex<Vec<PendingCapture>>>,
}
impl GdbDriver {
pub fn sock_path(&self) -> &Path {
&self.sock_path
}
fn shutdown(&mut self) {
self.stop.store(true, Ordering::Release);
if let Some(h) = self.accept.take() {
let _ = h.join();
}
if let Ok(captures) = self.captures.lock() {
if let Err(e) = write_pending_captures(&self.profile_dir, &captures) {
eprintln!("gdb sidecar write error: {e}");
}
}
let _ = std::fs::remove_file(&self.sock_path);
}
}
impl Drop for GdbDriver {
fn drop(&mut self) {
self.shutdown();
}
}
pub fn start_gdb_driver(profile_dir: &Path) -> Result<GdbDriver> {
std::fs::create_dir_all(profile_dir).ok();
let sock_path = profile_dir.join("gdb.sock");
let _ = std::fs::remove_file(&sock_path); let listener =
UnixListener::bind(&sock_path).with_context(|| format!("bind {}", sock_path.display()))?;
let stop = Arc::new(AtomicBool::new(false));
let captures = Arc::new(Mutex::new(Vec::new()));
let accept_stop = Arc::clone(&stop);
let profile_dir_for_accept = profile_dir.to_path_buf();
let captures_for_accept = Arc::clone(&captures);
let accept_handle = thread::spawn(move || -> Result<()> {
listener
.set_nonblocking(true)
.context("set listener non-blocking")?;
let mut drivers: Vec<thread::JoinHandle<()>> = Vec::new();
while !accept_stop.load(Ordering::Acquire) {
match listener.accept() {
Ok((conn, _addr)) => {
let pd = profile_dir_for_accept.clone();
let captures = Arc::clone(&captures_for_accept);
drivers.push(thread::spawn(move || {
if let Err(e) = handle_announce(conn, &pd, captures) {
eprintln!("gdb driver error: {e}");
}
}));
drivers.retain(|h| !h.is_finished());
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(20));
}
Err(e) => return Err(e).context("accept"),
}
}
for d in drivers {
let _ = d.join();
}
Ok(())
});
Ok(GdbDriver {
profile_dir: profile_dir.to_path_buf(),
sock_path,
stop,
accept: Some(accept_handle),
captures,
})
}
#[allow(clippy::too_many_arguments)]
pub fn run_gdb_mode(
cargo_cwd: &Path,
current_package: Option<&str>,
profile_feature: &str,
profile_dir: &Path,
test_filter: Option<&str>,
) -> Result<()> {
let driver = start_gdb_driver(profile_dir)?;
let mut cmd = std::process::Command::new("cargo");
cmd.current_dir(cargo_cwd)
.env(SOCKET_ENV, driver.sock_path())
.env("ANCHOR_PROFILE_DIR", profile_dir)
.env("RUST_TEST_THREADS", "1")
.arg("test")
.arg("--features")
.arg(profile_feature);
if let Some(pkg) = current_package {
cmd.arg("-p").arg(pkg);
}
cmd.arg("--").arg("--test-threads=1");
if let Some(filter) = test_filter {
cmd.arg(filter);
}
let test_status = cmd.status().context("spawn cargo test")?;
drop(driver);
if !test_status.success() {
return Err(anyhow!("cargo test failed"));
}
Ok(())
}
fn handle_announce(
conn: UnixStream,
profile_dir: &Path,
captures: Arc<Mutex<Vec<PendingCapture>>>,
) -> Result<()> {
let mut rdr = BufReader::new(conn);
let mut line = String::new();
rdr.read_line(&mut line).context("read announce")?;
let line = line.trim_end();
let (port_str, test_name) = line
.split_once('\t')
.ok_or_else(|| anyhow!("malformed announce: {line:?}"))?;
let port: u16 = port_str.parse().context("parse port")?;
let test_dir = profile_dir.join(sanitize(test_name));
std::fs::create_dir_all(&test_dir).with_context(|| format!("create {}", test_dir.display()))?;
let mut deadline = Duration::from_secs(5);
while let Some(outer) = wait_for_connect(port, deadline) {
let capture = drive_session(outer, port, test_name, 0)?;
captures.lock().unwrap().push(PendingCapture {
test_name: sanitize(test_name),
capture,
});
deadline = Duration::from_millis(750);
}
Ok(())
}
struct PendingCapture {
test_name: String,
capture: CapturedInvocation,
}
struct CapturedInvocation {
regs: Vec<u8>,
insns: Vec<u8>,
cu: Vec<u8>,
children: Vec<CapturedInvocation>,
}
fn write_pending_captures(profile_dir: &Path, captures: &[PendingCapture]) -> Result<()> {
let mut by_test = std::collections::BTreeMap::<&str, Vec<&CapturedInvocation>>::new();
for pending in captures {
by_test
.entry(&pending.test_name)
.or_default()
.push(&pending.capture);
}
for (test_name, captures) in by_test {
let test_dir = profile_dir.join(test_name);
std::fs::create_dir_all(&test_dir)
.with_context(|| format!("create {}", test_dir.display()))?;
let mut flattened = Vec::new();
for capture in captures {
flatten_capture_postorder(capture, &mut flattened);
}
let mut regular = regular_invocation_stems(&test_dir)?;
for capture in flattened {
let step_count = capture.regs.len() / REG_BYTES;
let regular_idx = regular
.iter()
.position(|r| !r.used && r.step_count == step_count)
.or_else(|| regular.iter().position(|r| !r.used));
let stem = if let Some(idx) = regular_idx {
regular[idx].used = true;
regular[idx].stem.clone()
} else {
let fallback = regular.len() + 1;
test_dir.join(format!("{fallback:04}__tx1"))
};
write_capture_sidecars(capture, &stem)?;
}
}
Ok(())
}
fn flatten_capture_postorder<'a>(
capture: &'a CapturedInvocation,
out: &mut Vec<&'a CapturedInvocation>,
) {
for child in &capture.children {
flatten_capture_postorder(child, out);
}
out.push(capture);
}
struct RegularInvocationStem {
stem: PathBuf,
step_count: usize,
used: bool,
}
fn regular_invocation_stems(test_dir: &Path) -> Result<Vec<RegularInvocationStem>> {
let mut found = Vec::new();
if !test_dir.exists() {
return Ok(Vec::new());
}
for entry in std::fs::read_dir(test_dir)? {
let path = entry?.path();
if path.extension().and_then(|e| e.to_str()) != Some("regs") {
continue;
}
let Some(stem_str) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
if stem_str.ends_with(".gdb") {
continue;
}
let Some((inv_part, tx_part)) = stem_str.split_once("__") else {
continue;
};
let Some(tx_digits) = tx_part.strip_prefix("tx") else {
continue;
};
let Ok(inv_seq) = inv_part.parse::<u32>() else {
continue;
};
let Ok(tx_seq) = tx_digits.parse::<u32>() else {
continue;
};
let step_count = std::fs::metadata(&path)
.map(|m| m.len() as usize / REG_BYTES)
.unwrap_or(0);
found.push((
tx_seq,
inv_seq,
RegularInvocationStem {
stem: path.with_extension(""),
step_count,
used: false,
},
));
}
found.sort_by_key(|(tx_seq, inv_seq, _)| (*tx_seq, *inv_seq));
Ok(found.into_iter().map(|(_, _, stem)| stem).collect())
}
fn write_capture_sidecars(capture: &CapturedInvocation, stem: &Path) -> Result<()> {
std::fs::write(stem.with_extension("gdb.regs"), &capture.regs)?;
std::fs::write(stem.with_extension("gdb.insns"), &capture.insns)?;
std::fs::write(stem.with_extension("gdb.cu"), &capture.cu)?;
Ok(())
}
fn drive_session(
stream: TcpStream,
port: u16,
test_name: &str,
depth: usize,
) -> Result<CapturedInvocation> {
stream.set_nodelay(true).ok();
let mut rsp = Rsp::new(stream);
let started = Instant::now();
let mut last_progress = started;
rsp.send("QStartNoAckMode");
let reply = rsp.recv();
rsp.no_ack = reply == "OK";
rsp.send("qSupported:multiprocess+;swbreak+;hwbreak+;vContSupported+");
let _ = rsp.recv();
rsp.send("?");
let mut reply = rsp.recv();
let mut capture = CapturedInvocation {
regs: Vec::new(),
insns: Vec::new(),
cu: Vec::new(),
children: Vec::new(),
};
let mut steps: u64 = 0;
print_progress(test_name, port, depth, steps, started, false);
loop {
if reply.starts_with('W') || reply.starts_with('X') {
break;
}
if !reply.starts_with('T') && !reply.starts_with('S') {
eprintln!("unexpected stop reply: {reply}");
break;
}
rsp.send("g");
let regs_hex = rsp.recv();
if regs_hex.is_empty() {
break;
}
rsp.send("p0c");
let cu_hex = rsp.recv();
let (regs, pc, _cu_stub, insn) = decode_regs_hex(®s_hex)?;
let cu = decode_u64_hex(&cu_hex).unwrap_or(0);
capture.regs.extend_from_slice(®s);
capture.cu.extend_from_slice(&cu.to_le_bytes());
let insn_bytes = read_insn_at(&mut rsp, pc)?;
capture.insns.extend_from_slice(&insn_bytes);
let _ = insn;
let nested_port = port;
let nested_test_name = test_name.to_owned();
let probe_cancel = Arc::new(AtomicBool::new(false));
let probe_cancel_for_thread = Arc::clone(&probe_cancel);
let probe = thread::spawn(move || -> Result<Option<CapturedInvocation>> {
if let Some(inner) = probe_for_nested(
nested_port,
Duration::from_millis(250),
&probe_cancel_for_thread,
) {
return drive_session(inner, nested_port, &nested_test_name, depth + 1).map(Some);
}
Ok(None)
});
rsp.send("s");
let step_reply = rsp.recv();
probe_cancel.store(true, Ordering::Release);
if let Ok(Ok(Some(child))) = probe.join() {
capture.children.push(child);
}
if step_reply.is_empty() {
break;
}
reply = step_reply;
steps += 1;
if last_progress.elapsed() >= Duration::from_secs(5) {
print_progress(test_name, port, depth, steps, started, false);
last_progress = Instant::now();
}
}
print_progress(test_name, port, depth, steps, started, true);
Ok(capture)
}
fn print_progress(
test_name: &str,
port: u16,
depth: usize,
steps: u64,
started: Instant,
done: bool,
) {
let elapsed = started.elapsed().as_secs_f64().max(0.001);
let rate = steps as f64 / elapsed;
let indent = " ".repeat(depth);
let status = if done { "captured" } else { "stepping" };
eprintln!(
"{indent}gdb {status} {test_name} :{port} depth={depth} steps={steps} elapsed={:.1}s \
rate={:.1}/s",
elapsed, rate,
);
}
fn wait_for_connect(port: u16, deadline: Duration) -> Option<TcpStream> {
let start = Instant::now();
while start.elapsed() < deadline {
if let Ok(s) = TcpStream::connect_timeout(
&format!("127.0.0.1:{port}").parse().ok()?,
Duration::from_millis(50),
) {
return Some(s);
}
thread::sleep(Duration::from_millis(5));
}
None
}
fn probe_for_nested(port: u16, window: Duration, cancel: &AtomicBool) -> Option<TcpStream> {
let start = Instant::now();
while start.elapsed() < window && !cancel.load(Ordering::Acquire) {
if let Ok(s) = TcpStream::connect(("127.0.0.1", port)) {
return Some(s);
}
thread::yield_now();
}
None
}
fn decode_regs_hex(hex: &str) -> Result<([u8; REG_BYTES], u64, u64, [u8; 8])> {
if hex.len() < REG_HEX_LEN {
return Err(anyhow!(
"register hex too short: {} bytes, payload={hex:?}",
hex.len()
));
}
let mut regs = [0u8; REG_BYTES];
for (i, pair) in hex.as_bytes()[..REG_HEX_LEN].chunks_exact(2).enumerate() {
let s = std::str::from_utf8(pair).unwrap();
regs[i] = u8::from_str_radix(s, 16).context("invalid hex")?;
}
let pc = u64::from_le_bytes(regs[11 * 8..12 * 8].try_into().unwrap());
let cu = 0; let insn = [0u8; 8]; Ok((regs, pc, cu, insn))
}
fn decode_u64_hex(hex: &str) -> Option<u64> {
if hex.len() < 16 {
return None;
}
let mut bytes = [0u8; 8];
for (i, pair) in hex.as_bytes()[..16].chunks_exact(2).enumerate() {
bytes[i] = u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()?;
}
Some(u64::from_le_bytes(bytes))
}
fn read_insn_at(rsp: &mut Rsp, pc: u64) -> Result<[u8; 8]> {
rsp.send(&format!("m{pc:x},8"));
decode_bytes_hex::<8>(&rsp.recv())
}
fn decode_bytes_hex<const N: usize>(hex: &str) -> Result<[u8; N]> {
if hex.len() < N * 2 {
return Err(anyhow!(
"memory hex too short: {} bytes, payload={hex:?}",
hex.len()
));
}
let mut out = [0u8; N];
for (i, pair) in hex.as_bytes()[..N * 2].chunks_exact(2).enumerate() {
out[i] =
u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).context("invalid hex")?;
}
Ok(out)
}
fn sanitize(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect()
}
struct Rsp {
stream: TcpStream,
buf: Vec<u8>,
no_ack: bool,
}
impl Rsp {
fn new(stream: TcpStream) -> Self {
Self {
stream,
buf: Vec::with_capacity(512),
no_ack: false,
}
}
fn send(&mut self, payload: &str) {
let mut cksum: u32 = 0;
for b in payload.bytes() {
cksum = cksum.wrapping_add(b as u32);
}
let pkt = format!("${payload}#{:02x}", cksum & 0xff);
let _ = self.stream.write_all(pkt.as_bytes());
let _ = self.stream.flush();
if !self.no_ack {
let mut one = [0u8; 1];
let _ = self.stream.read_exact(&mut one);
}
}
fn recv(&mut self) -> String {
let mut one = [0u8; 1];
loop {
if self.stream.read_exact(&mut one).is_err() {
return String::new();
}
if one[0] == b'$' {
break;
}
}
self.buf.clear();
loop {
if self.stream.read_exact(&mut one).is_err() {
return String::new();
}
if one[0] == b'#' {
break;
}
self.buf.push(one[0]);
}
let mut cksum = [0u8; 2];
let _ = self.stream.read_exact(&mut cksum);
if !self.no_ack {
let _ = self.stream.write_all(b"+");
let _ = self.stream.flush();
}
let decoded = rle_decode(&self.buf);
String::from_utf8_lossy(&decoded).into_owned()
}
}
fn rle_decode(src: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(src.len() * 4);
let mut i = 0;
while i < src.len() {
let c = src[i];
if c == b'*' && i + 1 < src.len() && !out.is_empty() {
let n = (src[i + 1] as usize).saturating_sub(29);
let last = *out.last().unwrap();
for _ in 0..n {
out.push(last);
}
i += 2;
} else {
out.push(c);
i += 1;
}
}
out
}
#[cfg(test)]
mod tests {
use {super::*, std::fmt::Write as _, tempfile::tempdir};
fn bytes_to_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
write!(&mut out, "{byte:02x}").unwrap();
}
out
}
fn regs_hex(regs: [u64; REG_COUNT]) -> String {
let bytes = regs
.iter()
.flat_map(|reg| reg.to_le_bytes())
.collect::<Vec<_>>();
bytes_to_hex(&bytes)
}
#[test]
fn decode_regs_hex_reads_pc_from_r11_little_endian() {
let mut regs = [0u64; REG_COUNT];
regs[0] = 0x11;
regs[11] = 0x1122_3344_5566_7788;
let (raw, pc, cu, insn) = decode_regs_hex(®s_hex(regs)).unwrap();
assert_eq!(pc, 0x1122_3344_5566_7788);
assert_eq!(cu, 0);
assert_eq!(insn, [0; 8]);
assert_eq!(u64::from_le_bytes(raw[0..8].try_into().unwrap()), 0x11);
}
#[test]
fn decode_bytes_hex_rejects_short_or_invalid_payload() {
assert!(decode_bytes_hex::<8>("00").is_err());
assert!(decode_bytes_hex::<1>("zz").is_err());
}
#[test]
fn rle_decode_expands_gdb_rsp_runs() {
assert_eq!(rle_decode(b"A* B"), b"AAAAB".to_vec());
}
#[test]
fn regular_invocation_stems_ignores_gdb_sidecars_and_sorts_by_tx_inv() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("0002__tx1.regs"), vec![0u8; REG_BYTES]).unwrap();
std::fs::write(dir.path().join("0001__tx1.regs"), vec![0u8; REG_BYTES * 2]).unwrap();
std::fs::write(dir.path().join("0001__tx2.regs"), vec![0u8; REG_BYTES]).unwrap();
std::fs::write(dir.path().join("0001__tx1.gdb.regs"), vec![0u8; REG_BYTES]).unwrap();
let stems = regular_invocation_stems(dir.path()).unwrap();
assert_eq!(stems.len(), 3);
assert_eq!(stems[0].stem.file_name().unwrap(), "0001__tx1");
assert_eq!(stems[0].step_count, 2);
assert_eq!(stems[1].stem.file_name().unwrap(), "0002__tx1");
assert_eq!(stems[2].stem.file_name().unwrap(), "0001__tx2");
}
}