1use {
30 anyhow::{anyhow, Context, Result},
31 std::{
32 io::{BufRead, BufReader, Read, Write},
33 net::TcpStream,
34 os::unix::net::{UnixListener, UnixStream},
35 path::{Path, PathBuf},
36 sync::{
37 atomic::{AtomicBool, Ordering},
38 Arc, Mutex,
39 },
40 thread,
41 time::{Duration, Instant},
42 },
43};
44
45const REG_COUNT: usize = 12;
53const REG_BYTES: usize = REG_COUNT * 8;
54const REG_HEX_LEN: usize = REG_BYTES * 2;
55
56pub const SOCKET_ENV: &str = "ANCHOR_GDB_SOCKET";
61
62pub struct GdbDriver {
70 profile_dir: PathBuf,
71 sock_path: PathBuf,
72 stop: Arc<AtomicBool>,
73 accept: Option<thread::JoinHandle<Result<()>>>,
74 captures: Arc<Mutex<Vec<PendingCapture>>>,
75}
76
77impl GdbDriver {
78 pub fn sock_path(&self) -> &Path {
79 &self.sock_path
80 }
81
82 fn shutdown(&mut self) {
83 self.stop.store(true, Ordering::Release);
84 if let Some(h) = self.accept.take() {
85 let _ = h.join();
86 }
87 if let Ok(captures) = self.captures.lock() {
88 if let Err(e) = write_pending_captures(&self.profile_dir, &captures) {
89 eprintln!("gdb sidecar write error: {e}");
90 }
91 }
92 let _ = std::fs::remove_file(&self.sock_path);
93 }
94}
95
96impl Drop for GdbDriver {
97 fn drop(&mut self) {
98 self.shutdown();
99 }
100}
101
102pub fn start_gdb_driver(profile_dir: &Path) -> Result<GdbDriver> {
106 std::fs::create_dir_all(profile_dir).ok();
109 let sock_path = profile_dir.join("gdb.sock");
110 let _ = std::fs::remove_file(&sock_path); let listener =
112 UnixListener::bind(&sock_path).with_context(|| format!("bind {}", sock_path.display()))?;
113
114 let stop = Arc::new(AtomicBool::new(false));
115 let captures = Arc::new(Mutex::new(Vec::new()));
116
117 let accept_stop = Arc::clone(&stop);
120 let profile_dir_for_accept = profile_dir.to_path_buf();
121 let captures_for_accept = Arc::clone(&captures);
122 let accept_handle = thread::spawn(move || -> Result<()> {
123 listener
126 .set_nonblocking(true)
127 .context("set listener non-blocking")?;
128 let mut drivers: Vec<thread::JoinHandle<()>> = Vec::new();
129 while !accept_stop.load(Ordering::Acquire) {
130 match listener.accept() {
131 Ok((conn, _addr)) => {
132 let pd = profile_dir_for_accept.clone();
133 let captures = Arc::clone(&captures_for_accept);
134 drivers.push(thread::spawn(move || {
135 if let Err(e) = handle_announce(conn, &pd, captures) {
136 eprintln!("gdb driver error: {e}");
137 }
138 }));
139 drivers.retain(|h| !h.is_finished());
142 }
143 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
144 thread::sleep(Duration::from_millis(20));
145 }
146 Err(e) => return Err(e).context("accept"),
147 }
148 }
149 for d in drivers {
152 let _ = d.join();
153 }
154 Ok(())
155 });
156
157 Ok(GdbDriver {
158 profile_dir: profile_dir.to_path_buf(),
159 sock_path,
160 stop,
161 accept: Some(accept_handle),
162 captures,
163 })
164}
165
166#[allow(clippy::too_many_arguments)]
176pub fn run_gdb_mode(
177 cargo_cwd: &Path,
178 current_package: Option<&str>,
179 profile_feature: &str,
180 profile_dir: &Path,
181 test_filter: Option<&str>,
182) -> Result<()> {
183 let driver = start_gdb_driver(profile_dir)?;
184
185 let mut cmd = std::process::Command::new("cargo");
186 cmd.current_dir(cargo_cwd)
187 .env(SOCKET_ENV, driver.sock_path())
188 .env("ANCHOR_PROFILE_DIR", profile_dir)
189 .env("RUST_TEST_THREADS", "1")
190 .arg("test")
191 .arg("--features")
192 .arg(profile_feature);
193 if let Some(pkg) = current_package {
194 cmd.arg("-p").arg(pkg);
195 }
196 cmd.arg("--").arg("--test-threads=1");
197 if let Some(filter) = test_filter {
198 cmd.arg(filter);
199 }
200 let test_status = cmd.status().context("spawn cargo test")?;
201
202 drop(driver);
203
204 if !test_status.success() {
205 return Err(anyhow!("cargo test failed"));
206 }
207 Ok(())
208}
209
210fn handle_announce(
214 conn: UnixStream,
215 profile_dir: &Path,
216 captures: Arc<Mutex<Vec<PendingCapture>>>,
217) -> Result<()> {
218 let mut rdr = BufReader::new(conn);
219 let mut line = String::new();
220 rdr.read_line(&mut line).context("read announce")?;
221 let line = line.trim_end();
222 let (port_str, test_name) = line
223 .split_once('\t')
224 .ok_or_else(|| anyhow!("malformed announce: {line:?}"))?;
225 let port: u16 = port_str.parse().context("parse port")?;
226
227 let test_dir = profile_dir.join(sanitize(test_name));
228 std::fs::create_dir_all(&test_dir).with_context(|| format!("create {}", test_dir.display()))?;
229
230 let mut deadline = Duration::from_secs(5);
235 while let Some(outer) = wait_for_connect(port, deadline) {
236 let capture = drive_session(outer, port, test_name, 0)?;
237 captures.lock().unwrap().push(PendingCapture {
238 test_name: sanitize(test_name),
239 capture,
240 });
241 deadline = Duration::from_millis(750);
242 }
243 Ok(())
244}
245
246struct PendingCapture {
247 test_name: String,
248 capture: CapturedInvocation,
249}
250
251struct CapturedInvocation {
252 regs: Vec<u8>,
253 insns: Vec<u8>,
254 cu: Vec<u8>,
255 children: Vec<CapturedInvocation>,
256}
257
258fn write_pending_captures(profile_dir: &Path, captures: &[PendingCapture]) -> Result<()> {
259 let mut by_test = std::collections::BTreeMap::<&str, Vec<&CapturedInvocation>>::new();
260 for pending in captures {
261 by_test
262 .entry(&pending.test_name)
263 .or_default()
264 .push(&pending.capture);
265 }
266
267 for (test_name, captures) in by_test {
268 let test_dir = profile_dir.join(test_name);
269 std::fs::create_dir_all(&test_dir)
270 .with_context(|| format!("create {}", test_dir.display()))?;
271
272 let mut flattened = Vec::new();
273 for capture in captures {
274 flatten_capture_postorder(capture, &mut flattened);
275 }
276
277 let mut regular = regular_invocation_stems(&test_dir)?;
278 for capture in flattened {
279 let step_count = capture.regs.len() / REG_BYTES;
280 let regular_idx = regular
281 .iter()
282 .position(|r| !r.used && r.step_count == step_count)
283 .or_else(|| regular.iter().position(|r| !r.used));
284 let stem = if let Some(idx) = regular_idx {
285 regular[idx].used = true;
286 regular[idx].stem.clone()
287 } else {
288 let fallback = regular.len() + 1;
289 test_dir.join(format!("{fallback:04}__tx1"))
290 };
291 write_capture_sidecars(capture, &stem)?;
292 }
293 }
294 Ok(())
295}
296
297fn flatten_capture_postorder<'a>(
298 capture: &'a CapturedInvocation,
299 out: &mut Vec<&'a CapturedInvocation>,
300) {
301 for child in &capture.children {
302 flatten_capture_postorder(child, out);
303 }
304 out.push(capture);
305}
306
307struct RegularInvocationStem {
308 stem: PathBuf,
309 step_count: usize,
310 used: bool,
311}
312
313fn regular_invocation_stems(test_dir: &Path) -> Result<Vec<RegularInvocationStem>> {
314 let mut found = Vec::new();
315 if !test_dir.exists() {
316 return Ok(Vec::new());
317 }
318
319 for entry in std::fs::read_dir(test_dir)? {
320 let path = entry?.path();
321 if path.extension().and_then(|e| e.to_str()) != Some("regs") {
322 continue;
323 }
324 let Some(stem_str) = path.file_stem().and_then(|s| s.to_str()) else {
325 continue;
326 };
327 if stem_str.ends_with(".gdb") {
328 continue;
329 }
330 let Some((inv_part, tx_part)) = stem_str.split_once("__") else {
331 continue;
332 };
333 let Some(tx_digits) = tx_part.strip_prefix("tx") else {
334 continue;
335 };
336 let Ok(inv_seq) = inv_part.parse::<u32>() else {
337 continue;
338 };
339 let Ok(tx_seq) = tx_digits.parse::<u32>() else {
340 continue;
341 };
342 let step_count = std::fs::metadata(&path)
343 .map(|m| m.len() as usize / REG_BYTES)
344 .unwrap_or(0);
345 found.push((
346 tx_seq,
347 inv_seq,
348 RegularInvocationStem {
349 stem: path.with_extension(""),
350 step_count,
351 used: false,
352 },
353 ));
354 }
355
356 found.sort_by_key(|(tx_seq, inv_seq, _)| (*tx_seq, *inv_seq));
357 Ok(found.into_iter().map(|(_, _, stem)| stem).collect())
358}
359
360fn write_capture_sidecars(capture: &CapturedInvocation, stem: &Path) -> Result<()> {
361 std::fs::write(stem.with_extension("gdb.regs"), &capture.regs)?;
362 std::fs::write(stem.with_extension("gdb.insns"), &capture.insns)?;
363 std::fs::write(stem.with_extension("gdb.cu"), &capture.cu)?;
364 Ok(())
365}
366
367fn drive_session(
372 stream: TcpStream,
373 port: u16,
374 test_name: &str,
375 depth: usize,
376) -> Result<CapturedInvocation> {
377 stream.set_nodelay(true).ok();
378 let mut rsp = Rsp::new(stream);
379 let started = Instant::now();
380 let mut last_progress = started;
381
382 rsp.send("QStartNoAckMode");
383 let reply = rsp.recv();
384 rsp.no_ack = reply == "OK";
385
386 rsp.send("qSupported:multiprocess+;swbreak+;hwbreak+;vContSupported+");
387 let _ = rsp.recv();
388
389 rsp.send("?");
390 let mut reply = rsp.recv();
391
392 let mut capture = CapturedInvocation {
396 regs: Vec::new(),
397 insns: Vec::new(),
398 cu: Vec::new(),
399 children: Vec::new(),
400 };
401
402 let mut steps: u64 = 0;
403 print_progress(test_name, port, depth, steps, started, false);
404 loop {
405 if reply.starts_with('W') || reply.starts_with('X') {
406 break;
407 }
408 if !reply.starts_with('T') && !reply.starts_with('S') {
409 eprintln!("unexpected stop reply: {reply}");
410 break;
411 }
412
413 rsp.send("g");
414 let regs_hex = rsp.recv();
415 if regs_hex.is_empty() {
416 break;
417 }
418 rsp.send("p0c");
421 let cu_hex = rsp.recv();
422
423 let (regs, pc, _cu_stub, insn) = decode_regs_hex(®s_hex)?;
424 let cu = decode_u64_hex(&cu_hex).unwrap_or(0);
425 capture.regs.extend_from_slice(®s);
426 capture.cu.extend_from_slice(&cu.to_le_bytes());
427 let insn_bytes = read_insn_at(&mut rsp, pc)?;
428 capture.insns.extend_from_slice(&insn_bytes);
429 let _ = insn;
430
431 let nested_port = port;
435 let nested_test_name = test_name.to_owned();
436 let probe_cancel = Arc::new(AtomicBool::new(false));
437 let probe_cancel_for_thread = Arc::clone(&probe_cancel);
438 let probe = thread::spawn(move || -> Result<Option<CapturedInvocation>> {
439 if let Some(inner) = probe_for_nested(
440 nested_port,
441 Duration::from_millis(250),
442 &probe_cancel_for_thread,
443 ) {
444 return drive_session(inner, nested_port, &nested_test_name, depth + 1).map(Some);
445 }
446 Ok(None)
447 });
448
449 rsp.send("s");
450 let step_reply = rsp.recv();
451 probe_cancel.store(true, Ordering::Release);
452 if let Ok(Ok(Some(child))) = probe.join() {
453 capture.children.push(child);
454 }
455 if step_reply.is_empty() {
456 break;
458 }
459
460 reply = step_reply;
461 steps += 1;
462 if last_progress.elapsed() >= Duration::from_secs(5) {
463 print_progress(test_name, port, depth, steps, started, false);
464 last_progress = Instant::now();
465 }
466 }
467 print_progress(test_name, port, depth, steps, started, true);
468
469 Ok(capture)
470}
471
472fn print_progress(
473 test_name: &str,
474 port: u16,
475 depth: usize,
476 steps: u64,
477 started: Instant,
478 done: bool,
479) {
480 let elapsed = started.elapsed().as_secs_f64().max(0.001);
481 let rate = steps as f64 / elapsed;
482 let indent = " ".repeat(depth);
483 let status = if done { "captured" } else { "stepping" };
484 eprintln!(
485 "{indent}gdb {status} {test_name} :{port} depth={depth} steps={steps} elapsed={:.1}s \
486 rate={:.1}/s",
487 elapsed, rate,
488 );
489}
490
491fn wait_for_connect(port: u16, deadline: Duration) -> Option<TcpStream> {
492 let start = Instant::now();
493 while start.elapsed() < deadline {
494 if let Ok(s) = TcpStream::connect_timeout(
495 &format!("127.0.0.1:{port}").parse().ok()?,
496 Duration::from_millis(50),
497 ) {
498 return Some(s);
499 }
500 thread::sleep(Duration::from_millis(5));
501 }
502 None
503}
504
505fn probe_for_nested(port: u16, window: Duration, cancel: &AtomicBool) -> Option<TcpStream> {
506 let start = Instant::now();
507 while start.elapsed() < window && !cancel.load(Ordering::Acquire) {
508 if let Ok(s) = TcpStream::connect(("127.0.0.1", port)) {
509 return Some(s);
510 }
511 thread::yield_now();
512 }
513 None
514}
515
516fn decode_regs_hex(hex: &str) -> Result<([u8; REG_BYTES], u64, u64, [u8; 8])> {
517 if hex.len() < REG_HEX_LEN {
518 return Err(anyhow!(
519 "register hex too short: {} bytes, payload={hex:?}",
520 hex.len()
521 ));
522 }
523 let mut regs = [0u8; REG_BYTES];
524 for (i, pair) in hex.as_bytes()[..REG_HEX_LEN].chunks_exact(2).enumerate() {
525 let s = std::str::from_utf8(pair).unwrap();
526 regs[i] = u8::from_str_radix(s, 16).context("invalid hex")?;
527 }
528 let pc = u64::from_le_bytes(regs[11 * 8..12 * 8].try_into().unwrap());
530 let cu = 0; let insn = [0u8; 8]; Ok((regs, pc, cu, insn))
533}
534
535fn decode_u64_hex(hex: &str) -> Option<u64> {
536 if hex.len() < 16 {
537 return None;
538 }
539 let mut bytes = [0u8; 8];
540 for (i, pair) in hex.as_bytes()[..16].chunks_exact(2).enumerate() {
541 bytes[i] = u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()?;
542 }
543 Some(u64::from_le_bytes(bytes))
544}
545
546fn read_insn_at(rsp: &mut Rsp, pc: u64) -> Result<[u8; 8]> {
547 rsp.send(&format!("m{pc:x},8"));
550 decode_bytes_hex::<8>(&rsp.recv())
551}
552
553fn decode_bytes_hex<const N: usize>(hex: &str) -> Result<[u8; N]> {
554 if hex.len() < N * 2 {
555 return Err(anyhow!(
556 "memory hex too short: {} bytes, payload={hex:?}",
557 hex.len()
558 ));
559 }
560 let mut out = [0u8; N];
561 for (i, pair) in hex.as_bytes()[..N * 2].chunks_exact(2).enumerate() {
562 out[i] =
563 u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).context("invalid hex")?;
564 }
565 Ok(out)
566}
567
568fn sanitize(s: &str) -> String {
569 s.chars()
570 .map(|c| {
571 if c.is_ascii_alphanumeric() || c == '_' {
572 c
573 } else {
574 '_'
575 }
576 })
577 .collect()
578}
579
580struct Rsp {
584 stream: TcpStream,
585 buf: Vec<u8>,
586 no_ack: bool,
587}
588
589impl Rsp {
590 fn new(stream: TcpStream) -> Self {
591 Self {
592 stream,
593 buf: Vec::with_capacity(512),
594 no_ack: false,
595 }
596 }
597
598 fn send(&mut self, payload: &str) {
599 let mut cksum: u32 = 0;
600 for b in payload.bytes() {
601 cksum = cksum.wrapping_add(b as u32);
602 }
603 let pkt = format!("${payload}#{:02x}", cksum & 0xff);
604 let _ = self.stream.write_all(pkt.as_bytes());
605 let _ = self.stream.flush();
606 if !self.no_ack {
607 let mut one = [0u8; 1];
608 let _ = self.stream.read_exact(&mut one);
609 }
610 }
611
612 fn recv(&mut self) -> String {
613 let mut one = [0u8; 1];
614 loop {
615 if self.stream.read_exact(&mut one).is_err() {
616 return String::new();
617 }
618 if one[0] == b'$' {
619 break;
620 }
621 }
622 self.buf.clear();
623 loop {
624 if self.stream.read_exact(&mut one).is_err() {
625 return String::new();
626 }
627 if one[0] == b'#' {
628 break;
629 }
630 self.buf.push(one[0]);
631 }
632 let mut cksum = [0u8; 2];
633 let _ = self.stream.read_exact(&mut cksum);
634 if !self.no_ack {
635 let _ = self.stream.write_all(b"+");
636 let _ = self.stream.flush();
637 }
638 let decoded = rle_decode(&self.buf);
643 String::from_utf8_lossy(&decoded).into_owned()
644 }
645}
646
647fn rle_decode(src: &[u8]) -> Vec<u8> {
648 let mut out = Vec::with_capacity(src.len() * 4);
652 let mut i = 0;
653 while i < src.len() {
654 let c = src[i];
655 if c == b'*' && i + 1 < src.len() && !out.is_empty() {
656 let n = (src[i + 1] as usize).saturating_sub(29);
657 let last = *out.last().unwrap();
658 for _ in 0..n {
659 out.push(last);
660 }
661 i += 2;
662 } else {
663 out.push(c);
664 i += 1;
665 }
666 }
667 out
668}
669
670#[cfg(test)]
671mod tests {
672 use {super::*, std::fmt::Write as _, tempfile::tempdir};
673
674 fn bytes_to_hex(bytes: &[u8]) -> String {
675 let mut out = String::with_capacity(bytes.len() * 2);
676 for byte in bytes {
677 write!(&mut out, "{byte:02x}").unwrap();
678 }
679 out
680 }
681
682 fn regs_hex(regs: [u64; REG_COUNT]) -> String {
683 let bytes = regs
684 .iter()
685 .flat_map(|reg| reg.to_le_bytes())
686 .collect::<Vec<_>>();
687 bytes_to_hex(&bytes)
688 }
689
690 #[test]
691 fn decode_regs_hex_reads_pc_from_r11_little_endian() {
692 let mut regs = [0u64; REG_COUNT];
693 regs[0] = 0x11;
694 regs[11] = 0x1122_3344_5566_7788;
695
696 let (raw, pc, cu, insn) = decode_regs_hex(®s_hex(regs)).unwrap();
697
698 assert_eq!(pc, 0x1122_3344_5566_7788);
699 assert_eq!(cu, 0);
700 assert_eq!(insn, [0; 8]);
701 assert_eq!(u64::from_le_bytes(raw[0..8].try_into().unwrap()), 0x11);
702 }
703
704 #[test]
705 fn decode_bytes_hex_rejects_short_or_invalid_payload() {
706 assert!(decode_bytes_hex::<8>("00").is_err());
707 assert!(decode_bytes_hex::<1>("zz").is_err());
708 }
709
710 #[test]
711 fn rle_decode_expands_gdb_rsp_runs() {
712 assert_eq!(rle_decode(b"A* B"), b"AAAAB".to_vec());
713 }
714
715 #[test]
716 fn regular_invocation_stems_ignores_gdb_sidecars_and_sorts_by_tx_inv() {
717 let dir = tempdir().unwrap();
718 std::fs::write(dir.path().join("0002__tx1.regs"), vec![0u8; REG_BYTES]).unwrap();
719 std::fs::write(dir.path().join("0001__tx1.regs"), vec![0u8; REG_BYTES * 2]).unwrap();
720 std::fs::write(dir.path().join("0001__tx2.regs"), vec![0u8; REG_BYTES]).unwrap();
721 std::fs::write(dir.path().join("0001__tx1.gdb.regs"), vec![0u8; REG_BYTES]).unwrap();
722
723 let stems = regular_invocation_stems(dir.path()).unwrap();
724
725 assert_eq!(stems.len(), 3);
726 assert_eq!(stems[0].stem.file_name().unwrap(), "0001__tx1");
727 assert_eq!(stems[0].step_count, 2);
728 assert_eq!(stems[1].stem.file_name().unwrap(), "0002__tx1");
729 assert_eq!(stems[2].stem.file_name().unwrap(), "0001__tx2");
730 }
731}