Skip to main content

harn_dap/
lib.rs

1//! Harn debug adapter (DAP).
2//!
3//! Exposed as a library so the single multi-call `harn` binary can dispatch
4//! into the debug adapter when launched under the `harn-dap` name (see
5//! `harn-cli`'s `main`), instead of shipping a second fully-linked binary.
6//! The thin `src/main.rs` shim keeps `harn-dap` buildable as its own binary.
7
8#![recursion_limit = "256"]
9
10mod debugger;
11mod host_bridge;
12mod protocol;
13
14use std::io::{self, BufRead, Read, Write};
15use std::sync::atomic::AtomicI64;
16use std::sync::mpsc::{channel, Sender};
17use std::sync::{Arc, Mutex};
18use std::thread;
19
20use debugger::Debugger;
21use host_bridge::{deliver_reply, pending_map_new, DapHostBridge, DapHostCallReply, PendingMap};
22use protocol::{DapMessage, DapResponse};
23
24const MAX_DAP_FRAME_BYTES: usize = 16 * 1024 * 1024;
25
26/// Run the Harn debug adapter over stdio until the client disconnects.
27///
28/// Called by the `harn-dap` binary shim and by the `harn` multi-call binary
29/// when invoked as `harn-dap`.
30pub fn run() {
31    // Defeat rlib dead-code stripping of the linkme distributed slice
32    // (linkme issue #36) before reading `all_builtin_signatures()`.
33    harn_vm::stdlib::force_link();
34    // Install the macro-emitted builtin signature slice into the parser
35    // registry so source-file typechecking inside DAP launch covers
36    // `#[harn_builtin]`-migrated entries.
37    harn_parser::install_builtin_signatures(harn_vm::stdlib::all_builtin_signatures());
38
39    // Shared seq counter spans both forward responses (debugger.next_seq)
40    // and reverse requests (DapHostBridge.next_seq) so every adapter-
41    // initiated message uses a globally unique seq, matching the DAP spec.
42    let seq = Arc::new(AtomicI64::new(1_000_000));
43
44    // Stdout writer behind a mutex — both the main response loop and the
45    // host bridge serialize their writes here.
46    let stdout: Arc<Mutex<Box<dyn Write + Send>>> = Arc::new(Mutex::new(Box::new(io::stdout())));
47    let pending: PendingMap = pending_map_new();
48
49    // Stdin reader runs on its own OS thread so the bridge can block on
50    // reverse-request replies without starving the read loop.
51    let (request_tx, request_rx) = channel::<DapMessage>();
52    let pending_for_reader = Arc::clone(&pending);
53    thread::spawn(move || stdin_reader(request_tx, pending_for_reader));
54
55    let bridge = Arc::new(DapHostBridge::new(
56        Arc::clone(&seq),
57        Arc::clone(&stdout),
58        Arc::clone(&pending),
59    ));
60
61    let mut debugger = Debugger::new();
62    debugger.attach_host_bridge(Arc::clone(&bridge));
63
64    // Interleaved drive loop. Two phases per iteration:
65    //   1. Drain any pending DAP messages from the channel (try_recv —
66    //      non-blocking) so commands like pause / disconnect /
67    //      setBreakpoints get serviced even mid-run.
68    //   2. If the debugger is in a "running" state (after continue /
69    //      next / stepIn / stepOut / configurationDone), take ONE VM
70    //      step and emit any events. Otherwise block waiting for the
71    //      next message — we don't busy-loop while idle.
72    //
73    // This is what makes pause work during long scripts. The previous
74    // model called run_to_breakpoint() inside handle_continue, which
75    // monopolized the main thread until the VM voluntarily stopped;
76    // any pause / disconnect arriving in the meantime sat in the
77    // channel ignored.
78    use std::sync::mpsc::TryRecvError;
79    loop {
80        // Phase 1: drain pending messages.
81        loop {
82            match request_rx.try_recv() {
83                Ok(msg) => {
84                    let responses = debugger.handle_message(msg);
85                    for response in responses {
86                        send_response(&stdout, &response);
87                    }
88                }
89                Err(TryRecvError::Empty) => break,
90                Err(TryRecvError::Disconnected) => return,
91            }
92        }
93        // Phase 2: step the VM if running, else block on next message.
94        if debugger.is_running() {
95            let responses = debugger.step_running_vm();
96            for response in responses {
97                send_response(&stdout, &response);
98            }
99        } else {
100            match request_rx.recv() {
101                Ok(msg) => {
102                    let responses = debugger.handle_message(msg);
103                    for response in responses {
104                        send_response(&stdout, &response);
105                    }
106                }
107                Err(_) => return,
108            }
109        }
110    }
111}
112
113/// Stdin reader: parses LSP-framed DAP messages and demuxes by `type`.
114/// `request` and `event`-typed frames flow into the debugger via
115/// `request_tx`. `response`-typed frames are matched against pending
116/// reverse requests and routed into the bridge's reply channels.
117fn stdin_reader(request_tx: Sender<DapMessage>, pending: PendingMap) {
118    let stdin = io::stdin();
119    let mut reader = io::BufReader::new(stdin.lock());
120
121    loop {
122        let content_length = match read_content_length(&mut reader) {
123            Ok(Some(content_length)) => content_length,
124            Ok(None) => break,
125            Err(error) => {
126                eprintln!("Failed to read DAP frame header: {error}");
127                break;
128            }
129        };
130        if content_length == 0 {
131            continue;
132        }
133        let mut body_bytes = vec![0u8; content_length];
134        if reader.read_exact(&mut body_bytes).is_err() {
135            break;
136        }
137        let body = String::from_utf8_lossy(&body_bytes);
138
139        match serde_json::from_str::<DapMessage>(&body) {
140            Ok(msg) => {
141                if msg.msg_type == "response" {
142                    if let Some(request_seq) = msg.request_seq {
143                        deliver_reply(
144                            &pending,
145                            request_seq,
146                            DapHostCallReply {
147                                success: msg.success.unwrap_or(false),
148                                body: msg.body,
149                                message: msg.message,
150                            },
151                        );
152                        continue;
153                    }
154                }
155                if request_tx.send(msg).is_err() {
156                    break;
157                }
158            }
159            Err(e) => {
160                eprintln!("Failed to parse DAP message: {e}");
161                eprintln!("Body: {body}");
162            }
163        }
164    }
165}
166
167fn read_content_length<R: BufRead>(reader: &mut R) -> io::Result<Option<usize>> {
168    let mut content_length = 0usize;
169    loop {
170        let mut line = String::new();
171        match reader.read_line(&mut line) {
172            Ok(0) => return Ok(None),
173            Ok(_) => {
174                let trimmed = line.trim();
175                if trimmed.is_empty() {
176                    if content_length > 0 {
177                        return Ok(Some(content_length));
178                    }
179                    continue;
180                }
181                if let Some((name, val)) = trimmed.split_once(':') {
182                    if !name.eq_ignore_ascii_case("Content-Length") {
183                        continue;
184                    }
185                    if let Ok(len) = val.trim().parse::<usize>() {
186                        content_length = bounded_dap_content_length(len)?;
187                    }
188                }
189            }
190            Err(error) => return Err(error),
191        }
192    }
193}
194
195fn bounded_dap_content_length(content_length: usize) -> io::Result<usize> {
196    if content_length > MAX_DAP_FRAME_BYTES {
197        Err(io::Error::new(
198            io::ErrorKind::InvalidData,
199            format!(
200                "DAP Content-Length {content_length} exceeds limit {MAX_DAP_FRAME_BYTES} bytes"
201            ),
202        ))
203    } else {
204        Ok(content_length)
205    }
206}
207
208fn send_response(stdout: &Arc<Mutex<Box<dyn Write + Send>>>, response: &DapResponse) {
209    let body = match serde_json::to_string(response) {
210        Ok(b) => b,
211        Err(_) => return,
212    };
213    let header = format!("Content-Length: {}\r\n\r\n", body.len());
214    let mut guard = match stdout.lock() {
215        Ok(g) => g,
216        Err(_) => return,
217    };
218    let _ = guard.write_all(header.as_bytes());
219    let _ = guard.write_all(body.as_bytes());
220    let _ = guard.flush();
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn read_content_length_accepts_case_insensitive_header() {
229        let mut reader = io::BufReader::new(b"content-length: 12\r\n\r\n".as_slice());
230        assert_eq!(
231            read_content_length(&mut reader).expect("content length"),
232            Some(12)
233        );
234    }
235
236    #[test]
237    fn read_content_length_rejects_oversized_frame() {
238        let header = format!("Content-Length: {}\r\n\r\n", MAX_DAP_FRAME_BYTES + 1);
239        let mut reader = io::BufReader::new(header.as_bytes());
240        let error = read_content_length(&mut reader).expect_err("oversized frame");
241        assert_eq!(error.kind(), io::ErrorKind::InvalidData);
242    }
243}