use alloc::{
string::{String, ToString},
vec::Vec,
};
use std::{
collections::VecDeque,
io::{BufRead, BufReader, BufWriter, Read, Write},
net::TcpStream,
};
use dap::{
events::Event,
responses::{Response, ResponseBody},
types,
};
use super::{DapUiState, DapVariableValue};
use crate::debug::ReadMemoryExpr;
#[derive(Debug)]
pub enum DapStopReason {
Stopped(DapUiState),
Terminated,
Restarting,
}
#[derive(Debug)]
enum DapMessage {
Response(Response),
Event(Event),
}
pub struct DapClient {
reader: BufReader<TcpStream>,
writer: BufWriter<TcpStream>,
pending_events: VecDeque<Event>,
seq: i64,
}
impl DapClient {
pub fn connect(addr: &str) -> Result<Self, String> {
let stream = TcpStream::connect(addr)
.map_err(|e| format!("failed to connect to DAP server at {addr}: {e}"))?;
Self::from_stream(stream)
}
pub fn from_stream(stream: TcpStream) -> Result<Self, String> {
let reader = BufReader::new(
stream.try_clone().map_err(|e| format!("failed to clone TCP stream: {e}"))?,
);
let writer = BufWriter::new(stream);
Ok(Self {
reader,
writer,
pending_events: VecDeque::new(),
seq: 0,
})
}
pub fn handshake(&mut self) -> Result<DapUiState, String> {
self.send_request(
"initialize",
serde_json::json!({
"adapterID": "miden-debug-tui",
"clientName": "miden-debug TUI",
"linesStartAt1": true,
"columnsStartAt1": true,
}),
)?;
self.wait_for_response("initialize")?;
self.send_request("launch", serde_json::json!({}))?;
self.wait_for_response("launch")?;
self.send_request("configurationDone", serde_json::json!({}))?;
self.wait_for_response("configurationDone")?;
match self.wait_for_stopped()? {
DapStopReason::Stopped(snapshot) => Ok(snapshot),
DapStopReason::Terminated => Err("server terminated before entry stop".into()),
DapStopReason::Restarting => Err("server requested restart during handshake".into()),
}
}
pub fn step_in(&mut self) -> Result<DapStopReason, String> {
self.send_request("stepIn", serde_json::json!({"threadId": 1}))?;
self.wait_for_response("stepIn")?;
self.wait_for_stopped()
}
pub fn step_over(&mut self) -> Result<DapStopReason, String> {
self.send_request("next", serde_json::json!({"threadId": 1}))?;
self.wait_for_response("next")?;
self.wait_for_stopped()
}
pub fn step_out(&mut self) -> Result<DapStopReason, String> {
self.send_request("stepOut", serde_json::json!({"threadId": 1}))?;
self.wait_for_response("stepOut")?;
self.wait_for_stopped()
}
pub fn continue_(&mut self) -> Result<DapStopReason, String> {
self.send_request("continue", serde_json::json!({"threadId": 1}))?;
self.wait_for_response("continue")?;
self.wait_for_stopped()
}
pub fn stack_trace(&mut self) -> Result<Vec<types::StackFrame>, String> {
self.send_request("stackTrace", serde_json::json!({"threadId": 1}))?;
let resp = self.wait_for_response("stackTrace")?;
match resp.body {
Some(ResponseBody::StackTrace(st)) => Ok(st.stack_frames),
_ => Err("unexpected response to stackTrace".into()),
}
}
pub fn scopes(&mut self, frame_id: i64) -> Result<Vec<types::Scope>, String> {
self.send_request("scopes", serde_json::json!({ "frameId": frame_id }))?;
match self.wait_for_response("scopes")?.body {
Some(ResponseBody::Scopes(scopes)) => Ok(scopes.scopes),
_ => Err("unexpected response to scopes".into()),
}
}
pub fn variables(&mut self, variables_reference: i64) -> Result<Vec<types::Variable>, String> {
self.send_request(
"variables",
serde_json::json!({
"variablesReference": variables_reference
}),
)?;
let resp = self.wait_for_response("variables")?;
match resp.body {
Some(ResponseBody::Variables(v)) => Ok(v.variables),
_ => Err("unexpected response to variables".into()),
}
}
pub fn evaluate(&mut self, expression: &str) -> Result<String, String> {
self.evaluate_with_frame(expression, None)
}
pub fn evaluate_in_frame(&mut self, expression: &str, frame_id: i64) -> Result<String, String> {
self.evaluate_with_frame(expression, Some(frame_id))
}
fn evaluate_with_frame(
&mut self,
expression: &str,
frame_id: Option<i64>,
) -> Result<String, String> {
self.send_request(
"evaluate",
serde_json::json!({
"expression": expression,
"frameId": frame_id
}),
)?;
let resp = self.wait_for_response("evaluate")?;
match resp.body {
Some(ResponseBody::Evaluate(e)) => Ok(e.result),
_ => Err("unexpected response to evaluate".into()),
}
}
pub fn frame_variables(
&mut self,
frame_id: i64,
show_all: bool,
) -> Result<Vec<DapVariableValue>, String> {
let expression = if show_all {
"__miden_variables all"
} else {
"__miden_variables"
};
let snapshot = self.evaluate_in_frame(expression, frame_id)?;
serde_json::from_str(&snapshot)
.map_err(|error| format!("invalid DAP variable snapshot: {error}"))
}
pub fn read_memory(&mut self, expr: &ReadMemoryExpr) -> Result<String, String> {
self.evaluate(&format!("__miden_read_memory {expr}"))
}
pub fn set_breakpoints(&mut self, path: &str, lines: &[i64]) -> Result<(), String> {
let breakpoints: Vec<serde_json::Value> =
lines.iter().map(|&line| serde_json::json!({"line": line})).collect();
self.send_request(
"setBreakpoints",
serde_json::json!({
"source": {"path": path},
"breakpoints": breakpoints,
}),
)?;
self.wait_for_response("setBreakpoints")?;
Ok(())
}
pub fn set_function_breakpoints(&mut self, names: &[String]) -> Result<(), String> {
let breakpoints: Vec<serde_json::Value> =
names.iter().map(|name| serde_json::json!({"name": name})).collect();
self.send_request(
"setFunctionBreakpoints",
serde_json::json!({
"breakpoints": breakpoints,
}),
)?;
self.wait_for_response("setFunctionBreakpoints")?;
Ok(())
}
pub fn restart(&mut self) -> Result<DapStopReason, String> {
self.send_request("restart", serde_json::json!({}))?;
self.wait_for_response("restart")?;
self.wait_for_stopped()
}
pub fn restart_phase2(&mut self) -> Result<DapStopReason, String> {
self.send_request("restart", serde_json::json!({"arguments": {}}))?;
self.wait_for_response("restart")?;
self.wait_for_stopped()
}
pub fn connect_with_retry(addr: &str, timeout: std::time::Duration) -> Result<Self, String> {
let start = std::time::Instant::now();
let mut delay = std::time::Duration::from_millis(50);
loop {
match TcpStream::connect(addr) {
Ok(stream) => {
let reader = BufReader::new(
stream
.try_clone()
.map_err(|e| format!("failed to clone TCP stream: {e}"))?,
);
let writer = BufWriter::new(stream);
return Ok(Self {
reader,
writer,
pending_events: VecDeque::new(),
seq: 0,
});
}
Err(_) if start.elapsed() < timeout => {
std::thread::sleep(delay);
delay = (delay * 2).min(std::time::Duration::from_secs(1));
}
Err(e) => {
return Err(format!(
"failed to reconnect to {addr} after {:.1}s: {e}",
timeout.as_secs_f64()
));
}
}
}
}
pub fn disconnect(&mut self) -> Result<(), String> {
self.send_request("disconnect", serde_json::json!({}))?;
let _ = self.wait_for_response("disconnect");
Ok(())
}
fn send_request(&mut self, command: &str, arguments: serde_json::Value) -> Result<(), String> {
self.seq += 1;
let msg = serde_json::json!({
"seq": self.seq,
"command": command,
"arguments": arguments,
});
let body = serde_json::to_string(&msg).map_err(|e| format!("serialize error: {e}"))?;
write!(self.writer, "Content-Length: {}\r\n\r\n{}", body.len(), body)
.map_err(|e| format!("write error: {e}"))?;
self.writer.flush().map_err(|e| format!("flush error: {e}"))?;
Ok(())
}
fn read_message(&mut self) -> Result<DapMessage, String> {
let mut content_length: usize = 0;
loop {
let mut line = String::new();
self.reader.read_line(&mut line).map_err(|e| format!("read error: {e}"))?;
let trimmed = line.trim();
if trimmed.is_empty() {
if content_length > 0 {
break;
}
continue;
}
if let Some(val) = trimmed.strip_prefix("Content-Length:") {
content_length = val
.trim()
.parse::<usize>()
.map_err(|e| format!("invalid Content-Length: {e}"))?;
}
}
if content_length == 0 {
return Err("missing or zero Content-Length header".into());
}
if content_length > dap::server::MAX_CONTENT_LENGTH {
return Err(format!(
"Content-Length {content_length} exceeds the maximum of {}",
dap::server::MAX_CONTENT_LENGTH
));
}
let mut buf = vec![0u8; content_length];
self.reader.read_exact(&mut buf).map_err(|e| format!("read body error: {e}"))?;
let content = std::str::from_utf8(&buf).map_err(|e| format!("invalid utf-8: {e}"))?;
let raw: serde_json::Value =
serde_json::from_str(content).map_err(|e| format!("JSON parse error: {e}"))?;
match raw.get("type").and_then(|t| t.as_str()) {
Some("response") => {
let resp: Response = serde_json::from_value(raw)
.map_err(|e| format!("response parse error: {e}"))?;
Ok(DapMessage::Response(resp))
}
Some("event") => {
let event: Event =
serde_json::from_value(raw).map_err(|e| format!("event parse error: {e}"))?;
Ok(DapMessage::Event(event))
}
other => Err(format!("unexpected message type: {other:?}")),
}
}
fn next_message(&mut self) -> Result<DapMessage, String> {
if let Some(event) = self.pending_events.pop_front() {
return Ok(DapMessage::Event(event));
}
self.read_message()
}
fn wait_for_response(&mut self, _command: &str) -> Result<Response, String> {
loop {
match self.read_message()? {
DapMessage::Response(resp) => {
if !resp.success {
let msg = resp
.message
.as_ref()
.map(|m| format!("{m:?}"))
.unwrap_or_else(|| "unknown error".into());
return Err(format!("DAP error: {msg}"));
}
return Ok(resp);
}
DapMessage::Event(event) => {
self.pending_events.push_back(event);
continue;
}
}
}
}
fn wait_for_stopped(&mut self) -> Result<DapStopReason, String> {
let mut snapshot: Option<DapUiState> = None;
loop {
match self.next_message()? {
DapMessage::Event(Event::Stopped(_)) => {
let snapshot = snapshot.ok_or_else(|| {
"DAP server sent 'stopped' without a preceding miden/uiState event; the \
remote debugger does not appear to be a miden-debug DAP server"
.to_string()
})?;
return Ok(DapStopReason::Stopped(snapshot));
}
DapMessage::Event(Event::Terminated(body)) => {
let is_restart = body
.as_ref()
.and_then(|b| b.restart.as_ref())
.and_then(|v| v.as_bool())
.unwrap_or(false);
if is_restart {
return Ok(DapStopReason::Restarting);
}
return Ok(DapStopReason::Terminated);
}
DapMessage::Event(Event::MidenUiState(value)) => {
snapshot = Some(
serde_json::from_value::<DapUiState>(value)
.map_err(|e| format!("invalid miden/uiState payload: {e}"))?,
);
}
_ => continue,
}
}
}
}
impl Drop for DapClient {
fn drop(&mut self) {
let _ = self.disconnect();
}
}
#[cfg(test)]
mod tests;