use crate::common::config::TcpSpawnStyle;
use crate::common::{parse_listen_address, Error, Result};
use std::path::Path;
use std::process::Stdio;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tokio::process::{Child, Command};
use tokio::time::{timeout, Duration};
#[derive(Debug, Clone)]
pub struct VerifyResult {
pub success: bool,
pub capabilities: Option<DapCapabilities>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct DapCapabilities {
pub supports_configuration_done_request: bool,
pub supports_function_breakpoints: bool,
pub supports_conditional_breakpoints: bool,
pub supports_evaluate_for_hovers: bool,
}
pub async fn verify_dap_adapter(
path: &Path,
args: &[String],
) -> Result<VerifyResult> {
let mut child = spawn_adapter(path, args).await?;
let init_result = timeout(Duration::from_secs(5), send_initialize(&mut child)).await;
let _ = child.kill().await;
match init_result {
Ok(Ok(caps)) => Ok(VerifyResult {
success: true,
capabilities: Some(caps),
error: None,
}),
Ok(Err(e)) => Ok(VerifyResult {
success: false,
capabilities: None,
error: Some(e.to_string()),
}),
Err(_) => Ok(VerifyResult {
success: false,
capabilities: None,
error: Some("Timeout waiting for adapter response".to_string()),
}),
}
}
pub async fn verify_dap_adapter_tcp(
path: &Path,
args: &[String],
spawn_style: TcpSpawnStyle,
) -> Result<VerifyResult> {
let (mut child, addr) = match spawn_style {
TcpSpawnStyle::TcpListen => {
let mut cmd = Command::new(path);
cmd.args(args)
.arg("--listen=127.0.0.1:0")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| {
Error::Internal(format!("Failed to spawn adapter: {}", e))
})?;
let stdout = child.stdout.take().ok_or_else(|| {
Error::Internal("Failed to get adapter stdout".to_string())
})?;
let mut stdout_reader = BufReader::new(stdout);
let mut line = String::new();
let listen_result = timeout(Duration::from_secs(10), async {
loop {
line.clear();
let bytes_read = stdout_reader.read_line(&mut line).await.map_err(|e| {
Error::Internal(format!("Failed to read adapter output: {}", e))
})?;
if bytes_read == 0 {
return Err(Error::Internal(
"Adapter exited before outputting listen address".to_string(),
));
}
if let Some(addr) = parse_listen_address(&line) {
return Ok(addr);
}
}
})
.await;
let addr = match listen_result {
Ok(Ok(addr)) => addr,
Ok(Err(e)) => {
let _ = child.kill().await;
return Ok(VerifyResult {
success: false,
capabilities: None,
error: Some(e.to_string()),
});
}
Err(_) => {
let _ = child.kill().await;
return Ok(VerifyResult {
success: false,
capabilities: None,
error: Some("Timeout waiting for adapter to start listening".to_string()),
});
}
};
(child, addr)
}
TcpSpawnStyle::TcpPortArg => {
use std::net::TcpListener as StdTcpListener;
let listener = StdTcpListener::bind("127.0.0.1:0").map_err(|e| {
Error::Internal(format!("Failed to allocate port: {}", e))
})?;
let port = listener.local_addr().map_err(|e| {
Error::Internal(format!("Failed to get port: {}", e))
})?.port();
drop(listener);
let addr = format!("127.0.0.1:{}", port);
let mut cmd = Command::new(path);
let mut full_args = args.to_vec();
full_args.push(port.to_string());
cmd.args(&full_args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let child = cmd.spawn().map_err(|e| {
Error::Internal(format!("Failed to spawn adapter: {}", e))
})?;
(child, addr)
}
};
let stream = {
let mut last_error = String::new();
let mut delay = Duration::from_millis(100);
let max_delay = Duration::from_millis(1000);
let timeout_duration = Duration::from_secs(10);
let start = std::time::Instant::now();
loop {
match TcpStream::connect(&addr).await {
Ok(s) => break s,
Err(e) => {
last_error = e.to_string();
if start.elapsed() >= timeout_duration {
let _ = child.kill().await;
return Ok(VerifyResult {
success: false,
capabilities: None,
error: Some(format!("Failed to connect to {} after {:?}: {}", addr, timeout_duration, last_error)),
});
}
tokio::time::sleep(delay).await;
delay = std::cmp::min(delay * 2, max_delay);
}
}
}
};
let init_result = timeout(Duration::from_secs(5), send_initialize_tcp(stream)).await;
let _ = child.kill().await;
match init_result {
Ok(Ok(caps)) => Ok(VerifyResult {
success: true,
capabilities: Some(caps),
error: None,
}),
Ok(Err(e)) => Ok(VerifyResult {
success: false,
capabilities: None,
error: Some(e.to_string()),
}),
Err(_) => Ok(VerifyResult {
success: false,
capabilities: None,
error: Some("Timeout waiting for adapter response".to_string()),
}),
}
}
async fn spawn_adapter(path: &Path, args: &[String]) -> Result<Child> {
let child = Command::new(path)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped()) .spawn()
.map_err(|e| Error::Internal(format!("Failed to spawn adapter: {}", e)))?;
Ok(child)
}
fn build_initialize_request() -> serde_json::Value {
serde_json::json!({
"seq": 1,
"type": "request",
"command": "initialize",
"arguments": {
"clientID": "debugger-cli",
"clientName": "debugger-cli",
"adapterID": "test",
"pathFormat": "path",
"linesStartAt1": true,
"columnsStartAt1": true,
"supportsRunInTerminalRequest": false
}
})
}
fn parse_initialize_response(response: &serde_json::Value) -> Result<DapCapabilities> {
if response.get("success").and_then(|v| v.as_bool()) != Some(true) {
let message = response
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
return Err(Error::Internal(format!("Initialize failed: {}", message)));
}
let body = response.get("body").cloned().unwrap_or_default();
Ok(DapCapabilities {
supports_configuration_done_request: body
.get("supportsConfigurationDoneRequest")
.and_then(|v| v.as_bool())
.unwrap_or(false),
supports_function_breakpoints: body
.get("supportsFunctionBreakpoints")
.and_then(|v| v.as_bool())
.unwrap_or(false),
supports_conditional_breakpoints: body
.get("supportsConditionalBreakpoints")
.and_then(|v| v.as_bool())
.unwrap_or(false),
supports_evaluate_for_hovers: body
.get("supportsEvaluateForHovers")
.and_then(|v| v.as_bool())
.unwrap_or(false),
})
}
async fn send_dap_message<W, R>(writer: &mut W, reader: &mut BufReader<R>, request: &serde_json::Value) -> Result<serde_json::Value>
where
W: AsyncWriteExt + Unpin,
R: tokio::io::AsyncRead + Unpin,
{
let body = serde_json::to_string(request)?;
let header = format!("Content-Length: {}\r\n\r\n", body.len());
writer.write_all(header.as_bytes()).await?;
writer.write_all(body.as_bytes()).await?;
writer.flush().await?;
let mut content_length: Option<usize> = None;
loop {
let mut header_line = String::new();
reader.read_line(&mut header_line).await?;
let trimmed = header_line.trim();
if trimmed.is_empty() {
break;
}
if let Some(len_str) = trimmed.strip_prefix("Content-Length:") {
content_length = len_str.trim().parse().ok();
}
}
let content_length = content_length
.ok_or_else(|| Error::Internal("Missing Content-Length in DAP response".to_string()))?;
let mut body = vec![0u8; content_length];
reader.read_exact(&mut body).await?;
let response: serde_json::Value = serde_json::from_slice(&body)?;
Ok(response)
}
async fn send_initialize(child: &mut Child) -> Result<DapCapabilities> {
let stdin = child.stdin.as_mut().ok_or_else(|| {
Error::Internal("Failed to get stdin".to_string())
})?;
let stdout = child.stdout.as_mut().ok_or_else(|| {
Error::Internal("Failed to get stdout".to_string())
})?;
let request = build_initialize_request();
let mut reader = BufReader::new(stdout);
let response = send_dap_message(stdin, &mut reader, &request).await?;
parse_initialize_response(&response)
}
async fn send_initialize_tcp(stream: TcpStream) -> Result<DapCapabilities> {
let (read_half, mut write_half) = tokio::io::split(stream);
let request = build_initialize_request();
let mut reader = BufReader::new(read_half);
let response = send_dap_message(&mut write_half, &mut reader, &request).await?;
parse_initialize_response(&response)
}
pub async fn verify_executable(path: &Path, version_arg: Option<&str>) -> Result<VerifyResult> {
let arg = version_arg.unwrap_or("--version");
let output = tokio::process::Command::new(path)
.arg(arg)
.output()
.await
.map_err(|e| Error::Internal(format!("Failed to run {}: {}", path.display(), e)))?;
if output.status.success() {
Ok(VerifyResult {
success: true,
capabilities: None,
error: None,
})
} else {
Ok(VerifyResult {
success: false,
capabilities: None,
error: Some(format!(
"Exit code: {}",
output.status.code().unwrap_or(-1)
)),
})
}
}