#![warn(clippy::all, clippy::pedantic, clippy::cargo)]
#![allow(clippy::missing_errors_doc)]
use std::{
borrow::Cow, collections::HashMap, fmt, num::NonZeroUsize, process::Stdio, time::Duration,
};
use breakpoint::{Breakpoint, LineSpec};
use camino::Utf8PathBuf;
use checkpoint::Checkpoint;
use frame::Frame;
use rand::Rng;
use status::Status;
use tokio::{io, process, sync::mpsc, time};
use tracing::{debug, error, info};
use variable::Variable;
pub mod address;
pub mod breakpoint;
pub mod checkpoint;
pub mod frame;
pub mod parser;
pub mod raw;
pub mod status;
mod string_stream;
pub mod symbol;
pub mod variable;
mod worker;
#[cfg(test)]
mod test_common;
#[derive(Debug, Clone, thiserror::Error, Eq, PartialEq)]
pub enum Error {
#[error(transparent)]
Gdb(#[from] GdbError),
#[error("Expected result response")]
ExpectedResultResponse,
#[error("Expected a different payload from gdb")]
ExpectedDifferentPayload,
#[error("Expected response to have a payload")]
ExpectedPayload,
#[error("Failed to parse payload value as u32")]
ParseU32(#[from] std::num::ParseIntError),
#[error("Failed to parse payload value as hex")]
ParseHex(#[from] ParseHexError),
#[error("Expected response to have message {expected}, got {actual}")]
UnexpectedResponseMessage { expected: String, actual: String },
#[error("Expected different console output in reply to command")]
ExpectedDifferentConsole,
#[error(transparent)]
Timeout(#[from] TimeoutError),
}
#[derive(Debug, Clone, thiserror::Error, Eq, PartialEq)]
#[error("Timed out waiting for a message")]
pub struct TimeoutError;
#[derive(Debug, Clone, thiserror::Error, displaydoc::Display, Eq, PartialEq)]
pub struct GdbError {
pub code: Option<String>,
pub msg: Option<String>,
}
#[derive(Debug, Clone, thiserror::Error, Eq, PartialEq)]
pub enum ParseHexError {
#[error("Expected to start with 0x")]
InvalidPrefix,
#[error(transparent)]
ParseInt(#[from] std::num::ParseIntError),
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct GdbBuilder {
is_rust: bool,
time_travel: Option<BuilderTimeTravel>,
target: Utf8PathBuf,
timeout: Duration,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum BuilderTimeTravel {
Rr,
Rd,
}
impl GdbBuilder {
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
pub fn new(target: impl Into<Utf8PathBuf>) -> Self {
Self {
is_rust: true,
time_travel: None,
timeout: Self::DEFAULT_TIMEOUT,
target: target.into(),
}
}
pub fn rr(trace_dir: impl Into<Utf8PathBuf>) -> Self {
Self {
is_rust: true,
time_travel: Some(BuilderTimeTravel::Rr),
timeout: Self::DEFAULT_TIMEOUT,
target: trace_dir.into(),
}
}
pub fn rd(trace_dir: impl Into<Utf8PathBuf>) -> Self {
Self {
is_rust: true,
time_travel: Some(BuilderTimeTravel::Rd),
timeout: Self::DEFAULT_TIMEOUT,
target: trace_dir.into(),
}
}
pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
self.timeout = timeout;
self
}
pub fn rust(&mut self, is_rust: bool) -> &mut Self {
self.is_rust = is_rust;
self
}
pub fn spawn(&self) -> io::Result<Gdb> {
info!("Spawning {:?}", self);
let mut cmd = self.time_travel.map_or_else(
|| {
let mut cmd = if self.is_rust {
process::Command::new("rust-gdb")
} else {
process::Command::new("gdb")
};
cmd.args(&["--interpreter=mi3", "--quiet", self.target.as_str()]);
cmd
},
|tt| {
let program = match tt {
BuilderTimeTravel::Rr => "rr",
BuilderTimeTravel::Rd => "rd",
};
let mut cmd = process::Command::new(program);
cmd.arg("replay");
if self.is_rust {
cmd.args(&["-d", "rust-gdb"]);
}
cmd.arg("--mark-stdio");
cmd.arg(self.target.as_str());
cmd.args(&["--", "--interpreter=mi3", "--quiet"]);
cmd
},
);
let cmd = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
Ok(Gdb::new(cmd, self.timeout))
}
}
pub struct Gdb {
worker: mpsc::UnboundedSender<worker::Msg>,
timeout: Duration,
}
impl Gdb {
pub fn spawn(target: impl Into<Utf8PathBuf>) -> io::Result<Self> {
GdbBuilder::new(target).spawn()
}
pub async fn status(&self) -> Result<Status, TimeoutError> {
let (out_tx, out_rx) = mpsc::channel(1);
self.worker_send(worker::Msg::Status(out_tx));
Self::worker_receive(out_rx, self.timeout).await
}
pub async fn next_status(
&self,
current: Status,
timeout: Option<Duration>,
) -> Result<Status, TimeoutError> {
let timeout = timeout.unwrap_or(self.timeout);
let (out_tx, out_rx) = mpsc::channel(1);
self.worker_send(worker::Msg::NextStatus {
current,
out: out_tx,
});
Self::worker_receive(out_rx, timeout).await
}
pub async fn await_stopped(
&self,
timeout: Option<Duration>,
) -> Result<status::Stopped, TimeoutError> {
if let Status::Stopped(status) = self.status().await? {
debug!("Already stopped");
return Ok(status);
}
let status = self
.await_status(|s| matches!(s, Status::Stopped(_)), timeout)
.await?;
match status {
Status::Stopped(status) => Ok(status),
_ => unreachable!(),
}
}
pub async fn await_status<P>(
&self,
pred: P,
timeout: Option<Duration>,
) -> Result<Status, TimeoutError>
where
P: Fn(&Status) -> bool + Send + Sync + 'static,
{
let timeout = timeout.unwrap_or(self.timeout);
let (out_tx, out_rx) = mpsc::channel(1);
self.worker_send(worker::Msg::AwaitStatus {
pred: Box::new(pred),
out: out_tx,
});
Self::worker_receive(out_rx, timeout).await
}
pub async fn exec_run(&self) -> Result<(), Error> {
self.raw_cmd("-exec-run")
.await?
.expect_result()?
.expect_msg_is("running")
}
pub async fn exec_continue(&self) -> Result<(), Error> {
self.raw_cmd("-exec-continue")
.await?
.expect_result()?
.expect_msg_is("running")
}
pub async fn exec_continue_reverse(&self) -> Result<(), Error> {
self.raw_cmd("-exec-continue --reverse")
.await?
.expect_result()?
.expect_msg_is("running")
}
pub async fn exec_finish(&self) -> Result<(), Error> {
self.raw_cmd("-exec-finish")
.await?
.expect_result()?
.expect_msg_is("running")
}
pub async fn exec_finish_reverse(&self) -> Result<(), Error> {
self.raw_cmd("-exec-finish --reverse")
.await?
.expect_result()?
.expect_msg_is("running")
}
pub async fn exec_step(&self) -> Result<(), Error> {
self.raw_cmd("-exec-step")
.await?
.expect_result()?
.expect_msg_is("running")
}
pub async fn exec_step_reverse(&self) -> Result<(), Error> {
self.raw_cmd("-exec-step --reverse")
.await?
.expect_result()?
.expect_msg_is("running")
}
pub async fn break_insert(&self, at: LineSpec) -> Result<Breakpoint, Error> {
let raw = self
.raw_cmd(format!("-break-insert {}", at.serialize()))
.await?
.expect_result()?
.expect_payload()?
.remove_expect("bkpt")?
.expect_dict()?;
Breakpoint::from_raw(raw)
}
pub async fn break_disable<'a, I>(&self, breakpoints: I) -> Result<(), Error>
where
I: IntoIterator<Item = &'a Breakpoint>,
{
let mut raw = String::new();
for bp in breakpoints {
raw.push_str(&format!("{} ", bp.number));
}
self.raw_cmd(format!("-break-disable {}", raw))
.await?
.expect_result()?
.expect_msg_is("done")
}
pub async fn break_delete<'a, I>(&self, breakpoints: I) -> Result<(), Error>
where
I: IntoIterator<Item = &'a Breakpoint>,
{
let mut raw = String::new();
for bp in breakpoints {
raw.push_str(&format!("{} ", bp.number));
}
self.raw_cmd(format!("-break-delete {}", raw))
.await?
.expect_result()?
.expect_msg_is("done")
}
pub async fn enable_filter_frames(&self) -> Result<(), Error> {
self.raw_cmd("-enable-frame-filters")
.await?
.expect_result()?
.expect_msg_is("done")
}
pub async fn stack_depth(&self, max: Option<u32>) -> Result<u32, Error> {
let msg = if let Some(max) = max {
Cow::Owned(format!("-stack-info-depth {}", max))
} else {
Cow::Borrowed("-stack-info-depth")
};
self.raw_cmd(msg)
.await?
.expect_result()?
.expect_payload()?
.remove_expect("depth")?
.expect_number()
}
pub async fn stack_list_variables(&self, frame_filters: bool) -> Result<Vec<Variable>, Error> {
let msg = if frame_filters {
"-stack-list-variables --simple-values"
} else {
"-stack-list-variables --no-frame-filters --simple-values"
};
let payload = self.raw_cmd(msg).await?.expect_result()?.expect_payload()?;
variable::from_stack_list(payload)
}
pub async fn stack_info_frame(&self) -> Result<Frame, Error> {
let raw = self
.raw_cmd("-stack-info-frame")
.await?
.expect_result()?
.expect_payload()?
.remove_expect("frame")?
.expect_dict()?;
Frame::from_dict(raw)
}
pub async fn symbol_info_functions(
&self,
) -> Result<HashMap<Utf8PathBuf, Vec<symbol::Function>>, Error> {
let payload = self
.raw_cmd("-symbol-info-functions")
.await?
.expect_result()?
.expect_payload()?;
symbol::from_symbol_info_functions_payload(payload)
}
pub async fn symbol_info_functions_re(
&self,
name_regex: &str,
) -> Result<HashMap<Utf8PathBuf, Vec<symbol::Function>>, Error> {
let payload = self
.raw_cmd(format!(
"-symbol-info-functions --name {}",
escape_arg(name_regex)
))
.await?
.expect_result()?
.expect_payload()?;
symbol::from_symbol_info_functions_payload(payload)
}
pub async fn save_checkpoint(&self) -> Result<Checkpoint, Error> {
let (resp, lines) = self.raw_console_cmd_for_output("checkpoint", 1).await?;
resp.expect_result()?.expect_msg_is("done")?;
checkpoint::parse_save_line(&lines[0])
}
pub async fn goto_checkpoint(&self, checkpoint: Checkpoint) -> Result<(), Error> {
self.raw_console_cmd(format!("restart {}", checkpoint.0))
.await?
.expect_result()?
.expect_msg_is("running")
}
pub async fn raw_cmd(&self, msg: impl Into<String>) -> Result<raw::Response, Error> {
let token = Token::generate();
let (out_tx, out_rx) = mpsc::channel(1);
self.worker_send(worker::Msg::Cmd {
token,
msg: msg.into(),
out: out_tx,
});
Self::worker_receive(out_rx, self.timeout).await?
}
pub async fn raw_console_cmd(&self, msg: impl Into<String>) -> Result<raw::Response, Error> {
let msg = msg.into();
let msg = format!("-interpreter-exec console {}", escape_arg(msg));
self.raw_cmd(msg).await
}
pub async fn raw_console_cmd_for_output(
&self,
msg: impl AsRef<str>,
capture_lines: usize,
) -> Result<(raw::Response, Vec<String>), Error> {
let msg = format!("-interpreter-exec console {}", escape_arg(msg));
let capture_lines = NonZeroUsize::new(capture_lines).expect("capture_lines nonzero");
self.await_ready().await?;
let token = Token::generate();
let (out_tx, out_rx) = mpsc::channel(1);
self.worker_send(worker::Msg::ConsoleCmd {
token,
msg,
out: out_tx,
capture_lines,
});
Self::worker_receive(out_rx, self.timeout).await?
}
pub async fn await_ready(&self) -> Result<(), Error> {
self.raw_cmd("-list-target-features").await?;
Ok(())
}
pub async fn pop_general(&self) -> Result<Vec<raw::GeneralMessage>, TimeoutError> {
let (out_tx, out_rx) = mpsc::channel(1);
self.worker_send(worker::Msg::PopGeneral(out_tx));
Self::worker_receive(out_rx, self.timeout).await
}
#[must_use]
pub fn new(cmd: process::Child, timeout: Duration) -> Self {
let worker = worker::spawn(cmd);
Self { worker, timeout }
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.timeout = timeout;
}
fn worker_send(&self, msg: worker::Msg) {
self.worker.send(msg).expect("Can send to mainloop");
}
async fn worker_receive<O: std::fmt::Debug>(
mut rx: mpsc::Receiver<O>,
timeout: Duration,
) -> Result<O, TimeoutError> {
time::timeout(timeout, rx.recv())
.await
.map(|o| o.expect("out chan not closed"))
.map_err(|_| TimeoutError)
}
}
impl fmt::Debug for Gdb {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Gdb").finish() }
}
fn escape_arg(arg: impl AsRef<str>) -> String {
let arg = arg.as_ref();
let mut out = String::with_capacity(arg.len() + 2);
out.push('"');
for c in arg.chars() {
if c == '"' {
out.push('\\');
out.push('"');
} else {
out.push(c);
}
}
out.push('"');
out
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
struct Token(u32);
impl Token {
fn generate() -> Self {
Self(rand::thread_rng().gen())
}
pub(crate) fn serialize(self) -> String {
format!("{}", self.0)
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, iter};
use crate::status::{ExitReason, StopReason};
use super::*;
use insta::assert_debug_snapshot;
use pretty_assertions::assert_eq;
use test_common::{build_hello_world, init, Result};
fn fixture() -> eyre::Result<Gdb> {
init();
let bin = build_hello_world();
Ok(Gdb::spawn(bin)?)
}
#[cfg(feature = "test_rr")]
fn rr_fixture() -> eyre::Result<Gdb> {
init();
let trace = crate::test_common::record_hello_world();
Ok(GdbBuilder::rr(trace).spawn()?)
}
#[tokio::test]
async fn test_enable_filter_frames() -> Result {
let subject = fixture()?;
subject.enable_filter_frames().await?;
Ok(())
}
#[tokio::test]
async fn test_exec_finish() -> Result {
let subject = fixture()?;
subject
.break_insert(LineSpec::function("hello_world::HelloMsg::say"))
.await?;
subject.exec_run().await?;
subject.await_stopped(None).await?;
subject.exec_finish().await?;
subject.await_stopped(None).await?;
Ok(())
}
#[tokio::test]
async fn test_exec_step() -> Result {
let subject = fixture()?;
subject
.break_insert(LineSpec::function("hello_world::main"))
.await?;
subject.exec_run().await?;
subject.await_stopped(None).await?;
subject.exec_step().await?;
subject.await_stopped(None).await?;
Ok(())
}
#[cfg(feature = "test_rr")]
#[tokio::test]
async fn test_exec_step_reverse() -> Result {
let subject = rr_fixture()?;
subject
.break_insert(LineSpec::function("hello_world::main"))
.await?;
subject.exec_run().await?;
subject.exec_continue().await?;
subject.await_stopped(None).await?;
subject.exec_step().await?;
subject.await_stopped(None).await?;
subject.exec_step_reverse().await?;
subject.await_stopped(None).await?;
Ok(())
}
#[cfg(feature = "test_rr")]
#[tokio::test]
async fn test_exec_finish_reverse() -> Result {
let subject = rr_fixture()?;
subject
.break_insert(LineSpec::function("hello_world::HelloMsg::say"))
.await?;
subject.exec_run().await?;
subject.exec_continue().await?;
subject.await_stopped(None).await?;
subject.exec_finish().await?;
subject.await_stopped(None).await?;
subject.exec_step_reverse().await?;
subject.exec_finish_reverse().await?;
subject.await_stopped(None).await?;
Ok(())
}
#[tokio::test]
async fn test_gdb_builders() -> Result {
let target = build_hello_world();
let timeout = Duration::from_secs(0);
GdbBuilder::new(&target).spawn()?;
GdbBuilder::new(&target).rust(false).spawn()?;
GdbBuilder::new(&target).timeout(timeout).spawn()?;
GdbBuilder::new(&target)
.rust(false)
.timeout(timeout)
.spawn()?;
Ok(())
}
#[cfg(feature = "test_rd")]
#[tokio::test]
async fn test_rd_builders() -> Result {
let trace = test_common::record_hello_world();
let timeout = Duration::from_secs(0);
GdbBuilder::rd(&trace).spawn()?;
GdbBuilder::rd(&trace).rust(false).spawn()?;
GdbBuilder::rd(&trace).timeout(timeout).spawn()?;
GdbBuilder::rd(&trace)
.rust(false)
.timeout(timeout)
.spawn()?;
Ok(())
}
#[cfg(feature = "test_rr")]
#[tokio::test]
async fn test_rr_builders() -> Result {
let trace = test_common::record_hello_world();
let timeout = Duration::from_secs(0);
GdbBuilder::rr(&trace).spawn()?;
GdbBuilder::rr(&trace).rust(false).spawn()?;
GdbBuilder::rr(&trace).timeout(timeout).spawn()?;
GdbBuilder::rr(&trace)
.rust(false)
.timeout(timeout)
.spawn()?;
Ok(())
}
#[tokio::test]
async fn test_stack() -> Result {
let subject = fixture()?;
subject
.break_insert(LineSpec::function("hello_world::HelloMsg::say"))
.await?;
subject.exec_run().await?;
subject.await_stopped(None).await?;
assert_eq!(2, subject.stack_depth(None).await?);
let vars = subject.stack_list_variables(false).await?;
assert_eq!(1, vars.len());
assert_eq!("self", vars[0].name);
assert_eq!("*mut hello_world::HelloMsg", vars[0].var_type);
assert!(vars[0].value.is_some());
let frame = subject.stack_info_frame().await?;
assert_eq!(0, frame.level);
assert_eq!("hello_world::HelloMsg::say", frame.function.unwrap());
assert!(frame.file.unwrap().ends_with("src/main.rs"));
assert_eq!(Some(11), frame.line);
Ok(())
}
#[cfg(feature = "test_rr")]
#[tokio::test]
async fn test_checkpoint() -> Result {
let subject = rr_fixture()?;
subject
.break_insert(LineSpec::function("hello_world::main"))
.await?;
subject.exec_continue().await?;
let status_at_check = subject.await_stopped(None).await?;
assert!(matches!(
&status_at_check.reason,
&Some(StopReason::Breakpoint { .. })
));
let addr_at_check = status_at_check.address;
let check = subject.save_checkpoint().await?;
assert_eq!(Checkpoint(1), check);
subject.exec_continue().await?;
subject
.await_status(|s| matches!(s, &Status::Stopped { .. }), None)
.await?;
subject.goto_checkpoint(check).await?;
assert_eq!(addr_at_check, subject.await_stopped(None).await?.address);
subject.exec_continue().await?;
assert_eq!(
Some(StopReason::SignalReceived),
subject.await_stopped(None).await?.reason
);
Ok(())
}
#[tokio::test]
async fn test_raw_console_for_out() -> Result {
let subject = fixture()?;
subject
.break_insert(LineSpec::function("hello_world::main"))
.await?;
subject.exec_run().await?;
let (resp, lines) = subject.raw_console_cmd_for_output("info locals", 1).await?;
resp.expect_result()?.expect_msg_is("done")?;
assert_eq!(vec!["No locals.\\n"], lines);
Ok(())
}
#[tokio::test]
async fn test_next_status_when_wrong_about_current() -> Result {
let subject = fixture()?;
subject.exec_run().await?;
let status = subject.next_status(Status::Unstarted, None).await?;
assert_eq!(Status::Running, status);
Ok(())
}
#[tokio::test]
async fn test_next_status_when_correct_about_current() -> Result {
let subject = fixture()?;
subject.exec_run().await?;
let status = subject.next_status(Status::Running, None).await?;
assert_eq!(status, Status::Exited(ExitReason::Normal));
Ok(())
}
#[tokio::test]
async fn test_status_through_break_continue() -> Result {
let subject = fixture()?;
let status = subject.status().await?;
assert_eq!(Status::Unstarted, status);
subject.break_insert(LineSpec::function("main")).await?;
subject.exec_run().await?;
let status = subject.next_status(status, None).await?;
assert_eq!(Status::Running, status);
let status = subject.next_status(status, None).await?;
assert!(matches!(
&status,
&Status::Stopped(status::Stopped {
reason: Some(StopReason::Breakpoint { number }),
function: Some(ref function),
..
}) if number == 1 && function == "main"
));
subject.exec_continue().await?;
let status = subject.next_status(status, None).await?;
assert_eq!(Status::Running, status);
let status = subject.next_status(status, None).await?;
assert_eq!(status, Status::Exited(ExitReason::Normal));
Ok(())
}
#[tokio::test]
async fn test_break() -> Result {
let subject = fixture()?;
let bp = subject
.break_insert(LineSpec::line("samples/hello_world/src/main.rs", 13))
.await?;
assert_eq!(1, bp.number);
assert!(bp
.file
.as_ref()
.unwrap()
.ends_with("samples/hello_world/src/main.rs"));
assert_eq!(17, bp.line.unwrap());
assert_eq!(0, bp.times);
subject.break_disable(iter::once(&bp)).await?;
subject.break_delete(iter::once(&bp)).await?;
Ok(())
}
#[tokio::test]
async fn test_exec_continue() -> Result {
let subject = fixture()?;
subject.break_insert(LineSpec::function("main")).await?;
subject.exec_run().await?;
subject.exec_continue().await?;
Ok(())
}
#[tokio::test]
async fn test_exec_continue_not_running() -> Result {
let subject = fixture()?;
let error = match subject.exec_continue().await {
Err(Error::Gdb(error)) => error,
got => panic!("Expected Error::Gdb, got {:?}", got),
};
assert_eq!(error.msg.unwrap(), "The program is not being run.");
Ok(())
}
#[tokio::test]
async fn test_exec_run() -> Result {
let subject = fixture()?;
subject.exec_run().await?;
Ok(())
}
#[tokio::test]
async fn test_symbol_info_function() -> Result {
let subject = fixture()?;
let symbols: BTreeMap<_, _> = subject.symbol_info_functions().await?.into_iter().collect();
assert_debug_snapshot!(symbols);
Ok(())
}
#[tokio::test]
async fn test_await_ready() -> Result {
let subject = fixture()?;
subject.await_ready().await?;
Ok(())
}
#[tokio::test]
async fn test_pop_general() -> Result {
let subject = fixture()?;
subject.raw_cmd("-gdb-version").await?;
let general = subject.pop_general().await?;
assert!(!general.is_empty());
Ok(())
}
#[tokio::test]
async fn test_invalid_command() -> Result {
let subject = fixture()?;
let err = subject.raw_cmd("-invalid-command").await.unwrap_err();
assert_eq!(
Error::Gdb(GdbError {
code: Some("undefined-command".into()),
msg: Some("Undefined MI command: invalid-command".into()),
}),
err
);
Ok(())
}
}