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