Skip to main content

debugger/setup/
verifier.rs

1//! Installation verification
2//!
3//! Verifies that installed debuggers work correctly by sending DAP messages.
4
5use crate::common::{parse_listen_address, Error, Result};
6use std::path::Path;
7use std::process::Stdio;
8use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
9use tokio::net::TcpStream;
10use tokio::process::{Child, Command};
11use tokio::time::{timeout, Duration};
12
13/// Result of verifying a debugger installation
14#[derive(Debug, Clone)]
15pub struct VerifyResult {
16    /// Whether verification succeeded
17    pub success: bool,
18    /// Debugger capabilities if successful
19    pub capabilities: Option<DapCapabilities>,
20    /// Error message if verification failed
21    pub error: Option<String>,
22}
23
24/// DAP capabilities (subset)
25#[derive(Debug, Clone, Default)]
26pub struct DapCapabilities {
27    pub supports_configuration_done_request: bool,
28    pub supports_function_breakpoints: bool,
29    pub supports_conditional_breakpoints: bool,
30    pub supports_evaluate_for_hovers: bool,
31}
32
33/// Verify a DAP adapter by sending initialize request
34pub async fn verify_dap_adapter(
35    path: &Path,
36    args: &[String],
37) -> Result<VerifyResult> {
38    // Spawn the adapter
39    let mut child = spawn_adapter(path, args).await?;
40
41    // Send initialize request
42    let init_result = timeout(Duration::from_secs(5), send_initialize(&mut child)).await;
43
44    // Cleanup
45    let _ = child.kill().await;
46
47    match init_result {
48        Ok(Ok(caps)) => Ok(VerifyResult {
49            success: true,
50            capabilities: Some(caps),
51            error: None,
52        }),
53        Ok(Err(e)) => Ok(VerifyResult {
54            success: false,
55            capabilities: None,
56            error: Some(e.to_string()),
57        }),
58        Err(_) => Ok(VerifyResult {
59            success: false,
60            capabilities: None,
61            error: Some("Timeout waiting for adapter response".to_string()),
62        }),
63    }
64}
65
66/// Verify a TCP-based DAP adapter (like Delve) by spawning it with --listen
67/// and connecting via TCP to send the initialize request
68pub async fn verify_dap_adapter_tcp(
69    path: &Path,
70    args: &[String],
71) -> Result<VerifyResult> {
72    // Spawn the adapter with --listen=127.0.0.1:0
73    let mut cmd = Command::new(path);
74    cmd.args(args)
75        .arg("--listen=127.0.0.1:0")
76        .stdin(Stdio::null())
77        .stdout(Stdio::piped())
78        .stderr(Stdio::piped());
79
80    let mut child = cmd.spawn().map_err(|e| {
81        Error::Internal(format!("Failed to spawn adapter: {}", e))
82    })?;
83
84    // Read stdout to find the listening address
85    let stdout = child.stdout.take().ok_or_else(|| {
86        Error::Internal("Failed to get adapter stdout".to_string())
87    })?;
88
89    let mut stdout_reader = BufReader::new(stdout);
90    let mut line = String::new();
91
92    // Wait for the "listening at" message with timeout
93    let listen_result = timeout(Duration::from_secs(10), async {
94        loop {
95            line.clear();
96            let bytes_read = stdout_reader.read_line(&mut line).await.map_err(|e| {
97                Error::Internal(format!("Failed to read adapter output: {}", e))
98            })?;
99
100            if bytes_read == 0 {
101                return Err(Error::Internal(
102                    "Adapter exited before outputting listen address".to_string(),
103                ));
104            }
105
106            // Look for the listening address in the output
107            if let Some(addr) = parse_listen_address(&line) {
108                return Ok(addr);
109            }
110        }
111    })
112    .await;
113
114    let addr = match listen_result {
115        Ok(Ok(addr)) => addr,
116        Ok(Err(e)) => {
117            let _ = child.kill().await;
118            return Ok(VerifyResult {
119                success: false,
120                capabilities: None,
121                error: Some(e.to_string()),
122            });
123        }
124        Err(_) => {
125            let _ = child.kill().await;
126            return Ok(VerifyResult {
127                success: false,
128                capabilities: None,
129                error: Some("Timeout waiting for adapter to start listening".to_string()),
130            });
131        }
132    };
133
134    // Connect to the TCP port
135    let stream = match TcpStream::connect(&addr).await {
136        Ok(s) => s,
137        Err(e) => {
138            let _ = child.kill().await;
139            return Ok(VerifyResult {
140                success: false,
141                capabilities: None,
142                error: Some(format!("Failed to connect to {}: {}", addr, e)),
143            });
144        }
145    };
146
147    // Send initialize request and wait for response
148    let init_result = timeout(Duration::from_secs(5), send_initialize_tcp(stream)).await;
149
150    // Cleanup
151    let _ = child.kill().await;
152
153    match init_result {
154        Ok(Ok(caps)) => Ok(VerifyResult {
155            success: true,
156            capabilities: Some(caps),
157            error: None,
158        }),
159        Ok(Err(e)) => Ok(VerifyResult {
160            success: false,
161            capabilities: None,
162            error: Some(e.to_string()),
163        }),
164        Err(_) => Ok(VerifyResult {
165            success: false,
166            capabilities: None,
167            error: Some("Timeout waiting for adapter response".to_string()),
168        }),
169    }
170}
171
172/// Spawn the adapter process
173async fn spawn_adapter(path: &Path, args: &[String]) -> Result<Child> {
174    let child = Command::new(path)
175        .args(args)
176        .stdin(Stdio::piped())
177        .stdout(Stdio::piped())
178        .stderr(Stdio::piped()) // Capture stderr for better error messages
179        .spawn()
180        .map_err(|e| Error::Internal(format!("Failed to spawn adapter: {}", e)))?;
181
182    Ok(child)
183}
184
185/// Build the DAP initialize request JSON
186fn build_initialize_request() -> serde_json::Value {
187    serde_json::json!({
188        "seq": 1,
189        "type": "request",
190        "command": "initialize",
191        "arguments": {
192            "clientID": "debugger-cli",
193            "clientName": "debugger-cli",
194            "adapterID": "test",
195            "pathFormat": "path",
196            "linesStartAt1": true,
197            "columnsStartAt1": true,
198            "supportsRunInTerminalRequest": false
199        }
200    })
201}
202
203/// Parse a DAP response and extract capabilities
204fn parse_initialize_response(response: &serde_json::Value) -> Result<DapCapabilities> {
205    // Check for success
206    if response.get("success").and_then(|v| v.as_bool()) != Some(true) {
207        let message = response
208            .get("message")
209            .and_then(|v| v.as_str())
210            .unwrap_or("Unknown error");
211        return Err(Error::Internal(format!("Initialize failed: {}", message)));
212    }
213
214    // Extract capabilities
215    let body = response.get("body").cloned().unwrap_or_default();
216    Ok(DapCapabilities {
217        supports_configuration_done_request: body
218            .get("supportsConfigurationDoneRequest")
219            .and_then(|v| v.as_bool())
220            .unwrap_or(false),
221        supports_function_breakpoints: body
222            .get("supportsFunctionBreakpoints")
223            .and_then(|v| v.as_bool())
224            .unwrap_or(false),
225        supports_conditional_breakpoints: body
226            .get("supportsConditionalBreakpoints")
227            .and_then(|v| v.as_bool())
228            .unwrap_or(false),
229        supports_evaluate_for_hovers: body
230            .get("supportsEvaluateForHovers")
231            .and_then(|v| v.as_bool())
232            .unwrap_or(false),
233    })
234}
235
236/// Send a DAP message and read the response from a reader/writer pair
237async fn send_dap_message<W, R>(writer: &mut W, reader: &mut BufReader<R>, request: &serde_json::Value) -> Result<serde_json::Value>
238where
239    W: AsyncWriteExt + Unpin,
240    R: tokio::io::AsyncRead + Unpin,
241{
242    // Send with DAP header
243    let body = serde_json::to_string(request)?;
244    let header = format!("Content-Length: {}\r\n\r\n", body.len());
245    writer.write_all(header.as_bytes()).await?;
246    writer.write_all(body.as_bytes()).await?;
247    writer.flush().await?;
248
249    // Parse DAP headers - some adapters emit multiple headers (Content-Length, Content-Type)
250    let mut content_length: Option<usize> = None;
251    loop {
252        let mut header_line = String::new();
253        reader.read_line(&mut header_line).await?;
254        let trimmed = header_line.trim();
255
256        // Empty line marks end of headers
257        if trimmed.is_empty() {
258            break;
259        }
260
261        // Parse Content-Length header
262        if let Some(len_str) = trimmed.strip_prefix("Content-Length:") {
263            content_length = len_str.trim().parse().ok();
264        }
265        // Ignore other headers (e.g., Content-Type)
266    }
267
268    let content_length = content_length
269        .ok_or_else(|| Error::Internal("Missing Content-Length in DAP response".to_string()))?;
270
271    // Read body
272    let mut body = vec![0u8; content_length];
273    reader.read_exact(&mut body).await?;
274
275    // Parse response
276    let response: serde_json::Value = serde_json::from_slice(&body)?;
277    Ok(response)
278}
279
280/// Send DAP initialize request and parse response (stdio)
281async fn send_initialize(child: &mut Child) -> Result<DapCapabilities> {
282    let stdin = child.stdin.as_mut().ok_or_else(|| {
283        Error::Internal("Failed to get stdin".to_string())
284    })?;
285    let stdout = child.stdout.as_mut().ok_or_else(|| {
286        Error::Internal("Failed to get stdout".to_string())
287    })?;
288
289    let request = build_initialize_request();
290    let mut reader = BufReader::new(stdout);
291    let response = send_dap_message(stdin, &mut reader, &request).await?;
292    parse_initialize_response(&response)
293}
294
295/// Send DAP initialize request via TCP and parse response
296async fn send_initialize_tcp(stream: TcpStream) -> Result<DapCapabilities> {
297    let (read_half, mut write_half) = tokio::io::split(stream);
298    
299    let request = build_initialize_request();
300    let mut reader = BufReader::new(read_half);
301    let response = send_dap_message(&mut write_half, &mut reader, &request).await?;
302    parse_initialize_response(&response)
303}
304
305/// Simple executable check (just verifies the binary runs)
306pub async fn verify_executable(path: &Path, version_arg: Option<&str>) -> Result<VerifyResult> {
307    let arg = version_arg.unwrap_or("--version");
308
309    let output = tokio::process::Command::new(path)
310        .arg(arg)
311        .output()
312        .await
313        .map_err(|e| Error::Internal(format!("Failed to run {}: {}", path.display(), e)))?;
314
315    if output.status.success() {
316        Ok(VerifyResult {
317            success: true,
318            capabilities: None,
319            error: None,
320        })
321    } else {
322        Ok(VerifyResult {
323            success: false,
324            capabilities: None,
325            error: Some(format!(
326                "Exit code: {}",
327                output.status.code().unwrap_or(-1)
328            )),
329        })
330    }
331}