Skip to main content

synwire_dap/
client.rs

1//! High-level DAP client wrapping the low-level transport.
2//!
3//! Provides typed methods for the full DAP lifecycle: initialization,
4//! breakpoints, stepping, evaluation, and teardown.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10use tokio::sync::RwLock;
11
12use crate::config::DapAdapterConfig;
13use crate::error::DapError;
14use crate::transport::{DapTransport, EventHandler};
15
16/// Current state of a debug session.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
18#[non_exhaustive]
19pub enum DapSessionState {
20    /// No session has been started.
21    #[default]
22    NotStarted,
23    /// The `initialize` handshake is in progress.
24    Initializing,
25    /// `configurationDone` has been sent.
26    Configured,
27    /// The debuggee is executing.
28    Running,
29    /// The debuggee has hit a breakpoint or been paused.
30    Stopped,
31    /// The debug session has terminated.
32    Terminated,
33}
34
35impl std::fmt::Display for DapSessionState {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            Self::NotStarted => write!(f, "not_started"),
39            Self::Initializing => write!(f, "initializing"),
40            Self::Configured => write!(f, "configured"),
41            Self::Running => write!(f, "running"),
42            Self::Stopped => write!(f, "stopped"),
43            Self::Terminated => write!(f, "terminated"),
44        }
45    }
46}
47
48/// High-level DAP client managing a single debug session.
49///
50/// Wraps [`DapTransport`] and tracks session state, capabilities,
51/// and active breakpoints.
52pub struct DapClient {
53    transport: Arc<DapTransport>,
54    capabilities: RwLock<Option<serde_json::Value>>,
55    status: Arc<RwLock<DapSessionState>>,
56    active_breakpoints: Arc<RwLock<HashMap<String, Vec<serde_json::Value>>>>,
57}
58
59impl DapClient {
60    /// Spawn an adapter process and create a new client.
61    ///
62    /// This only starts the child process. Call [`initialize`](Self::initialize)
63    /// afterwards to perform the DAP handshake.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`DapError::BinaryNotFound`] if the command is not on `PATH`,
68    /// or [`DapError::Io`] if the process fails to spawn.
69    pub fn start(config: &DapAdapterConfig, event_handler: EventHandler) -> Result<Self, DapError> {
70        // Verify the binary exists.
71        drop(
72            which::which(&config.command).map_err(|_| DapError::BinaryNotFound {
73                binary: config.command.clone(),
74            })?,
75        );
76
77        let transport =
78            DapTransport::spawn(&config.command, &config.args, &config.env, event_handler)?;
79
80        Ok(Self {
81            transport: Arc::new(transport),
82            capabilities: RwLock::new(None),
83            status: Arc::new(RwLock::new(DapSessionState::NotStarted)),
84            active_breakpoints: Arc::new(RwLock::new(HashMap::new())),
85        })
86    }
87
88    /// Perform the DAP `initialize` handshake.
89    ///
90    /// Sends `initialize`, stores the adapter's capabilities, then sends
91    /// `configurationDone`.
92    ///
93    /// # Errors
94    ///
95    /// Returns [`DapError::InitializationFailed`] if the handshake fails.
96    pub async fn initialize(&self) -> Result<(), DapError> {
97        self.set_status(DapSessionState::Initializing).await;
98
99        let args = serde_json::json!({
100            "clientID": "synwire",
101            "clientName": "Synwire Agent",
102            "adapterID": "synwire",
103            "linesStartAt1": true,
104            "columnsStartAt1": true,
105            "pathFormat": "path",
106            "supportsRunInTerminalRequest": false,
107            "supportsVariableType": true,
108            "supportsVariablePaging": false,
109        });
110
111        let response = self
112            .transport
113            .send_request("initialize", args)
114            .await
115            .map_err(|e| DapError::InitializationFailed(e.to_string()))?;
116
117        // Store capabilities from the response body.
118        if let Some(body) = response.get("body") {
119            *self.capabilities.write().await = Some(body.clone());
120        }
121
122        tracing::debug!("DAP initialize handshake complete");
123
124        Ok(())
125    }
126
127    /// Launch a program under the debugger.
128    ///
129    /// The adapter must have been initialized first. Sends `launch` followed
130    /// by `configurationDone`.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`DapError`] if the request fails.
135    pub async fn launch(&self, config: serde_json::Value) -> Result<(), DapError> {
136        self.ensure_initialized().await?;
137
138        let _ = self.transport.send_request("launch", config).await?;
139        let _ = self
140            .transport
141            .send_request("configurationDone", serde_json::json!({}))
142            .await?;
143
144        self.set_status(DapSessionState::Running).await;
145        tracing::debug!("DAP launch complete");
146        Ok(())
147    }
148
149    /// Attach to a running process.
150    ///
151    /// # Errors
152    ///
153    /// Returns [`DapError`] if the request fails.
154    pub async fn attach(&self, config: serde_json::Value) -> Result<(), DapError> {
155        self.ensure_initialized().await?;
156
157        let _ = self.transport.send_request("attach", config).await?;
158        let _ = self
159            .transport
160            .send_request("configurationDone", serde_json::json!({}))
161            .await?;
162
163        self.set_status(DapSessionState::Running).await;
164        tracing::debug!("DAP attach complete");
165        Ok(())
166    }
167
168    /// Disconnect from the debug session.
169    ///
170    /// # Errors
171    ///
172    /// Returns [`DapError`] if the request fails.
173    pub async fn disconnect(&self) -> Result<(), DapError> {
174        let _ = self
175            .transport
176            .send_request(
177                "disconnect",
178                serde_json::json!({
179                    "restart": false,
180                    "terminateDebuggee": true,
181                }),
182            )
183            .await?;
184
185        self.set_status(DapSessionState::Terminated).await;
186        tracing::debug!("DAP disconnected");
187        Ok(())
188    }
189
190    /// Terminate the debuggee.
191    ///
192    /// # Errors
193    ///
194    /// Returns [`DapError`] if the request fails.
195    pub async fn terminate(&self) -> Result<(), DapError> {
196        let _ = self
197            .transport
198            .send_request("terminate", serde_json::json!({ "restart": false }))
199            .await?;
200
201        self.set_status(DapSessionState::Terminated).await;
202        tracing::debug!("DAP terminated");
203        Ok(())
204    }
205
206    /// Set breakpoints for a source file.
207    ///
208    /// Returns the list of verified breakpoint objects from the adapter.
209    ///
210    /// # Errors
211    ///
212    /// Returns [`DapError`] if the request fails.
213    pub async fn set_breakpoints(
214        &self,
215        source_path: &str,
216        lines: &[i64],
217    ) -> Result<Vec<serde_json::Value>, DapError> {
218        let breakpoints: Vec<serde_json::Value> = lines
219            .iter()
220            .map(|&line| serde_json::json!({ "line": line }))
221            .collect();
222
223        let args = serde_json::json!({
224            "source": { "path": source_path },
225            "breakpoints": breakpoints,
226        });
227
228        let response = self.transport.send_request("setBreakpoints", args).await?;
229
230        let result = response
231            .get("body")
232            .and_then(|b| b.get("breakpoints"))
233            .and_then(serde_json::Value::as_array)
234            .cloned()
235            .unwrap_or_default();
236
237        // Track active breakpoints.
238        let _ = self
239            .active_breakpoints
240            .write()
241            .await
242            .insert(source_path.to_string(), result.clone());
243
244        Ok(result)
245    }
246
247    /// Set function breakpoints by name.
248    ///
249    /// # Errors
250    ///
251    /// Returns [`DapError`] if the request fails.
252    pub async fn set_function_breakpoints(
253        &self,
254        names: &[String],
255    ) -> Result<Vec<serde_json::Value>, DapError> {
256        let breakpoints: Vec<serde_json::Value> = names
257            .iter()
258            .map(|name| serde_json::json!({ "name": name }))
259            .collect();
260
261        let args = serde_json::json!({ "breakpoints": breakpoints });
262
263        let response = self
264            .transport
265            .send_request("setFunctionBreakpoints", args)
266            .await?;
267
268        let result = response
269            .get("body")
270            .and_then(|b| b.get("breakpoints"))
271            .and_then(serde_json::Value::as_array)
272            .cloned()
273            .unwrap_or_default();
274
275        Ok(result)
276    }
277
278    /// Set exception breakpoints by filter ID.
279    ///
280    /// # Errors
281    ///
282    /// Returns [`DapError`] if the request fails.
283    pub async fn set_exception_breakpoints(&self, filters: &[String]) -> Result<(), DapError> {
284        let _ = self
285            .transport
286            .send_request(
287                "setExceptionBreakpoints",
288                serde_json::json!({ "filters": filters }),
289            )
290            .await?;
291        Ok(())
292    }
293
294    /// Continue execution of the specified thread.
295    ///
296    /// # Errors
297    ///
298    /// Returns [`DapError`] if the request fails.
299    pub async fn continue_execution(&self, thread_id: i64) -> Result<(), DapError> {
300        let _ = self
301            .transport
302            .send_request("continue", serde_json::json!({ "threadId": thread_id }))
303            .await?;
304        self.set_status(DapSessionState::Running).await;
305        Ok(())
306    }
307
308    /// Step over (next) for the specified thread.
309    ///
310    /// # Errors
311    ///
312    /// Returns [`DapError`] if the request fails.
313    pub async fn next(&self, thread_id: i64) -> Result<(), DapError> {
314        let _ = self
315            .transport
316            .send_request("next", serde_json::json!({ "threadId": thread_id }))
317            .await?;
318        self.set_status(DapSessionState::Running).await;
319        Ok(())
320    }
321
322    /// Step into for the specified thread.
323    ///
324    /// # Errors
325    ///
326    /// Returns [`DapError`] if the request fails.
327    pub async fn step_in(&self, thread_id: i64) -> Result<(), DapError> {
328        let _ = self
329            .transport
330            .send_request("stepIn", serde_json::json!({ "threadId": thread_id }))
331            .await?;
332        self.set_status(DapSessionState::Running).await;
333        Ok(())
334    }
335
336    /// Step out for the specified thread.
337    ///
338    /// # Errors
339    ///
340    /// Returns [`DapError`] if the request fails.
341    pub async fn step_out(&self, thread_id: i64) -> Result<(), DapError> {
342        let _ = self
343            .transport
344            .send_request("stepOut", serde_json::json!({ "threadId": thread_id }))
345            .await?;
346        self.set_status(DapSessionState::Running).await;
347        Ok(())
348    }
349
350    /// Pause the specified thread.
351    ///
352    /// # Errors
353    ///
354    /// Returns [`DapError`] if the request fails.
355    pub async fn pause(&self, thread_id: i64) -> Result<(), DapError> {
356        let _ = self
357            .transport
358            .send_request("pause", serde_json::json!({ "threadId": thread_id }))
359            .await?;
360        self.set_status(DapSessionState::Stopped).await;
361        Ok(())
362    }
363
364    /// List all threads in the debuggee.
365    ///
366    /// # Errors
367    ///
368    /// Returns [`DapError`] if the request fails.
369    pub async fn threads(&self) -> Result<Vec<serde_json::Value>, DapError> {
370        let response = self
371            .transport
372            .send_request("threads", serde_json::json!({}))
373            .await?;
374        Ok(extract_array(&response, "threads"))
375    }
376
377    /// Get the stack trace for a thread.
378    ///
379    /// # Errors
380    ///
381    /// Returns [`DapError`] if the request fails.
382    pub async fn stack_trace(&self, thread_id: i64) -> Result<Vec<serde_json::Value>, DapError> {
383        let response = self
384            .transport
385            .send_request("stackTrace", serde_json::json!({ "threadId": thread_id }))
386            .await?;
387        Ok(extract_array(&response, "stackFrames"))
388    }
389
390    /// Get scopes for a stack frame.
391    ///
392    /// # Errors
393    ///
394    /// Returns [`DapError`] if the request fails.
395    pub async fn scopes(&self, frame_id: i64) -> Result<Vec<serde_json::Value>, DapError> {
396        let response = self
397            .transport
398            .send_request("scopes", serde_json::json!({ "frameId": frame_id }))
399            .await?;
400        Ok(extract_array(&response, "scopes"))
401    }
402
403    /// Get variables for a variables reference (from a scope or structured variable).
404    ///
405    /// # Errors
406    ///
407    /// Returns [`DapError`] if the request fails.
408    pub async fn variables(&self, variables_ref: i64) -> Result<Vec<serde_json::Value>, DapError> {
409        let response = self
410            .transport
411            .send_request(
412                "variables",
413                serde_json::json!({ "variablesReference": variables_ref }),
414            )
415            .await?;
416        Ok(extract_array(&response, "variables"))
417    }
418
419    /// Evaluate an expression in the debuggee context.
420    ///
421    /// # Errors
422    ///
423    /// Returns [`DapError`] if the request fails.
424    pub async fn evaluate(
425        &self,
426        expression: &str,
427        frame_id: Option<i64>,
428    ) -> Result<serde_json::Value, DapError> {
429        let mut args = serde_json::json!({
430            "expression": expression,
431            "context": "repl",
432        });
433
434        if let Some(fid) = frame_id
435            && let Some(obj) = args.as_object_mut()
436        {
437            let _ = obj.insert("frameId".to_string(), serde_json::json!(fid));
438        }
439
440        let response = self.transport.send_request("evaluate", args).await?;
441
442        Ok(response
443            .get("body")
444            .cloned()
445            .unwrap_or_else(|| serde_json::json!({})))
446    }
447
448    /// Return the current session state.
449    pub async fn status(&self) -> DapSessionState {
450        *self.status.read().await
451    }
452
453    /// Return the adapter capabilities received during initialization.
454    pub async fn capabilities(&self) -> Option<serde_json::Value> {
455        self.capabilities.read().await.clone()
456    }
457
458    /// Return a reference to the underlying transport.
459    #[must_use]
460    pub const fn transport(&self) -> &Arc<DapTransport> {
461        &self.transport
462    }
463
464    /// Return active breakpoints keyed by source path.
465    pub async fn active_breakpoints(&self) -> HashMap<String, Vec<serde_json::Value>> {
466        self.active_breakpoints.read().await.clone()
467    }
468
469    /// Update session state.
470    pub(crate) async fn set_status(&self, new_state: DapSessionState) {
471        *self.status.write().await = new_state;
472    }
473
474    /// Verify the adapter has been initialized.
475    async fn ensure_initialized(&self) -> Result<(), DapError> {
476        let state = *self.status.read().await;
477        if state == DapSessionState::NotStarted {
478            return Err(DapError::NotReady {
479                state: state.to_string(),
480            });
481        }
482        Ok(())
483    }
484}
485
486/// Extract an array field from a DAP response body.
487fn extract_array(response: &serde_json::Value, field: &str) -> Vec<serde_json::Value> {
488    response
489        .get("body")
490        .and_then(|b| b.get(field))
491        .and_then(serde_json::Value::as_array)
492        .cloned()
493        .unwrap_or_default()
494}