Skip to main content

debugger/ipc/
protocol.rs

1//! IPC protocol message types
2//!
3//! Defines the request/response format for CLI ↔ daemon communication.
4//! Uses a simple length-prefixed JSON protocol.
5
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9use crate::common::error::IpcError;
10
11/// IPC request from CLI to daemon
12#[derive(Debug, Serialize, Deserialize)]
13pub struct Request {
14    /// Request ID for matching responses
15    pub id: u64,
16    /// The command to execute
17    pub command: Command,
18}
19
20/// IPC response from daemon to CLI
21#[derive(Debug, Serialize, Deserialize)]
22pub struct Response {
23    /// Request ID this response corresponds to
24    pub id: u64,
25    /// Whether the command succeeded
26    pub success: bool,
27    /// Result data on success
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub result: Option<serde_json::Value>,
30    /// Error information on failure
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub error: Option<IpcError>,
33}
34
35impl Response {
36    /// Create a success response
37    pub fn success(id: u64, result: serde_json::Value) -> Self {
38        Self {
39            id,
40            success: true,
41            result: Some(result),
42            error: None,
43        }
44    }
45
46    /// Create an error response
47    pub fn error(id: u64, error: IpcError) -> Self {
48        Self {
49            id,
50            success: false,
51            result: None,
52            error: Some(error),
53        }
54    }
55
56    /// Create a success response with no data
57    pub fn ok(id: u64) -> Self {
58        Self {
59            id,
60            success: true,
61            result: Some(serde_json::json!({})),
62            error: None,
63        }
64    }
65}
66
67/// Commands that can be sent from CLI to daemon
68#[derive(Debug, Serialize, Deserialize)]
69#[serde(tag = "type", rename_all = "snake_case")]
70pub enum Command {
71    // === Session Management ===
72    /// Start debugging a program
73    Start {
74        program: PathBuf,
75        args: Vec<String>,
76        adapter: Option<String>,
77        stop_on_entry: bool,
78        /// Initial breakpoints to set before program starts (file:line or function name)
79        #[serde(default)]
80        initial_breakpoints: Vec<String>,
81    },
82
83    /// Attach to a running process
84    Attach {
85        pid: u32,
86        adapter: Option<String>,
87    },
88
89    /// Detach from process (keeps it running)
90    Detach,
91
92    /// Stop debugging (terminates debuggee)
93    Stop,
94
95    /// Restart program with same arguments
96    Restart,
97
98    /// Get session status
99    Status,
100
101    // === Breakpoints ===
102    /// Add a breakpoint
103    BreakpointAdd {
104        location: BreakpointLocation,
105        condition: Option<String>,
106        hit_count: Option<u32>,
107    },
108
109    /// Remove a breakpoint
110    BreakpointRemove {
111        id: Option<u32>,
112        all: bool,
113    },
114
115    /// List all breakpoints
116    BreakpointList,
117
118    /// Enable a breakpoint
119    BreakpointEnable { id: u32 },
120
121    /// Disable a breakpoint
122    BreakpointDisable { id: u32 },
123
124    // === Execution Control ===
125    /// Continue execution
126    Continue,
127
128    /// Step over (next line, skip function calls)
129    Next,
130
131    /// Step into (next line, enter function calls)
132    StepIn,
133
134    /// Step out (run until function returns)
135    StepOut,
136
137    /// Pause execution
138    Pause,
139
140    // === State Inspection ===
141    /// Get stack trace
142    StackTrace {
143        thread_id: Option<i64>,
144        limit: usize,
145    },
146
147    /// Get local variables
148    Locals { frame_id: Option<i64> },
149
150    /// Evaluate expression
151    Evaluate {
152        expression: String,
153        frame_id: Option<i64>,
154        context: EvaluateContext,
155    },
156
157    /// Get scopes for a frame
158    Scopes { frame_id: i64 },
159
160    /// Get variables in a scope
161    Variables { reference: i64 },
162
163    // === Thread/Frame Management ===
164    /// List all threads
165    Threads,
166
167    /// Switch to thread
168    ThreadSelect { id: i64 },
169
170    /// Select stack frame
171    FrameSelect { number: usize },
172
173    /// Move up the stack (to caller)
174    FrameUp,
175
176    /// Move down the stack (toward current frame)
177    FrameDown,
178
179    // === Context ===
180    /// Get current position with source context
181    Context { lines: usize },
182
183    // === Async ===
184    /// Wait for next stop event
185    Await { timeout_secs: u64 },
186
187    // === Output ===
188    /// Get buffered output
189    GetOutput {
190        tail: Option<usize>,
191        clear: bool,
192    },
193
194    /// Subscribe to output events (for --follow)
195    SubscribeOutput,
196
197    // === Shutdown ===
198    /// Shutdown the daemon
199    Shutdown,
200}
201
202/// Breakpoint location specification
203#[derive(Debug, Clone, Serialize, Deserialize)]
204#[serde(tag = "type", rename_all = "snake_case")]
205pub enum BreakpointLocation {
206    /// File and line number
207    Line { file: PathBuf, line: u32 },
208    /// Function name
209    Function { name: String },
210}
211
212impl BreakpointLocation {
213    /// Parse a location string like "file.rs:42" or "main"
214    pub fn parse(s: &str) -> Result<Self, crate::common::Error> {
215        // Handle file:line format, careful with Windows paths like "C:\path\file.rs:10"
216        // Strategy: find the last ':' that's followed by digits only
217        if let Some(colon_idx) = s.rfind(':') {
218            let (file_part, line_part) = s.split_at(colon_idx);
219            let line_str = &line_part[1..]; // Skip the ':'
220
221            // Only treat as file:line if the part after ':' is a valid line number
222            if !line_str.is_empty() && line_str.chars().all(|c| c.is_ascii_digit()) {
223                let line: u32 = line_str.parse().map_err(|_| {
224                    crate::common::Error::InvalidLocation(format!(
225                        "invalid line number: {}",
226                        line_str
227                    ))
228                })?;
229                return Ok(Self::Line {
230                    file: PathBuf::from(file_part),
231                    line,
232                });
233            }
234        }
235
236        // No valid file:line pattern, treat as function name
237        Ok(Self::Function {
238            name: s.to_string(),
239        })
240    }
241}
242
243impl std::fmt::Display for BreakpointLocation {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        match self {
246            Self::Line { file, line } => write!(f, "{}:{}", file.display(), line),
247            Self::Function { name } => write!(f, "{}", name),
248        }
249    }
250}
251
252/// Context for expression evaluation
253#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
254#[serde(rename_all = "snake_case")]
255pub enum EvaluateContext {
256    /// Watch expression (read-only evaluation)
257    #[default]
258    Watch,
259    /// REPL evaluation (can have side effects)
260    Repl,
261    /// Hover evaluation
262    Hover,
263}
264
265// === Result types for responses ===
266
267/// Status response
268#[derive(Debug, Serialize, Deserialize)]
269pub struct StatusResult {
270    pub daemon_running: bool,
271    pub session_active: bool,
272    pub state: Option<String>,
273    pub program: Option<String>,
274    pub adapter: Option<String>,
275    pub stopped_thread: Option<i64>,
276    pub stopped_reason: Option<String>,
277}
278
279/// Breakpoint information
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct BreakpointInfo {
282    pub id: u32,
283    pub verified: bool,
284    pub source: Option<String>,
285    pub line: Option<u32>,
286    pub message: Option<String>,
287    pub enabled: bool,
288    pub condition: Option<String>,
289    pub hit_count: Option<u32>,
290}
291
292/// Stack frame information
293#[derive(Debug, Serialize, Deserialize)]
294pub struct StackFrameInfo {
295    pub id: i64,
296    pub name: String,
297    pub source: Option<String>,
298    pub line: Option<u32>,
299    pub column: Option<u32>,
300}
301
302/// Thread information
303#[derive(Debug, Serialize, Deserialize)]
304pub struct ThreadInfo {
305    pub id: i64,
306    pub name: String,
307    pub state: Option<String>,
308}
309
310/// Variable information
311#[derive(Debug, Serialize, Deserialize)]
312pub struct VariableInfo {
313    pub name: String,
314    pub value: String,
315    pub type_name: Option<String>,
316    pub variables_reference: i64,
317}
318
319/// Stop event result
320#[derive(Debug, Serialize, Deserialize)]
321pub struct StopResult {
322    pub reason: String,
323    pub description: Option<String>,
324    #[serde(default)]
325    pub thread_id: Option<i64>,
326    #[serde(default)]
327    pub all_threads_stopped: bool,
328    #[serde(default)]
329    pub hit_breakpoint_ids: Vec<u32>,
330    /// Current location info
331    pub source: Option<String>,
332    pub line: Option<u32>,
333    pub column: Option<u32>,
334}
335
336/// Evaluate result
337#[derive(Debug, Serialize, Deserialize)]
338pub struct EvaluateResult {
339    pub result: String,
340    pub type_name: Option<String>,
341    pub variables_reference: i64,
342}
343
344/// Context result with source code
345#[derive(Debug, Serialize, Deserialize)]
346pub struct ContextResult {
347    pub thread_id: i64,
348    pub source: Option<String>,
349    pub line: u32,
350    pub column: Option<u32>,
351    pub function: Option<String>,
352    /// Source lines with line numbers
353    pub source_lines: Vec<SourceLine>,
354    /// Local variables
355    pub locals: Vec<VariableInfo>,
356}
357
358/// A source line with its number
359#[derive(Debug, Serialize, Deserialize)]
360pub struct SourceLine {
361    pub number: u32,
362    pub content: String,
363    pub is_current: bool,
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    #[test]
371    fn test_parse_file_line() {
372        let loc = BreakpointLocation::parse("src/main.rs:42").unwrap();
373        match loc {
374            BreakpointLocation::Line { file, line } => {
375                assert_eq!(file, PathBuf::from("src/main.rs"));
376                assert_eq!(line, 42);
377            }
378            _ => panic!("Expected Line variant"),
379        }
380    }
381
382    #[test]
383    fn test_parse_function() {
384        let loc = BreakpointLocation::parse("main").unwrap();
385        match loc {
386            BreakpointLocation::Function { name } => {
387                assert_eq!(name, "main");
388            }
389            _ => panic!("Expected Function variant"),
390        }
391    }
392
393    #[test]
394    fn test_parse_namespaced_function() {
395        let loc = BreakpointLocation::parse("mymod::MyStruct::method").unwrap();
396        match loc {
397            BreakpointLocation::Function { name } => {
398                assert_eq!(name, "mymod::MyStruct::method");
399            }
400            _ => panic!("Expected Function variant"),
401        }
402    }
403
404    #[cfg(windows)]
405    #[test]
406    fn test_parse_windows_path() {
407        let loc = BreakpointLocation::parse(r"C:\Users\test\src\main.rs:42").unwrap();
408        match loc {
409            BreakpointLocation::Line { file, line } => {
410                assert_eq!(file, PathBuf::from(r"C:\Users\test\src\main.rs"));
411                assert_eq!(line, 42);
412            }
413            _ => panic!("Expected Line variant"),
414        }
415    }
416}