debugger/setup/
verifier.rs

1//! Installation verification
2//!
3//! Verifies that installed debuggers work correctly by sending DAP messages.
4
5use crate::common::{Error, Result};
6use std::path::Path;
7use std::process::Stdio;
8use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
9use tokio::process::{Child, Command};
10use tokio::time::{timeout, Duration};
11
12/// Result of verifying a debugger installation
13#[derive(Debug, Clone)]
14pub struct VerifyResult {
15    /// Whether verification succeeded
16    pub success: bool,
17    /// Debugger capabilities if successful
18    pub capabilities: Option<DapCapabilities>,
19    /// Error message if verification failed
20    pub error: Option<String>,
21}
22
23/// DAP capabilities (subset)
24#[derive(Debug, Clone, Default)]
25pub struct DapCapabilities {
26    pub supports_configuration_done_request: bool,
27    pub supports_function_breakpoints: bool,
28    pub supports_conditional_breakpoints: bool,
29    pub supports_evaluate_for_hovers: bool,
30}
31
32/// Verify a DAP adapter by sending initialize request
33pub async fn verify_dap_adapter(
34    path: &Path,
35    args: &[String],
36) -> Result<VerifyResult> {
37    // Spawn the adapter
38    let mut child = spawn_adapter(path, args).await?;
39
40    // Send initialize request
41    let init_result = timeout(Duration::from_secs(5), send_initialize(&mut child)).await;
42
43    // Cleanup
44    let _ = child.kill().await;
45
46    match init_result {
47        Ok(Ok(caps)) => Ok(VerifyResult {
48            success: true,
49            capabilities: Some(caps),
50            error: None,
51        }),
52        Ok(Err(e)) => Ok(VerifyResult {
53            success: false,
54            capabilities: None,
55            error: Some(e.to_string()),
56        }),
57        Err(_) => Ok(VerifyResult {
58            success: false,
59            capabilities: None,
60            error: Some("Timeout waiting for adapter response".to_string()),
61        }),
62    }
63}
64
65/// Spawn the adapter process
66async fn spawn_adapter(path: &Path, args: &[String]) -> Result<Child> {
67    let child = Command::new(path)
68        .args(args)
69        .stdin(Stdio::piped())
70        .stdout(Stdio::piped())
71        .stderr(Stdio::piped()) // Capture stderr for better error messages
72        .spawn()
73        .map_err(|e| Error::Internal(format!("Failed to spawn adapter: {}", e)))?;
74
75    Ok(child)
76}
77
78/// Send DAP initialize request and parse response
79async fn send_initialize(child: &mut Child) -> Result<DapCapabilities> {
80    let stdin = child.stdin.as_mut().ok_or_else(|| {
81        Error::Internal("Failed to get stdin".to_string())
82    })?;
83    let stdout = child.stdout.as_mut().ok_or_else(|| {
84        Error::Internal("Failed to get stdout".to_string())
85    })?;
86
87    // Create initialize request
88    let request = serde_json::json!({
89        "seq": 1,
90        "type": "request",
91        "command": "initialize",
92        "arguments": {
93            "clientID": "debugger-cli",
94            "clientName": "debugger-cli",
95            "adapterID": "test",
96            "pathFormat": "path",
97            "linesStartAt1": true,
98            "columnsStartAt1": true,
99            "supportsRunInTerminalRequest": false
100        }
101    });
102
103    // Send with DAP header
104    let body = serde_json::to_string(&request)?;
105    let header = format!("Content-Length: {}\r\n\r\n", body.len());
106    stdin.write_all(header.as_bytes()).await?;
107    stdin.write_all(body.as_bytes()).await?;
108    stdin.flush().await?;
109
110    // Read response
111    let mut reader = BufReader::new(stdout);
112
113    // Parse DAP headers - some adapters emit multiple headers (Content-Length, Content-Type)
114    let mut content_length: Option<usize> = None;
115    loop {
116        let mut header_line = String::new();
117        reader.read_line(&mut header_line).await?;
118        let trimmed = header_line.trim();
119
120        // Empty line marks end of headers
121        if trimmed.is_empty() {
122            break;
123        }
124
125        // Parse Content-Length header
126        if let Some(len_str) = trimmed.strip_prefix("Content-Length:") {
127            content_length = len_str.trim().parse().ok();
128        }
129        // Ignore other headers (e.g., Content-Type)
130    }
131
132    let content_length = content_length
133        .ok_or_else(|| Error::Internal("Missing Content-Length in DAP response".to_string()))?;
134
135    // Read body
136    let mut body = vec![0u8; content_length];
137    tokio::io::AsyncReadExt::read_exact(&mut reader, &mut body).await?;
138
139    // Parse response
140    let response: serde_json::Value = serde_json::from_slice(&body)?;
141
142    // Check for success
143    if response.get("success").and_then(|v| v.as_bool()) != Some(true) {
144        let message = response
145            .get("message")
146            .and_then(|v| v.as_str())
147            .unwrap_or("Unknown error");
148        return Err(Error::Internal(format!("Initialize failed: {}", message)));
149    }
150
151    // Extract capabilities
152    let body = response.get("body").cloned().unwrap_or_default();
153    let caps = DapCapabilities {
154        supports_configuration_done_request: body
155            .get("supportsConfigurationDoneRequest")
156            .and_then(|v| v.as_bool())
157            .unwrap_or(false),
158        supports_function_breakpoints: body
159            .get("supportsFunctionBreakpoints")
160            .and_then(|v| v.as_bool())
161            .unwrap_or(false),
162        supports_conditional_breakpoints: body
163            .get("supportsConditionalBreakpoints")
164            .and_then(|v| v.as_bool())
165            .unwrap_or(false),
166        supports_evaluate_for_hovers: body
167            .get("supportsEvaluateForHovers")
168            .and_then(|v| v.as_bool())
169            .unwrap_or(false),
170    };
171
172    Ok(caps)
173}
174
175/// Simple executable check (just verifies the binary runs)
176pub async fn verify_executable(path: &Path, version_arg: Option<&str>) -> Result<VerifyResult> {
177    let arg = version_arg.unwrap_or("--version");
178
179    let output = tokio::process::Command::new(path)
180        .arg(arg)
181        .output()
182        .await
183        .map_err(|e| Error::Internal(format!("Failed to run {}: {}", path.display(), e)))?;
184
185    if output.status.success() {
186        Ok(VerifyResult {
187            success: true,
188            capabilities: None,
189            error: None,
190        })
191    } else {
192        Ok(VerifyResult {
193            success: false,
194            capabilities: None,
195            error: Some(format!(
196                "Exit code: {}",
197                output.status.code().unwrap_or(-1)
198            )),
199        })
200    }
201}