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