1use serde_json::{json, Value};
20use std::collections::HashMap;
21use std::io::{self, BufRead, BufReader, Read, Write};
22use std::net::TcpStream;
23use std::process::{Child, ChildStdout, Command, Stdio};
24use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
25use std::sync::{Arc, Condvar, Mutex, OnceLock};
26use std::thread;
27use std::time::Duration;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum DebugAction {
34 Continue,
36 StepOver,
40 StepIn,
42 StepOut,
44 Quit,
46}
47
48#[derive(Debug, Clone, Default)]
51pub struct PauseSnapshot {
52 pub reason: String, pub file: String,
55 pub line: u32,
57}
58
59#[derive(Debug, Default)]
63pub struct BreakpointState {
64 pub line_breakpoints: HashMap<String, Vec<u32>>,
66}
67
68struct DapSharedInner {
69 pending_action: Option<DebugAction>,
70 is_paused: bool,
71 pause_request: bool, step_mode: bool, }
74
75pub struct DapShared {
79 inner: Mutex<DapSharedInner>,
81 cv: Condvar,
83 seq: AtomicU64,
85 writer: Mutex<Box<dyn Write + Send>>,
88 pub configuration_done: AtomicBool,
90 pub disconnected: AtomicBool,
92 pub program: Mutex<String>,
95}
96
97impl DapShared {
98 fn new(writer: Box<dyn Write + Send>) -> Arc<Self> {
99 Arc::new(Self {
100 inner: Mutex::new(DapSharedInner {
101 pending_action: None,
102 is_paused: false,
103 pause_request: false,
104 step_mode: false,
105 }),
106 cv: Condvar::new(),
107 seq: AtomicU64::new(1),
108 writer: Mutex::new(writer),
109 configuration_done: AtomicBool::new(false),
110 disconnected: AtomicBool::new(false),
111 program: Mutex::new(String::new()),
112 })
113 }
114
115 pub fn pause(&self, snap: PauseSnapshot) -> DebugAction {
120 let _ = io::Write::flush(&mut io::stdout());
124 let _ = io::Write::flush(&mut io::stderr());
125 tracing::info!(
126 target: "zshrs::dap::pause",
127 reason = %snap.reason,
128 file = %snap.file,
129 line = snap.line,
130 "executor PAUSED (cv-wait)",
131 );
132 {
133 let mut s = self.inner.lock().expect("dap lock");
134 s.is_paused = true;
135 s.pending_action = None;
136 s.pause_request = false;
137 }
138 let _ = self.emit_event(
139 "stopped",
140 json!({
141 "reason": snap.reason,
142 "threadId": 1,
143 "allThreadsStopped": true,
144 "preserveFocusHint": false,
145 "description": snap.reason,
146 "text": format!("{}:{}", snap.file, snap.line),
147 }),
148 );
149 let mut guard = self.inner.lock().expect("dap lock");
150 while guard.pending_action.is_none() && !self.disconnected.load(Ordering::SeqCst) {
151 guard = self.cv.wait(guard).expect("dap cv");
152 }
153 let action = guard.pending_action.take().unwrap_or(DebugAction::Continue);
154 guard.is_paused = false;
155 guard.step_mode = matches!(action, DebugAction::StepOver | DebugAction::StepIn);
157 tracing::info!(
158 target: "zshrs::dap::pause",
159 ?action,
160 step_mode = guard.step_mode,
161 "executor RESUMED",
162 );
163 action
164 }
165
166 fn resume_with(&self, action: DebugAction) {
167 let mut g = self.inner.lock().expect("dap lock");
168 g.pending_action = Some(action);
169 self.cv.notify_all();
170 }
171
172 fn request_pause(&self) {
173 let mut g = self.inner.lock().expect("dap lock");
174 g.pause_request = true;
175 }
176
177 fn want_pause(&self) -> bool {
178 self.inner.lock().map(|g| g.pause_request).unwrap_or(false)
179 }
180
181 fn step_mode(&self) -> bool {
182 self.inner.lock().map(|g| g.step_mode).unwrap_or(false)
183 }
184
185 fn next_seq(&self) -> u64 {
186 self.seq.fetch_add(1, Ordering::SeqCst)
187 }
188
189 fn write_message(&self, body: Value) -> io::Result<()> {
190 let s = serde_json::to_string(&body)?;
191 let mut w = self.writer.lock().expect("dap writer");
192 write!(w, "Content-Length: {}\r\n\r\n{}", s.len(), s)?;
193 w.flush()
194 }
195
196 fn emit_response(
197 &self,
198 req_seq: i64,
199 command: &str,
200 success: bool,
201 body: Value,
202 ) -> io::Result<()> {
203 let seq = self.next_seq();
204 let msg = json!({
205 "seq": seq,
206 "type": "response",
207 "request_seq": req_seq,
208 "command": command,
209 "success": success,
210 "body": body,
211 });
212 tracing::trace!(target: "zshrs::dap::send", seq, %command, "response");
213 self.write_message(msg)
214 }
215 pub fn emit_event(&self, event: &str, body: Value) -> io::Result<()> {
217 let seq = self.next_seq();
218 let milestone = matches!(
219 event,
220 "stopped" | "terminated" | "exited" | "initialized" | "process" | "breakpoint"
221 );
222 if milestone {
223 tracing::info!(target: "zshrs::dap::send", seq, %event, "event (milestone)");
224 } else {
225 tracing::trace!(target: "zshrs::dap::send", seq, %event, "event");
226 }
227 let msg = json!({
228 "seq": seq,
229 "type": "event",
230 "event": event,
231 "body": body,
232 });
233 self.write_message(msg)
234 }
235}
236
237static DAP_SHARED: OnceLock<Arc<DapShared>> = OnceLock::new();
243
244static DAP_BREAKPOINTS: OnceLock<Arc<Mutex<BreakpointState>>> = OnceLock::new();
249
250pub fn check_line(line: u32) {
256 let Some(shared) = DAP_SHARED.get() else {
257 return;
258 };
259 if shared.disconnected.load(Ordering::SeqCst) {
260 return;
261 }
262 let program = shared.program.lock().map(|g| g.clone()).unwrap_or_default();
263 let reason = if shared.want_pause() {
264 "pause"
265 } else if shared.step_mode() {
266 "step"
267 } else {
268 let bp_arc = match DAP_BREAKPOINTS.get() {
270 Some(b) => b,
271 None => return,
272 };
273 let bp = match bp_arc.lock() {
274 Ok(g) => g,
275 Err(_) => return,
276 };
277 let hit = bp
278 .line_breakpoints
279 .get(&program)
280 .map(|lines| lines.contains(&line))
281 .unwrap_or(false);
282 if !hit {
283 return;
284 }
285 "breakpoint"
286 };
287 let snap = PauseSnapshot {
288 reason: reason.to_string(),
289 file: program,
290 line,
291 };
292 let action = shared.pause(snap);
293 if matches!(action, DebugAction::Quit) {
294 crate::ported::utils::errflag
298 .fetch_or(crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::Relaxed);
299 }
300}
301
302pub fn run_dap(addr: Option<&str>) -> i32 {
326 tracing::info!(
327 target: "zshrs::dap",
328 pid = std::process::id(),
329 mode = if addr.is_some() { "tcp" } else { "stdio" },
330 "starting --dap",
331 );
332
333 let (reader, writer): (Box<dyn Read + Send>, Box<dyn Write + Send>) = match addr {
336 Some(addr) => {
340 let stream = match TcpStream::connect(addr) {
341 Ok(s) => s,
342 Err(e) => {
343 tracing::error!(target: "zshrs::dap", %addr, %e, "tcp connect failed");
344 eprintln!("zshrs: --dap: connect {} failed: {}", addr, e);
345 return 1;
346 }
347 };
348 if let Err(e) = stream.set_nodelay(true) {
349 tracing::warn!(target: "zshrs::dap", %e, "TCP_NODELAY failed (non-fatal)");
350 }
351 let reader_stream = match stream.try_clone() {
352 Ok(s) => s,
353 Err(e) => {
354 tracing::error!(target: "zshrs::dap", %e, "tcp clone failed");
355 eprintln!("zshrs: --dap: clone socket: {}", e);
356 return 1;
357 }
358 };
359 tracing::info!(target: "zshrs::dap", %addr, "tcp connected");
360 (Box::new(reader_stream), Box::new(stream))
361 }
362 None => {
367 tracing::info!(target: "zshrs::dap", "stdio mode");
368 (Box::new(io::stdin()), Box::new(io::stdout()))
369 }
370 };
371
372 let shared = DapShared::new(writer);
373 let bp_state = Arc::new(Mutex::new(BreakpointState::default()));
374
375 let (launch_tx, launch_rx) = std::sync::mpsc::channel::<LaunchParams>();
378 let shared_reader = shared.clone();
379 let bp_reader = bp_state.clone();
380 let _reader = thread::spawn(move || {
381 let mut br = BufReader::new(reader);
382 loop {
383 let msg = match read_message(&mut br) {
384 Ok(Some(m)) => m,
385 Ok(None) => {
386 tracing::info!(target: "zshrs::dap", "client disconnected (EOF)");
387 break;
388 }
389 Err(e) => {
390 tracing::error!(target: "zshrs::dap", %e, "read error");
391 break;
392 }
393 };
394 handle_request(&shared_reader, &bp_reader, &launch_tx, msg);
395 if shared_reader.disconnected.load(Ordering::SeqCst) {
396 break;
397 }
398 }
399 shared_reader.disconnected.store(true, Ordering::SeqCst);
402 shared_reader.resume_with(DebugAction::Quit);
403 });
404
405 let lp = match launch_rx.recv() {
407 Ok(p) => p,
408 Err(_) => {
409 tracing::warn!(target: "zshrs::dap", "no launch received before disconnect");
410 return 1;
411 }
412 };
413 tracing::info!(
414 target: "zshrs::dap",
415 program = %lp.program,
416 cwd = ?lp.cwd,
417 args = ?lp.args,
418 stop_on_entry = lp.stop_on_entry,
419 "launch received",
420 );
421
422 *shared.program.lock().expect("program lock") = lp.program.clone();
424 let _ = DAP_SHARED.set(shared.clone());
425 let _ = DAP_BREAKPOINTS.set(bp_state.clone());
426 if lp.stop_on_entry {
427 shared.inner.lock().expect("dap lock").step_mode = true;
429 }
430
431 let _ = shared.emit_event(
433 "process",
434 json!({
435 "name": lp.program,
436 "systemProcessId": std::process::id(),
437 "isLocalProcess": true,
438 "startMethod": "launch",
439 }),
440 );
441 let _ = shared.emit_event("thread", json!({ "reason": "started", "threadId": 1 }));
442
443 if let Some(cwd) = &lp.cwd {
444 let _ = std::env::set_current_dir(cwd);
445 }
446
447 tracing::info!(target: "zshrs::dap", "entering executor (in-process)");
450 let mut exec = crate::vm_helper::ShellExecutor::new();
451 let exit_code = match exec.execute_script_file(&lp.program) {
452 Ok(status) => status,
453 Err(e) => {
454 tracing::error!(target: "zshrs::dap", %e, "executor returned error");
455 let _ = shared.emit_event(
456 "output",
457 json!({
458 "category": "stderr",
459 "output": format!("zshrs --dap: {}\n", e),
460 }),
461 );
462 1
463 }
464 };
465 tracing::info!(target: "zshrs::dap", exit_code, "executor exited");
466
467 let _ = shared.emit_event("exited", json!({ "exitCode": exit_code }));
468 let _ = shared.emit_event("terminated", json!({}));
469 let _ = shared.emit_event("thread", json!({ "reason": "exited", "threadId": 1 }));
470 thread::sleep(Duration::from_millis(50));
472 exit_code
473}
474
475#[derive(Debug, Clone)]
476struct LaunchParams {
477 program: String,
478 cwd: Option<String>,
479 args: Vec<String>,
480 stop_on_entry: bool,
481}
482
483fn handle_request(
487 shared: &Arc<DapShared>,
488 bp_state: &Arc<Mutex<BreakpointState>>,
489 launch_tx: &std::sync::mpsc::Sender<LaunchParams>,
490 msg: Value,
491) {
492 let cmd = msg
493 .get("command")
494 .and_then(|v| v.as_str())
495 .unwrap_or("")
496 .to_string();
497 let req_seq = msg.get("seq").and_then(|v| v.as_i64()).unwrap_or(0);
498 let args = msg.get("arguments").cloned().unwrap_or(Value::Null);
499 tracing::trace!(
500 target: "zshrs::dap::recv",
501 seq = req_seq,
502 %cmd,
503 "request",
504 );
505
506 match cmd.as_str() {
507 "initialize" => {
508 let _ = shared.emit_response(
509 req_seq,
510 &cmd,
511 true,
512 json!({
513 "supportsConfigurationDoneRequest": true,
514 "supportsEvaluateForHovers": true,
515 "supportsTerminateRequest": true,
516 "supportsStepBack": false,
517 "supportsSetVariable": false,
518 "supportsConditionalBreakpoints": false,
519 "supportsHitConditionalBreakpoints": false,
520 "supportsFunctionBreakpoints": false,
521 "supportsRestartFrame": false,
522 "supportsGotoTargetsRequest": false,
523 "supportsStepInTargetsRequest": false,
524 "supportsCompletionsRequest": false,
525 "supportsModulesRequest": false,
526 "supportsExceptionInfoRequest": false,
527 }),
528 );
529 let _ = shared.emit_event("initialized", json!({}));
530 }
531 "setBreakpoints" => {
532 let path = args["source"]["path"].as_str().unwrap_or("").to_string();
533 let canon_path = std::fs::canonicalize(&path)
537 .map(|p| p.to_string_lossy().into_owned())
538 .unwrap_or_else(|_| path.clone());
539 let mut lines: Vec<u32> = Vec::new();
540 let mut verified = Vec::new();
541 if let Some(arr) = args["breakpoints"].as_array() {
542 for b in arr {
543 if let Some(l) = b["line"].as_u64() {
544 lines.push(l as u32);
545 verified.push(json!({ "verified": true, "line": l }));
546 }
547 }
548 }
549 tracing::info!(
550 target: "zshrs::dap::breakpoints",
551 path = %canon_path,
552 count = lines.len(),
553 lines = ?lines,
554 "registered",
555 );
556 if let Ok(mut bp) = bp_state.lock() {
557 if !canon_path.is_empty() {
558 bp.line_breakpoints.insert(canon_path, lines);
559 }
560 }
561 let _ = shared.emit_response(req_seq, &cmd, true, json!({ "breakpoints": verified }));
562 }
563 "setExceptionBreakpoints" => {
564 let _ = shared.emit_response(req_seq, &cmd, true, json!({}));
565 }
566 "configurationDone" => {
567 shared.configuration_done.store(true, Ordering::SeqCst);
568 let _ = shared.emit_response(req_seq, &cmd, true, json!({}));
569 }
570 "launch" => {
571 let program_raw = args["program"].as_str().unwrap_or("").to_string();
572 let program = std::fs::canonicalize(&program_raw)
575 .map(|p| p.to_string_lossy().into_owned())
576 .unwrap_or(program_raw);
577 let cwd = args["cwd"].as_str().map(|s| s.to_string());
578 let lp_args: Vec<String> = args["args"]
579 .as_array()
580 .map(|a| {
581 a.iter()
582 .filter_map(|v| v.as_str().map(String::from))
583 .collect()
584 })
585 .unwrap_or_default();
586 let stop_on_entry = args["stopOnEntry"].as_bool().unwrap_or(false);
587 let _ = shared.emit_response(req_seq, &cmd, true, json!({}));
588 let _ = launch_tx.send(LaunchParams {
589 program,
590 cwd,
591 args: lp_args,
592 stop_on_entry,
593 });
594 }
595 "threads" => {
596 let _ = shared.emit_response(
597 req_seq,
598 &cmd,
599 true,
600 json!({ "threads": [{ "id": 1, "name": "main" }] }),
601 );
602 }
603 "stackTrace" => {
604 let program = shared.program.lock().map(|g| g.clone()).unwrap_or_default();
608 let line = current_lineno();
609 let frames = vec![json!({
610 "id": 1,
611 "name": "main",
612 "source": { "path": program, "name": file_name(&program) },
613 "line": line,
614 "column": 1,
615 })];
616 let _ = shared.emit_response(
617 req_seq,
618 &cmd,
619 true,
620 json!({ "stackFrames": frames, "totalFrames": 1 }),
621 );
622 }
623 "scopes" => {
624 let _ = shared.emit_response(
632 req_seq,
633 &cmd,
634 true,
635 json!({
636 "scopes": [
637 { "name": "Locals", "variablesReference": 1, "expensive": false, "presentationHint": "locals" },
638 { "name": "Specials", "variablesReference": 2, "expensive": false },
639 { "name": "Environment", "variablesReference": 3, "expensive": false },
640 ],
641 }),
642 );
643 }
644 "variables" => {
645 let r = args["variablesReference"].as_u64().unwrap_or(1);
646 let vars = match r {
647 1 => snapshot_user_vars(),
648 2 => snapshot_special_vars(),
649 3 => snapshot_env_vars(),
650 _ => snapshot_user_vars(),
651 };
652 let _ = shared.emit_response(req_seq, &cmd, true, json!({ "variables": vars }));
653 }
654 "evaluate" => {
655 let expr = args["expression"].as_str().unwrap_or("");
656 let (result, ty) = evaluate_expression(expr);
657 let _ = shared.emit_response(
658 req_seq,
659 &cmd,
660 true,
661 json!({
662 "result": result,
663 "type": ty,
664 "variablesReference": 0,
665 }),
666 );
667 }
668 "continue" => {
669 let _ =
670 shared.emit_response(req_seq, &cmd, true, json!({ "allThreadsContinued": true }));
671 shared.resume_with(DebugAction::Continue);
672 }
673 "next" => {
674 let _ =
675 shared.emit_response(req_seq, &cmd, true, json!({ "allThreadsContinued": true }));
676 shared.resume_with(DebugAction::StepOver);
677 }
678 "stepIn" => {
679 let _ =
680 shared.emit_response(req_seq, &cmd, true, json!({ "allThreadsContinued": true }));
681 shared.resume_with(DebugAction::StepIn);
682 }
683 "stepOut" => {
684 let _ =
685 shared.emit_response(req_seq, &cmd, true, json!({ "allThreadsContinued": true }));
686 shared.resume_with(DebugAction::StepOut);
687 }
688 "pause" => {
689 let _ = shared.emit_response(req_seq, &cmd, true, json!({}));
690 shared.request_pause();
691 }
692 "disconnect" | "terminate" => {
693 let _ = shared.emit_response(req_seq, &cmd, true, json!({}));
694 shared.disconnected.store(true, Ordering::SeqCst);
695 shared.resume_with(DebugAction::Quit);
696 }
697 "source" => {
698 let _ = shared.emit_response(req_seq, &cmd, true, json!({}));
699 }
700 _ => {
701 tracing::debug!(target: "zshrs::dap", %cmd, "unsupported request");
702 let _ = shared.emit_response(req_seq, &cmd, false, json!({ "error": "unsupported" }));
703 }
704 }
705}
706
707fn current_lineno() -> u32 {
710 crate::ported::params::paramtab()
711 .read()
712 .ok()
713 .and_then(|t| t.get("LINENO").map(|pm| pm.u_val as u32))
714 .unwrap_or(1)
715}
716
717fn snapshot_bucketed() -> (
727 Vec<(String, String)>,
728 Vec<(String, String)>,
729 Vec<(String, String)>,
730) {
731 let mut user: Vec<(String, String)> = Vec::new();
732 let mut specials: Vec<(String, String)> = Vec::new();
733 let mut env: Vec<(String, String)> = Vec::new();
734
735 if let Ok(tab) = crate::ported::params::paramtab().read() {
736 for (name, pm) in tab.iter() {
737 if matches!(
738 name.as_str(),
739 "_" | "PIPESTATUS" | "pipestatus" | "ZSH_ARGZERO"
740 ) {
741 continue;
742 }
743 let value = if pm.u_val != 0
744 && (pm.node.flags & crate::ported::zsh_h::PM_INTEGER as i32) != 0
745 {
746 pm.u_val.to_string()
747 } else if let Some(s) = pm.u_str.as_ref() {
748 s.clone()
749 } else {
750 String::new()
751 };
752 let is_special = (pm.node.flags & crate::ported::zsh_h::PM_SPECIAL as i32) != 0;
753 let in_process_env = std::env::var(name).is_ok();
758 let is_caps_only = !name.is_empty()
761 && name
762 .chars()
763 .all(|c| c.is_ascii_uppercase() || c == '_' || c.is_ascii_digit())
764 && name
765 .chars()
766 .next()
767 .map(|c| c.is_ascii_uppercase())
768 .unwrap_or(false);
769 if in_process_env {
770 env.push((name.clone(), value));
771 } else if is_special || is_caps_only {
772 specials.push((name.clone(), value));
773 } else {
774 user.push((name.clone(), value));
775 }
776 }
777 }
778 if user.is_empty() && specials.is_empty() && env.is_empty() {
779 for (k, v) in std::env::vars() {
780 env.push((k, v));
781 }
782 }
783 user.sort_by(|a, b| a.0.cmp(&b.0));
784 specials.sort_by(|a, b| a.0.cmp(&b.0));
785 env.sort_by(|a, b| a.0.cmp(&b.0));
786 (user, specials, env)
787}
788
789fn vars_to_json(vars: &[(String, String)]) -> Vec<Value> {
790 vars.iter()
791 .take(500)
792 .map(|(n, v)| {
793 json!({
794 "name": n,
795 "value": v,
796 "type": "scalar",
797 "variablesReference": 0,
798 })
799 })
800 .collect()
801}
802
803fn snapshot_user_vars() -> Vec<Value> {
804 let (user, _, _) = snapshot_bucketed();
805 vars_to_json(&user)
806}
807
808fn snapshot_special_vars() -> Vec<Value> {
809 let (_, specials, _) = snapshot_bucketed();
810 vars_to_json(&specials)
811}
812
813fn snapshot_env_vars() -> Vec<Value> {
814 let (_, _, env) = snapshot_bucketed();
815 vars_to_json(&env)
816}
817
818fn evaluate_expression(expr: &str) -> (String, String) {
819 let exe = std::env::current_exe().unwrap_or_else(|_| "zshrs".into());
822 let mut cmd = Command::new(exe);
823 cmd.arg("-c").arg(expr);
824 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
825 match cmd.output() {
826 Ok(o) => {
827 if o.status.success() {
828 let s = String::from_utf8_lossy(&o.stdout).trim_end().to_string();
829 (s, "scalar".into())
830 } else {
831 let s = String::from_utf8_lossy(&o.stderr).trim_end().to_string();
832 (s, "error".into())
833 }
834 }
835 Err(e) => (format!("evaluate: {}", e), "error".into()),
836 }
837}
838fn read_message<R: BufRead>(reader: &mut R) -> io::Result<Option<Value>> {
841 let mut content_length: Option<usize> = None;
842 loop {
843 let mut line = String::new();
844 let n = reader.read_line(&mut line)?;
845 if n == 0 {
846 return Ok(None);
847 }
848 if line == "\r\n" || line == "\n" {
849 break;
850 }
851 if let Some(rest) = line.strip_prefix("Content-Length:") {
852 content_length =
853 Some(rest.trim().parse().map_err(|_| {
854 io::Error::new(io::ErrorKind::InvalidData, "bad Content-Length")
855 })?);
856 }
857 }
858 let len = content_length
859 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing Content-Length"))?;
860 let mut buf = vec![0u8; len];
861 reader.read_exact(&mut buf)?;
862 let v: Value =
863 serde_json::from_slice(&buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
864 Ok(Some(v))
865}
866
867fn write_message<W: Write>(mut writer: W, msg: &Value) -> io::Result<()> {
868 let body = serde_json::to_vec(msg)?;
869 write!(writer, "Content-Length: {}\r\n\r\n", body.len())?;
870 writer.write_all(&body)?;
871 writer.flush()
872}
873
874fn file_name(path: &str) -> String {
875 path.rsplit_once('/')
876 .map(|x| x.1)
877 .unwrap_or(path)
878 .to_string()
879}