Skip to main content

debugger/cli/
mod.rs

1//! CLI command handling
2//!
3//! Dispatches CLI commands to the daemon and formats output.
4
5pub mod spawn;
6
7use crate::commands::{BreakpointCommands, Commands};
8use crate::common::{Error, Result};
9use crate::ipc::protocol::{
10    BreakpointInfo, BreakpointLocation, Command, ContextResult, EvaluateContext, EvaluateResult,
11    StackFrameInfo, StatusResult, StopResult, ThreadInfo, VariableInfo,
12};
13use crate::ipc::DaemonClient;
14use crate::setup;
15use crate::testing;
16
17/// Dispatch a CLI command
18pub async fn dispatch(command: Commands) -> Result<()> {
19    match command {
20        Commands::Daemon => {
21            // Should never happen - daemon mode is handled in main
22            unreachable!("Daemon command should be handled in main")
23        }
24
25        Commands::Start {
26            program,
27            args,
28            adapter,
29            stop_on_entry,
30            initial_breakpoints,
31        } => {
32            spawn::ensure_daemon_running().await?;
33            let mut client = DaemonClient::connect().await?;
34
35            let program = program.canonicalize().unwrap_or(program);
36
37            let has_initial_breakpoints = !initial_breakpoints.is_empty();
38
39            let _result = client
40                .send_command(Command::Start {
41                    program: program.clone(),
42                    args,
43                    adapter,
44                    stop_on_entry,
45                    initial_breakpoints: initial_breakpoints.clone(),
46                })
47                .await?;
48
49            println!("Started debugging: {}", program.display());
50
51            if has_initial_breakpoints {
52                println!("Set {} initial breakpoint(s)", initial_breakpoints.len());
53            }
54
55            if stop_on_entry || has_initial_breakpoints {
56                println!("Stopped at entry point. Use 'debugger continue' to run.");
57            } else {
58                println!("Program is running. Use 'debugger await' to wait for a stop.");
59            }
60
61            Ok(())
62        }
63
64        Commands::Attach { pid, adapter } => {
65            spawn::ensure_daemon_running().await?;
66            let mut client = DaemonClient::connect().await?;
67
68            client.send_command(Command::Attach { pid, adapter }).await?;
69
70            println!("Attached to process {}", pid);
71            println!("Program is stopped. Use 'debugger continue' to run.");
72
73            Ok(())
74        }
75
76        Commands::Breakpoint(bp_cmd) => match bp_cmd {
77            BreakpointCommands::Add {
78                location,
79                condition,
80                hit_count,
81            } => {
82                let mut client = DaemonClient::connect().await?;
83                let loc = BreakpointLocation::parse(&location)?;
84
85                let result = client
86                    .send_command(Command::BreakpointAdd {
87                        location: loc,
88                        condition,
89                        hit_count,
90                    })
91                    .await?;
92
93                let info: BreakpointInfo = serde_json::from_value(result)?;
94                print_breakpoint_added(&info);
95
96                Ok(())
97            }
98
99            BreakpointCommands::Remove { id, all } => {
100                let mut client = DaemonClient::connect().await?;
101
102                client
103                    .send_command(Command::BreakpointRemove { id, all })
104                    .await?;
105
106                if all {
107                    println!("All breakpoints removed");
108                } else if let Some(id) = id {
109                    println!("Breakpoint {} removed", id);
110                }
111
112                Ok(())
113            }
114
115            BreakpointCommands::List => {
116                let mut client = DaemonClient::connect().await?;
117
118                let result = client.send_command(Command::BreakpointList).await?;
119                let breakpoints: Vec<BreakpointInfo> =
120                    serde_json::from_value(result["breakpoints"].clone())?;
121
122                if breakpoints.is_empty() {
123                    println!("No breakpoints set");
124                } else {
125                    println!("Breakpoints:");
126                    for bp in &breakpoints {
127                        print_breakpoint(bp);
128                    }
129                }
130
131                Ok(())
132            }
133
134            BreakpointCommands::Enable { id } => {
135                let mut client = DaemonClient::connect().await?;
136                client
137                    .send_command(Command::BreakpointEnable { id })
138                    .await?;
139                println!("Breakpoint {} enabled", id);
140                Ok(())
141            }
142
143            BreakpointCommands::Disable { id } => {
144                let mut client = DaemonClient::connect().await?;
145                client
146                    .send_command(Command::BreakpointDisable { id })
147                    .await?;
148                println!("Breakpoint {} disabled", id);
149                Ok(())
150            }
151        },
152
153        Commands::Break { location, condition } => {
154            // Shorthand for breakpoint add
155            let mut client = DaemonClient::connect().await?;
156            let loc = BreakpointLocation::parse(&location)?;
157
158            let result = client
159                .send_command(Command::BreakpointAdd {
160                    location: loc,
161                    condition,
162                    hit_count: None,
163                })
164                .await?;
165
166            let info: BreakpointInfo = serde_json::from_value(result)?;
167            print_breakpoint_added(&info);
168
169            Ok(())
170        }
171
172        Commands::Continue => {
173            let mut client = DaemonClient::connect().await?;
174            client.send_command(Command::Continue).await?;
175            println!("Continuing execution...");
176            Ok(())
177        }
178
179        Commands::Next => {
180            let mut client = DaemonClient::connect().await?;
181            client.send_command(Command::Next).await?;
182            println!("Stepping over...");
183            Ok(())
184        }
185
186        Commands::Step => {
187            let mut client = DaemonClient::connect().await?;
188            client.send_command(Command::StepIn).await?;
189            println!("Stepping into...");
190            Ok(())
191        }
192
193        Commands::Finish => {
194            let mut client = DaemonClient::connect().await?;
195            client.send_command(Command::StepOut).await?;
196            println!("Stepping out...");
197            Ok(())
198        }
199
200        Commands::Pause => {
201            let mut client = DaemonClient::connect().await?;
202            client.send_command(Command::Pause).await?;
203            println!("Pausing execution...");
204            Ok(())
205        }
206
207        Commands::Backtrace { limit, locals } => {
208            let mut client = DaemonClient::connect().await?;
209
210            let result = client
211                .send_command(Command::StackTrace {
212                    thread_id: None,
213                    limit,
214                })
215                .await?;
216
217            let frames: Vec<StackFrameInfo> = serde_json::from_value(result["frames"].clone())?;
218
219            if frames.is_empty() {
220                println!("No stack frames");
221            } else {
222                for (i, frame) in frames.iter().enumerate() {
223                    let source = frame.source.as_deref().unwrap_or("?");
224                    let line = frame.line.map(|l| l.to_string()).unwrap_or_else(|| "?".to_string());
225                    println!("#{} {} at {}:{}", i, frame.name, source, line);
226
227                    if locals {
228                        // Get locals for this frame
229                        let locals_result = client
230                            .send_command(Command::Locals {
231                                frame_id: Some(frame.id),
232                            })
233                            .await;
234
235                        if let Ok(result) = locals_result {
236                            if let Ok(vars) =
237                                serde_json::from_value::<Vec<VariableInfo>>(result["variables"].clone())
238                            {
239                                for var in vars {
240                                    println!(
241                                        "    {} = {}{}",
242                                        var.name,
243                                        var.value,
244                                        var.type_name
245                                            .map(|t| format!(" ({})", t))
246                                            .unwrap_or_default()
247                                    );
248                                }
249                            }
250                        }
251                    }
252                }
253            }
254
255            Ok(())
256        }
257
258        Commands::Locals => {
259            let mut client = DaemonClient::connect().await?;
260
261            let result = client
262                .send_command(Command::Locals { frame_id: None })
263                .await?;
264
265            let vars: Vec<VariableInfo> = serde_json::from_value(result["variables"].clone())?;
266
267            if vars.is_empty() {
268                println!("No local variables");
269            } else {
270                println!("Local variables:");
271                for var in &vars {
272                    println!(
273                        "  {} = {}{}",
274                        var.name,
275                        var.value,
276                        var.type_name
277                            .as_ref()
278                            .map(|t| format!(" ({})", t))
279                            .unwrap_or_default()
280                    );
281                }
282            }
283
284            Ok(())
285        }
286
287        Commands::Print { expression } => {
288            let mut client = DaemonClient::connect().await?;
289
290            let result = client
291                .send_command(Command::Evaluate {
292                    expression: expression.clone(),
293                    frame_id: None,
294                    context: EvaluateContext::Watch,
295                })
296                .await?;
297
298            let eval: EvaluateResult = serde_json::from_value(result)?;
299            println!(
300                "{} = {}{}",
301                expression,
302                eval.result,
303                eval.type_name.map(|t| format!(" ({})", t)).unwrap_or_default()
304            );
305
306            Ok(())
307        }
308
309        Commands::Eval { expression } => {
310            let mut client = DaemonClient::connect().await?;
311
312            let result = client
313                .send_command(Command::Evaluate {
314                    expression: expression.clone(),
315                    frame_id: None,
316                    context: EvaluateContext::Repl,
317                })
318                .await?;
319
320            let eval: EvaluateResult = serde_json::from_value(result)?;
321            println!("{}", eval.result);
322
323            Ok(())
324        }
325
326        Commands::Context { lines } => {
327            let mut client = DaemonClient::connect().await?;
328
329            let result = client.send_command(Command::Context { lines }).await?;
330
331            let ctx: ContextResult = serde_json::from_value(result)?;
332
333            // Print header
334            if let Some(source) = &ctx.source {
335                println!(
336                    "Thread {} stopped at {}:{}",
337                    ctx.thread_id, source, ctx.line
338                );
339            }
340            if let Some(func) = &ctx.function {
341                println!("In function: {}", func);
342            }
343            println!();
344
345            // Print source with line numbers
346            for line in &ctx.source_lines {
347                let marker = if line.is_current { "->" } else { "  " };
348                println!("{} {:>4} | {}", marker, line.number, line.content);
349            }
350
351            // Print locals
352            if !ctx.locals.is_empty() {
353                println!();
354                println!("Locals:");
355                for var in &ctx.locals {
356                    println!(
357                        "  {} = {}{}",
358                        var.name,
359                        var.value,
360                        var.type_name
361                            .as_ref()
362                            .map(|t| format!(" ({})", t))
363                            .unwrap_or_default()
364                    );
365                }
366            }
367
368            Ok(())
369        }
370
371        Commands::Threads => {
372            let mut client = DaemonClient::connect().await?;
373
374            let result = client.send_command(Command::Threads).await?;
375            let threads: Vec<ThreadInfo> = serde_json::from_value(result["threads"].clone())?;
376
377            if threads.is_empty() {
378                println!("No threads");
379            } else {
380                println!("Threads:");
381                for thread in &threads {
382                    println!("  {} - {}", thread.id, thread.name);
383                }
384            }
385
386            Ok(())
387        }
388
389        Commands::Thread { id } => {
390            let mut client = DaemonClient::connect().await?;
391
392            if let Some(id) = id {
393                client
394                    .send_command(Command::ThreadSelect { id })
395                    .await?;
396                println!("Switched to thread {}", id);
397            } else {
398                // Show current thread info
399                let result = client.send_command(Command::Status).await?;
400                let status: StatusResult = serde_json::from_value(result)?;
401                if let Some(thread_id) = status.stopped_thread {
402                    println!("Current thread: {}", thread_id);
403                } else {
404                    println!("No thread selected");
405                }
406            }
407
408            Ok(())
409        }
410
411        Commands::Frame { number } => {
412            let mut client = DaemonClient::connect().await?;
413
414            if let Some(n) = number {
415                client
416                    .send_command(Command::FrameSelect { number: n })
417                    .await?;
418                println!("Switched to frame {}", n);
419            } else {
420                println!("Current frame: 0 (use 'debugger backtrace' to see all frames)");
421            }
422
423            Ok(())
424        }
425
426        Commands::Up => {
427            let mut client = DaemonClient::connect().await?;
428            let result = client.send_command(Command::FrameUp).await?;
429            print_frame_nav_result(&result);
430            Ok(())
431        }
432
433        Commands::Down => {
434            let mut client = DaemonClient::connect().await?;
435            let result = client.send_command(Command::FrameDown).await?;
436            print_frame_nav_result(&result);
437            Ok(())
438        }
439
440        Commands::Await { timeout } => {
441            let mut client = DaemonClient::connect().await?;
442
443            println!("Waiting for program to stop (timeout: {}s)...", timeout);
444
445            let result = client
446                .send_command(Command::Await {
447                    timeout_secs: timeout,
448                })
449                .await?;
450
451            // Check if we got a stop result or already stopped
452            if result.get("already_stopped").and_then(|v| v.as_bool()).unwrap_or(false) {
453                let reason = result["reason"].as_str().unwrap_or("unknown");
454                println!("Program was already stopped: {}", reason);
455            } else if let Some(reason) = result.get("reason").and_then(|v| v.as_str()) {
456                match reason {
457                    "exited" => {
458                        let code = result["exit_code"].as_i64().unwrap_or(0);
459                        println!("Program exited with code {}", code);
460                    }
461                    "terminated" => {
462                        println!("Program terminated");
463                    }
464                    _ => {
465                        let stop: StopResult = serde_json::from_value(result)?;
466                        print_stop_result(&stop);
467                    }
468                }
469            }
470
471            Ok(())
472        }
473
474        Commands::Output { follow, tail, clear } => {
475            let mut client = DaemonClient::connect().await?;
476
477            if follow {
478                println!("Output streaming not yet implemented");
479                return Ok(());
480            }
481
482            let result = client
483                .send_command(Command::GetOutput { tail, clear })
484                .await?;
485
486            let output = result["output"].as_str().unwrap_or("");
487            if output.is_empty() {
488                println!("(no output)");
489            } else {
490                print!("{}", output);
491            }
492
493            Ok(())
494        }
495
496        Commands::Status => {
497            match DaemonClient::connect().await {
498                Ok(mut client) => {
499                    let result = client.send_command(Command::Status).await?;
500                    let status: StatusResult = serde_json::from_value(result)?;
501
502                    println!("Daemon: running");
503                    if status.session_active {
504                        println!("Session: active");
505                        if let Some(program) = status.program {
506                            println!("Program: {}", program);
507                        }
508                        if let Some(adapter) = status.adapter {
509                            println!("Adapter: {}", adapter);
510                        }
511                        if let Some(state) = status.state {
512                            println!("State: {}", state);
513                        }
514                        if let Some(reason) = status.stopped_reason {
515                            println!("Stopped reason: {}", reason);
516                        }
517                        if let Some(thread) = status.stopped_thread {
518                            println!("Stopped thread: {}", thread);
519                        }
520                    } else {
521                        println!("Session: none");
522                    }
523                }
524                Err(Error::DaemonNotRunning) => {
525                    println!("Daemon: not running");
526                    println!("Session: none");
527                }
528                Err(e) => return Err(e),
529            }
530
531            Ok(())
532        }
533
534        Commands::Stop => {
535            let mut client = DaemonClient::connect().await?;
536            client.send_command(Command::Stop).await?;
537            println!("Debug session stopped");
538            Ok(())
539        }
540
541        Commands::Detach => {
542            let mut client = DaemonClient::connect().await?;
543            client.send_command(Command::Detach).await?;
544            println!("Detached from process (process continues running)");
545            Ok(())
546        }
547
548        Commands::Restart => {
549            let mut client = DaemonClient::connect().await?;
550            client.send_command(Command::Restart).await?;
551            println!("Program restarted");
552            Ok(())
553        }
554
555        Commands::Logs { lines, follow, clear } => {
556            use crate::common::logging;
557
558            let log_path = logging::daemon_log_path();
559
560            if let Some(path) = log_path {
561                if clear {
562                    logging::truncate_daemon_log()?;
563                    println!("Daemon log cleared: {}", path.display());
564                    return Ok(());
565                }
566
567                if !path.exists() {
568                    println!("No daemon log file found at: {}", path.display());
569                    println!("The daemon may not have been started yet.");
570                    return Ok(());
571                }
572
573                if follow {
574                    println!("Following daemon log: {} (Ctrl+C to stop)", path.display());
575                    println!("---");
576                    // Use tail -f for following
577                    let status = std::process::Command::new("tail")
578                        .args(["-f", "-n", &lines.to_string()])
579                        .arg(&path)
580                        .status();
581
582                    match status {
583                        Ok(_) => {}
584                        Err(e) => {
585                            eprintln!("Failed to follow log: {}", e);
586                        }
587                    }
588                } else {
589                    // Read last N lines
590                    let content = std::fs::read_to_string(&path)?;
591                    let all_lines: Vec<&str> = content.lines().collect();
592                    let start = all_lines.len().saturating_sub(lines);
593
594                    println!("Daemon log: {} (last {} lines)", path.display(), lines);
595                    println!("---");
596                    for line in &all_lines[start..] {
597                        println!("{}", line);
598                    }
599
600                    if all_lines.is_empty() {
601                        println!("(log is empty)");
602                    }
603                }
604            } else {
605                println!("Could not determine log file path");
606            }
607
608            Ok(())
609        }
610
611        Commands::Setup {
612            debugger,
613            version,
614            list,
615            check,
616            auto_detect,
617            uninstall,
618            path,
619            force,
620            dry_run,
621            json,
622        } => {
623            let opts = setup::SetupOptions {
624                debugger,
625                version,
626                list,
627                check,
628                auto_detect,
629                uninstall,
630                path,
631                force,
632                dry_run,
633                json,
634            };
635            setup::run(opts).await
636        }
637
638        Commands::Test { path, verbose } => {
639            let result = testing::run_scenario(&path, verbose).await?;
640
641            if result.passed {
642                std::process::exit(0);
643            } else {
644                std::process::exit(1);
645            }
646        }
647    }
648}
649
650/// Print the result of a frame navigation command (up/down)
651fn print_frame_nav_result(result: &serde_json::Value) {
652    let frame_index = result["selected"].as_u64().unwrap_or(0);
653
654    if let Ok(frame_info) = serde_json::from_value::<StackFrameInfo>(result["frame"].clone()) {
655        let source = frame_info.source.as_deref().unwrap_or("?");
656        let line = frame_info
657            .line
658            .map(|l| l.to_string())
659            .unwrap_or_else(|| "?".to_string());
660        println!("#{} {} at {}:{}", frame_index, frame_info.name, source, line);
661    } else {
662        println!("Switched to frame {}", frame_index);
663    }
664}
665
666fn print_breakpoint_added(info: &BreakpointInfo) {
667    if info.verified {
668        println!(
669            "Breakpoint {} set at {}:{}",
670            info.id,
671            info.source.as_deref().unwrap_or("?"),
672            info.line.map(|l| l.to_string()).unwrap_or_else(|| "?".to_string())
673        );
674    } else {
675        println!(
676            "Breakpoint {} pending{}",
677            info.id,
678            info.message.as_ref().map(|m| format!(": {}", m)).unwrap_or_default()
679        );
680    }
681}
682
683fn print_breakpoint(info: &BreakpointInfo) {
684    let status = if info.enabled {
685        if info.verified { "✓" } else { "?" }
686    } else {
687        "○"
688    };
689
690    let location = match (&info.source, info.line) {
691        (Some(source), Some(line)) => format!("{}:{}", source, line),
692        (Some(source), None) => source.clone(),
693        (None, Some(line)) => format!(":{}", line),
694        (None, None) => "unknown".to_string(),
695    };
696
697    let extras = [
698        info.condition.as_ref().map(|c| format!("if {}", c)),
699        info.hit_count.map(|n| format!("hits: {}", n)),
700        info.message.clone(),
701    ]
702    .into_iter()
703    .flatten()
704    .collect::<Vec<_>>()
705    .join(", ");
706
707    if extras.is_empty() {
708        println!("  {} {} {}", status, info.id, location);
709    } else {
710        println!("  {} {} {} ({})", status, info.id, location, extras);
711    }
712}
713
714fn print_stop_result(stop: &StopResult) {
715    match stop.reason.as_str() {
716        "breakpoint" => {
717            println!("Stopped at breakpoint");
718            if !stop.hit_breakpoint_ids.is_empty() {
719                println!("  Breakpoint IDs: {:?}", stop.hit_breakpoint_ids);
720            }
721        }
722        "step" => {
723            println!("Step completed");
724        }
725        "exception" | "signal" => {
726            println!(
727                "Stopped: {}",
728                stop.description.as_deref().unwrap_or(&stop.reason)
729            );
730        }
731        "pause" => {
732            println!("Paused");
733        }
734        "entry" => {
735            println!("Stopped at entry point");
736        }
737        _ => {
738            println!("Stopped: {}", stop.reason);
739        }
740    }
741
742    if let (Some(source), Some(line)) = (&stop.source, stop.line) {
743        println!("  Location: {}:{}", source, line);
744    }
745}