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