1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
pub mod canonical;
pub mod debugpy_proto;
pub mod delve;
pub mod delve_proto;
pub mod dotnettrace;
pub mod callgrind;
pub mod ghci;
pub mod ghcprof;
pub mod jdb;
pub mod jitdasm;
pub mod lldb;
pub mod lldb_dap_proto;
pub mod netcoredbg;
pub mod netcoredbg_proto;
pub mod massif;
pub mod memcheck;
pub mod node_proto;
pub mod nodeprof;
pub mod ocamldebug;
pub mod perf;
pub mod pdb;
pub mod phpdbg;
pub mod pprof;
pub mod pstats;
pub mod rdbg;
pub mod stackprof;
pub mod xdebug;
use std::collections::HashMap;
// Re-export dependency types from shared crate for backwards compatibility
pub use dbg_cli::deps::{Dependency, DependencyCheck, DepStatus};
pub use canonical::{BreakId, BreakLoc, CanonicalOps, CanonicalReq};
/// Result of cleaning debugger output.
pub struct CleanResult {
pub output: String,
#[cfg_attr(not(test), allow(dead_code))]
pub events: Vec<String>,
}
/// Configuration for spawning a debugger process.
pub struct SpawnConfig {
pub bin: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
/// Commands to run after spawn before the session is ready.
pub init_commands: Vec<String>,
}
/// The backend trait. Implement this for each debugger.
pub trait Backend: Send + Sync {
/// Human-readable name for this backend.
fn name(&self) -> &'static str;
/// Short description shown in backend listing.
fn description(&self) -> &'static str;
/// The type names this backend handles.
fn types(&self) -> &'static [&'static str];
/// How to spawn the debugger for the given target.
fn spawn_config(&self, target: &str, args: &[String]) -> anyhow::Result<SpawnConfig>;
/// Regex matching the debugger's ready-for-input prompt.
fn prompt_pattern(&self) -> &str;
/// Dependencies this backend requires.
fn dependencies(&self) -> Vec<Dependency>;
/// Runtime preflight check — called before fork/spawn. Use this
/// for conditions that aren't "is this binary installed?" but still
/// need to hold for the backend to work: kernel settings, perf
/// paranoia level, capabilities, writable cwd, etc. Default: ok.
fn preflight(&self) -> anyhow::Result<()> {
Ok(())
}
/// Format a breakpoint spec for this debugger.
/// Profiler backends that don't support breakpoints return empty by default.
fn format_breakpoint(&self, _spec: &str) -> String {
String::new()
}
/// The command to start/continue execution.
fn run_command(&self) -> &'static str;
/// The command to quit the debugger.
fn quit_command(&self) -> &'static str {
"quit"
}
/// The command to request help from the debugger.
fn help_command(&self) -> &'static str {
"help"
}
/// Adapter markdown files for AI skill integration.
fn adapters(&self) -> Vec<(&'static str, &'static str)>;
/// Parse raw help output into compact command list.
fn parse_help(&self, raw: &str) -> String;
/// Path to a Speedscope JSON file produced after init commands complete.
fn profile_output(&self) -> Option<String> {
None
}
/// Wall-clock deadline for each init command (see `SpawnConfig::init_commands`).
/// Defaults to the daemon's `CMD_TIMEOUT` (60s). Profiling backends that
/// wrap a slow child (cProfile, massif on a heavy program, …) override
/// this with a longer budget so the session doesn't die with
/// "Connection reset by peer" before the underlying tool finishes.
fn init_timeout(&self) -> std::time::Duration {
std::time::Duration::from_secs(60)
}
/// Clean noise from command output.
fn clean(&self, cmd: &str, output: &str) -> CleanResult {
let _ = cmd;
CleanResult {
output: output.to_string(),
events: vec![],
}
}
/// Canonical-operations hook. Debug backends that implement the
/// `CanonicalOps` trait should override this to return `Some(self)`.
/// Profiler backends and backends not yet ported return `None`; the
/// canonical dispatcher will surface a clear "canonical ops not
/// available for <tool>" to the agent in that case.
fn canonical_ops(&self) -> Option<&dyn CanonicalOps> {
None
}
/// Override to route through the V8 Inspector transport instead
/// of the default PTY transport. Only `node-proto` returns true
/// today; future protocol backends (DAP) add their own hooks.
fn uses_inspector(&self) -> bool {
false
}
/// Override to route through the DAP transport. When true, the
/// backend must provide `dap_launch` to describe how to spawn
/// the adapter and configure `launch`. The daemon calls this
/// instead of `spawn_config`.
fn uses_dap(&self) -> bool {
false
}
/// Adapter spawn + launch configuration for DAP backends. Only
/// invoked when `uses_dap()` returns true.
fn dap_launch(&self, _target: &str, _args: &[String]) -> anyhow::Result<crate::dap::DapLaunchConfig> {
anyhow::bail!("dap_launch not implemented for this backend")
}
/// Attach-mode variant of `dap_launch`. Daemon invokes this when
/// the user passed `--attach-pid` / `--attach-port`. The
/// `AttachSpec` carries whichever identifier the adapter expects.
/// Default bails: not every adapter supports attach.
fn dap_attach(&self, _spec: &AttachSpec) -> anyhow::Result<crate::dap::DapLaunchConfig> {
anyhow::bail!("attach not implemented for this backend")
}
}
/// How to locate a running debuggee for attach mode.
#[derive(Clone, Debug, Default)]
pub struct AttachSpec {
pub pid: Option<u32>,
}
/// Path to the current dbg binary, for exec-ing into sub-REPLs.
pub fn self_exe() -> String {
std::env::current_exe()
.unwrap_or_else(|_| "dbg".into())
.display()
.to_string()
}
/// Escape a string for safe interpolation into a bash command.
/// Wraps in single quotes and escapes embedded single quotes.
pub fn shell_escape(s: &str) -> String {
if s.is_empty() {
return "''".to_string();
}
// If the string is simple (no special chars), return as-is
if s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'/' || b == b'-' || b == b'_' || b == b'=' || b == b':') {
return s.to_string();
}
// Wrap in single quotes, escaping existing single quotes as '\''
format!("'{}'", s.replace('\'', "'\\''"))
}
/// Registry of all available backends.
pub struct Registry {
backends: Vec<Box<dyn Backend>>,
type_map: HashMap<String, usize>,
}
impl Registry {
pub fn new() -> Self {
Self {
backends: Vec::new(),
type_map: HashMap::new(),
}
}
pub fn register(&mut self, backend: Box<dyn Backend>) {
let idx = self.backends.len();
self.type_map.insert(backend.name().to_string(), idx);
for t in backend.types() {
self.type_map.insert(t.to_string(), idx);
}
self.backends.push(backend);
}
pub fn get(&self, type_name: &str) -> Option<&dyn Backend> {
self.type_map
.get(type_name)
.map(|&idx| self.backends[idx].as_ref())
}
pub fn available_types(&self) -> Vec<&str> {
let mut types: Vec<&str> = self.type_map.keys().map(|s| s.as_str()).collect();
types.sort();
types
}
pub fn all_backends(&self) -> &[Box<dyn Backend>] {
&self.backends
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shell_escape_simple_passthrough() {
assert_eq!(shell_escape("./myapp"), "./myapp");
assert_eq!(shell_escape("target/debug/foo"), "target/debug/foo");
assert_eq!(shell_escape("a-b_c.d"), "a-b_c.d");
}
#[test]
fn shell_escape_empty() {
assert_eq!(shell_escape(""), "''");
}
#[test]
fn shell_escape_spaces() {
assert_eq!(shell_escape("my app"), "'my app'");
assert_eq!(shell_escape("/path/to/my app"), "'/path/to/my app'");
}
#[test]
fn shell_escape_single_quotes() {
assert_eq!(shell_escape("it's"), "'it'\\''s'");
}
#[test]
fn shell_escape_special_chars() {
assert_eq!(shell_escape("$(rm -rf /)"), "'$(rm -rf /)'");
assert_eq!(shell_escape("foo;bar"), "'foo;bar'");
assert_eq!(shell_escape("a&b"), "'a&b'");
assert_eq!(shell_escape("a|b"), "'a|b'");
}
#[test]
fn shell_escape_backticks() {
assert_eq!(shell_escape("`whoami`"), "'`whoami`'");
}
}