use std::collections::HashMap;
use std::io::Write;
use std::path::Path;
use std::process::Stdio;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{Receiver as StdReceiver, SyncSender as StdSyncSender};
use std::sync::{Arc, Mutex, MutexGuard};
use serde_json::Value;
use crate::error::{Error, Result};
use crate::lsp::jsonrpc::{await_completion, encode_frame, read_frame_with_scratch};
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn tool_err(code: &str, message: impl Into<String>) -> Error {
Error::tool("debug", format!("[{code}] {}", message.into()))
}
#[derive(Debug, Clone)]
pub struct DapEvent {
pub event: String,
pub body: Value,
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DapError {
Adapter { command: String, message: String },
Transport(String),
Timeout { timeout_ms: u64 },
Cancelled,
}
impl DapError {
#[must_use]
pub const fn code(&self) -> &'static str {
match self {
Self::Adapter { .. } => "DAP_ADAPTER_ERROR",
Self::Transport(_) => "DAP_TRANSPORT",
Self::Timeout { .. } => "DAP_TIMEOUT",
Self::Cancelled => "DAP_CANCELLED",
}
}
#[must_use]
pub fn message(&self) -> String {
match self {
Self::Adapter { command, message } => format!("{command} failed: {message}"),
Self::Transport(reason) => format!("transport: {reason}"),
Self::Timeout { timeout_ms } => format!("timed out after {timeout_ms} ms"),
Self::Cancelled => "cancelled by ambient context".to_string(),
}
}
}
impl From<DapError> for Error {
fn from(err: DapError) -> Self {
Self::tool("debug", format!("[{}] {}", err.code(), err.message()))
}
}
impl std::fmt::Display for DapError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message())
}
}
type PendingMap = Mutex<HashMap<u64, StdSyncSender<std::result::Result<Value, DapError>>>>;
pub struct DapTransport {
child: Mutex<crate::tools::ProcessGuard>,
writer: Arc<Mutex<std::process::ChildStdin>>,
pending: Arc<PendingMap>,
next_seq: Arc<AtomicU64>,
alive: Arc<std::sync::atomic::AtomicBool>,
stderr_tail: Arc<Mutex<crate::lsp::jsonrpc::PublicTailBuffer>>,
event_rx: Mutex<StdReceiver<DapEvent>>,
frames_read: Arc<AtomicU64>,
}
impl DapTransport {
pub fn spawn(
command: &str,
args: &[String],
env: &[(String, String)],
cwd: &Path,
) -> Result<Self> {
Self::spawn_inner(command, args, env, cwd, false)
}
pub fn spawn_with_env(
command: &str,
args: &[String],
env: &[(String, String)],
cwd: &Path,
) -> Result<Self> {
Self::spawn_inner(command, args, env, cwd, true)
}
fn spawn_inner(
command: &str,
args: &[String],
env: &[(String, String)],
cwd: &Path,
explicit_env: bool,
) -> Result<Self> {
let mut cmd = std::process::Command::new(command); cmd.args(args)
.current_dir(cwd)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env_remove("CARGO_TARGET_DIR");
if explicit_env {
cmd.env_clear();
}
cmd.envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str())));
let mut child = cmd.spawn().map_err(|err| {
tool_err(
"DAP_ADAPTER_MISSING",
format!("failed to spawn debug adapter {command:?}: {err}"),
)
})?;
let stdin = child
.stdin
.take()
.ok_or_else(|| tool_err("DAP_TRANSPORT", "missing adapter stdin"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| tool_err("DAP_TRANSPORT", "missing adapter stdout"))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| tool_err("DAP_TRANSPORT", "missing adapter stderr"))?;
let writer = Arc::new(Mutex::new(stdin));
let next_seq = Arc::new(AtomicU64::new(1));
let pending: Arc<PendingMap> = Arc::new(Mutex::new(HashMap::new()));
let alive = Arc::new(std::sync::atomic::AtomicBool::new(true));
let stderr_tail = Arc::new(Mutex::new(crate::lsp::jsonrpc::PublicTailBuffer::new()));
let (event_tx, event_rx) = std::sync::mpsc::sync_channel::<DapEvent>(512);
let frames_read = Arc::new(AtomicU64::new(0));
spawn_reader(
stdout,
Arc::clone(&pending),
Arc::clone(&alive),
Arc::clone(&stderr_tail),
Arc::clone(&writer),
Arc::clone(&next_seq),
Arc::clone(&frames_read),
event_tx,
);
{
let stderr_tail = Arc::clone(&stderr_tail);
let pump_body = move || {
use std::io::Read as _;
let mut reader = std::io::BufReader::new(stderr);
let mut buf = [0u8; 4096];
loop {
match reader.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
let chunk = String::from_utf8_lossy(&buf[..n]);
lock(&stderr_tail).push(&chunk);
}
}
}
};
std::thread::spawn(pump_body); }
Ok(Self {
child: Mutex::new(crate::tools::ProcessGuard::new(
child,
crate::tools::ProcessCleanupMode::ProcessGroupTree,
)),
writer,
pending,
next_seq,
alive,
stderr_tail,
event_rx: Mutex::new(event_rx),
frames_read,
})
}
#[must_use]
pub fn frames_read(&self) -> u64 {
self.frames_read.load(Ordering::SeqCst)
}
#[must_use]
pub fn is_alive(&self) -> bool {
if !self.alive.load(Ordering::SeqCst) {
return false;
}
let status = lock(&self.child).try_wait_child();
!matches!(status, Ok(Some(_)))
}
pub async fn request(
&self,
command: &str,
arguments: Value,
timeout: std::time::Duration,
) -> std::result::Result<Value, DapError> {
if !self.is_alive() {
return Err(DapError::Transport("adapter is not running".to_string()));
}
let seq = self.next_seq.fetch_add(1, Ordering::SeqCst);
let (tx, rx) = std::sync::mpsc::sync_channel(1);
lock(&self.pending).insert(seq, tx);
let frame = encode_frame(&serde_json::json!({
"seq": seq,
"type": "request",
"command": command,
"arguments": arguments,
}));
let write_result = {
let mut guard = lock(&self.writer);
guard.write_all(&frame).and_then(|()| guard.flush())
};
if let Err(err) = write_result {
lock(&self.pending).remove(&seq);
return Err(DapError::Transport(format!("write failed: {err}")));
}
if std::env::var_os("PI_DAP_TRACE").is_some() {
eprintln!(
"[dap-request] wrote seq={seq} cmd={command} ({} bytes)",
frame.len()
);
}
let outcome = await_completion(rx, timeout, || {
lock(&self.pending).remove(&seq);
let cancel = encode_frame(&serde_json::json!({
"seq": self.next_seq.fetch_add(1, Ordering::SeqCst),
"type": "request",
"command": "cancel",
"arguments": { "requestId": seq },
}));
let mut guard = lock(&self.writer);
let _ = guard.write_all(&cancel).and_then(|()| guard.flush());
})
.await;
match outcome {
Ok(Ok(body)) => Ok(body),
Ok(Err(err)) => Err(err),
Err(crate::lsp::jsonrpc::CompletionWaitError::Timeout) => Err(DapError::Timeout {
timeout_ms: u64::try_from(timeout.as_millis()).unwrap_or(u64::MAX),
}),
Err(crate::lsp::jsonrpc::CompletionWaitError::Cancelled) => Err(DapError::Cancelled),
Err(crate::lsp::jsonrpc::CompletionWaitError::Closed) => Err(DapError::Transport(
"completion channel dropped".to_string(),
)),
}
}
pub fn drain_events(&self) -> Vec<DapEvent> {
let mut out = Vec::new();
let rx = lock(&self.event_rx);
while let Ok(event) = rx.try_recv() {
out.push(event);
}
out
}
#[must_use]
pub fn stderr_tail(&self) -> String {
lock(&self.stderr_tail).tail()
}
pub fn kill(&self) {
let _ = lock(&self.child).kill();
self.alive.store(false, Ordering::SeqCst);
}
}
impl Drop for DapTransport {
fn drop(&mut self) {
self.kill();
}
}
#[allow(clippy::too_many_arguments)]
fn spawn_reader(
stdout: std::process::ChildStdout,
pending: Arc<PendingMap>,
alive: Arc<std::sync::atomic::AtomicBool>,
stderr_tail: Arc<Mutex<crate::lsp::jsonrpc::PublicTailBuffer>>,
writer: Arc<Mutex<std::process::ChildStdin>>,
next_seq: Arc<AtomicU64>,
frames_read: Arc<AtomicU64>,
event_tx: StdSyncSender<DapEvent>,
) {
let reader_body = move || {
let mut reader = std::io::BufReader::new(stdout);
let mut scratch = Vec::new();
loop {
match read_frame_with_scratch(&mut reader, &mut scratch) {
Ok(Some(message)) => {
frames_read.fetch_add(1, Ordering::SeqCst);
if std::env::var_os("PI_DAP_TRACE").is_some() {
eprintln!(
"[dap-reader] frame: type={:?} request_seq={:?} command={:?} event={:?}",
message.get("type"),
message.get("request_seq"),
message.get("command"),
message.get("event"),
);
}
dispatch(
&message,
&pending,
&event_tx,
&stderr_tail,
&writer,
&next_seq,
);
}
Ok(None) => break,
Err(err) => {
if std::env::var_os("PI_DAP_TRACE").is_some() {
eprintln!("[dap-reader] read error: {err}");
}
break;
}
}
}
alive.store(false, Ordering::SeqCst);
let mut pending = lock(&pending);
for (_, sender) in pending.drain() {
let _ = sender.send(Err(DapError::Transport(
"adapter closed stdout (EOF)".to_string(),
)));
}
};
std::thread::spawn(reader_body); }
#[allow(clippy::too_many_arguments)]
fn dispatch<W: Write>(
message: &Value,
pending: &PendingMap,
event_tx: &StdSyncSender<DapEvent>,
stderr_tail: &Mutex<crate::lsp::jsonrpc::PublicTailBuffer>,
writer: &Mutex<W>,
next_seq: &AtomicU64,
) {
match message.get("type").and_then(Value::as_str) {
Some("response") => {
let trace = std::env::var_os("PI_DAP_TRACE").is_some();
let request_seq = message
.get("request_seq")
.and_then(Value::as_u64)
.unwrap_or(0);
let command = message
.get("command")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let success = message
.get("success")
.and_then(Value::as_bool)
.unwrap_or(false);
if trace {
eprintln!(
"[dap-dispatch] response seq={request_seq} cmd={command} — locking pending"
);
}
let sender = lock(pending).remove(&request_seq);
if trace {
eprintln!(
"[dap-dispatch] seq={request_seq} sender present={}",
sender.is_some()
);
}
if let Some(sender) = sender {
let outcome = if success {
Ok(message.get("body").cloned().unwrap_or(Value::Null))
} else {
Err(DapError::Adapter {
message: message
.get("message")
.and_then(Value::as_str)
.unwrap_or("adapter reported failure")
.to_string(),
command,
})
};
if trace {
eprintln!("[dap-dispatch] seq={request_seq} sending…");
}
let sent = sender.send(outcome);
if trace {
eprintln!("[dap-dispatch] seq={request_seq} sent={}", sent.is_ok());
}
}
}
Some("event") => {
let event = message
.get("event")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let body = message.get("body").cloned().unwrap_or(Value::Null);
if event == "output" {
let text = body.get("output").and_then(Value::as_str).unwrap_or("");
lock(stderr_tail).push(text);
}
let _ = event_tx.try_send(DapEvent { event, body });
}
Some("request") => {
let seq = message.get("seq").and_then(Value::as_u64).unwrap_or(0);
let command = message
.get("command")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let reply = encode_frame(&serde_json::json!({
"seq": next_seq.fetch_add(1, Ordering::SeqCst),
"type": "response",
"request_seq": seq,
"success": false,
"command": command,
"message": "pi_agent_rust declines reverse requests (no terminal host)"
}));
let mut guard = lock(writer);
let _ = guard.write_all(&reply).and_then(|()| guard.flush());
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_writer() -> (
Mutex<std::process::ChildStdin>,
AtomicU64,
std::process::Child,
) {
let mut child = std::process::Command::new("cat")
.stdin(std::process::Stdio::piped())
.spawn()
.expect("cat");
let stdin = child.stdin.take().expect("stdin");
(Mutex::new(stdin), AtomicU64::new(100), child)
}
#[test]
fn dispatch_routes_responses_by_request_seq() {
let pending: PendingMap = Mutex::new(HashMap::new());
let (tx, rx) = std::sync::mpsc::sync_channel(1);
lock(&pending).insert(7, tx);
let (event_tx, _rx) = std::sync::mpsc::sync_channel(4);
let tail = Mutex::new(crate::lsp::jsonrpc::PublicTailBuffer::new());
let (writer, next_seq, mut child) = test_writer();
dispatch(
&serde_json::json!({
"seq": 9, "type": "response", "request_seq": 7,
"success": true, "command": "stackTrace",
"body": {"stackFrames": []}
}),
&pending,
&event_tx,
&tail,
&writer,
&next_seq,
);
let got = rx.try_recv().expect("completed").expect("ok");
assert!(got.get("stackFrames").is_some());
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn dispatch_surfaces_adapter_failures() {
let pending: PendingMap = Mutex::new(HashMap::new());
let (tx, rx) = std::sync::mpsc::sync_channel(1);
lock(&pending).insert(3, tx);
let (event_tx, _rx) = std::sync::mpsc::sync_channel(4);
let tail = Mutex::new(crate::lsp::jsonrpc::PublicTailBuffer::new());
let (writer, next_seq, mut child) = test_writer();
dispatch(
&serde_json::json!({
"seq": 4, "type": "response", "request_seq": 3,
"success": false, "command": "evaluate", "message": "cannot evaluate while running"
}),
&pending,
&event_tx,
&tail,
&writer,
&next_seq,
);
let err = rx
.try_recv()
.expect("completed")
.expect_err("adapter error");
assert!(err.message().contains("cannot evaluate while running"));
assert_eq!(err.code(), "DAP_ADAPTER_ERROR");
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn dispatch_queues_events_and_captures_output() {
let pending: PendingMap = Mutex::new(HashMap::new());
let (event_tx, event_rx) = std::sync::mpsc::sync_channel(4);
let tail = Mutex::new(crate::lsp::jsonrpc::PublicTailBuffer::new());
let (writer, next_seq, mut child) = test_writer();
dispatch(
&serde_json::json!({
"seq": 5, "type": "event", "event": "stopped",
"body": {"reason": "breakpoint", "threadId": 42}
}),
&pending,
&event_tx,
&tail,
&writer,
&next_seq,
);
let event = event_rx.try_recv().expect("event queued");
assert_eq!(event.event, "stopped");
assert_eq!(event.body["threadId"], 42);
dispatch(
&serde_json::json!({
"seq": 6, "type": "event", "event": "output",
"body": {"category": "stdout", "output": "hello\n"}
}),
&pending,
&event_tx,
&tail,
&writer,
&next_seq,
);
assert!(lock(&tail).tail().contains("hello"));
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn dispatch_declines_reverse_requests() {
let pending: PendingMap = Mutex::new(HashMap::new());
let (event_tx, _rx) = std::sync::mpsc::sync_channel(4);
let tail = Mutex::new(crate::lsp::jsonrpc::PublicTailBuffer::new());
let (mut pipe_reader, pipe_writer) = std::io::pipe().expect("pipe");
let writer = Mutex::new(pipe_writer);
let next_seq = AtomicU64::new(100);
dispatch(
&serde_json::json!({
"seq": 55, "type": "request", "command": "runInTerminal",
"arguments": {"kind": "integrated", "title": "debuggee", "args": ["/bin/true"]}
}),
&pending,
&event_tx,
&tail,
&writer,
&next_seq,
);
let mut reader = std::io::BufReader::new(&mut pipe_reader);
let frame = crate::lsp::jsonrpc::read_frame(&mut reader)
.expect("read reply")
.expect("a reply frame");
assert_eq!(frame["type"], "response");
assert_eq!(frame["request_seq"], 55);
assert_eq!(frame["success"], false);
assert_eq!(frame["command"], "runInTerminal");
}
#[test]
fn live_lldb_dap_initialize_round_trip() {
let mut candidates = vec![];
if let Some(paths) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&paths) {
let full = dir.join("lldb-dap");
if full.exists() {
candidates.push("lldb-dap".to_string());
}
}
}
for dir in ["/usr/lib", "/usr/local/lib"] {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
if entry.file_name().to_string_lossy().starts_with("llvm") {
let candidate = entry.path().join("bin/lldb-dap");
if candidate.exists() {
candidates.push(candidate.display().to_string());
}
}
}
}
}
for fixed in [
"/usr/bin/lldb-dap",
"/Library/Developer/CommandLineTools/usr/bin/lldb-dap",
"/opt/homebrew/opt/llvm/bin/lldb-dap",
] {
if Path::new(fixed).exists() {
candidates.push(fixed.to_string());
}
}
let Some(command) = candidates.into_iter().next() else {
eprintln!("skip: no lldb-dap on this host");
return;
};
let transport =
DapTransport::spawn(&command, &[], &[], Path::new("/tmp")).expect("spawn lldb-dap");
let runtime = asupersync::runtime::RuntimeBuilder::new()
.enable_parking(false)
.worker_threads(1)
.blocking_threads(1, 8)
.build()
.expect("runtime");
let result = runtime.block_on(transport.request(
"initialize",
serde_json::json!({"clientID": "t", "adapterID": "pi-dap", "pathFormat": "path"}),
std::time::Duration::from_secs(10),
));
let body = result.unwrap_or_else(|err| panic!("initialize failed: {err}")); assert!(
body.get("supportsConfigurationDoneRequest").is_some(),
"capabilities: {body}"
);
let second = runtime.block_on(transport.request(
"configurationDone",
serde_json::json!({}),
std::time::Duration::from_secs(5),
));
let _ = second;
transport.kill();
}
#[test]
#[ignore = "same lldb-dap launch stall as the tool-level lane (adapter drops the request under fast pacing here); the initialize round-trip and the debugpy end-to-end lane stay active"]
fn live_lldb_dap_launch_fixture() {
let temp = tempfile::tempdir().expect("tempdir");
let source = temp.path().join("fx.c");
std::fs::write(
&source,
"#include <unistd.h>\nint main(void) { usleep(400000); return 0; }\n",
)
.expect("write");
let binary = temp.path().join("fx");
let source_str = source.to_string_lossy().into_owned();
let binary_str = binary.to_string_lossy().into_owned();
let status = std::process::Command::new("cc")
.args(["-g", "-O0", &source_str, "-o", &binary_str])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.expect("cc runs");
if !status.success() {
eprintln!("skip: cc failed");
return;
}
let mut candidates: Vec<String> = Vec::new();
for dir in ["/usr/lib", "/usr/local/lib"] {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
if entry.file_name().to_string_lossy().starts_with("llvm") {
let candidate = entry.path().join("bin/lldb-dap");
if candidate.exists() {
candidates.push(candidate.display().to_string());
}
}
}
}
}
for fixed in [
"/usr/bin/lldb-dap",
"/Library/Developer/CommandLineTools/usr/bin/lldb-dap",
"/opt/homebrew/opt/llvm/bin/lldb-dap",
] {
if Path::new(fixed).exists() {
candidates.push(fixed.to_string());
}
}
let Some(command) = candidates.into_iter().next() else {
eprintln!("skip: no lldb-dap");
return;
};
let mut clean_env: Vec<(String, String)> = Vec::new();
for (k, v) in std::env::vars() {
if k != "LD_LIBRARY_PATH"
&& k != "DYLD_LIBRARY_PATH"
&& k != "DYLD_FALLBACK_LIBRARY_PATH"
{
clean_env.push((k, v));
}
}
let transport =
DapTransport::spawn_with_env(&command, &[], &clean_env, temp.path()).expect("spawn");
let runtime = asupersync::runtime::RuntimeBuilder::new()
.enable_parking(false)
.worker_threads(1)
.blocking_threads(1, 8)
.build()
.expect("runtime");
let launch_result = (|| {
runtime.block_on(transport.request(
"initialize",
serde_json::json!({"clientID": "t", "adapterID": "pi-dap", "pathFormat": "path"}),
std::time::Duration::from_secs(10),
))?;
std::thread::sleep(std::time::Duration::from_millis(400));
runtime.block_on(transport.request(
"launch",
serde_json::json!({
"program": binary.display().to_string(),
"args": [],
"cwd": temp.path().display().to_string(),
"console": "internalConsole",
"stopOnEntry": false,
}),
std::time::Duration::from_secs(10),
))
})();
let stderr = transport.stderr_tail();
let events = transport.drain_events();
let alive = transport.is_alive();
let frames = transport.frames_read();
transport.kill();
let message = launch_result
.as_ref()
.err()
.map(|err| {
format!(
"launch sequence failed: {err}; adapter stderr: {stderr}; alive: {alive}; frames_read: {frames}; events: {events:?}"
)
})
.unwrap_or_default();
launch_result.unwrap_or_else(|_| panic!("{message}")); }
const FAKE_ADAPTER_PY: &str = r#"
import json, sys
def read_frame():
headers = {}
while True:
line = sys.stdin.buffer.readline()
if not line:
return None
line = line.strip()
if not line:
break
k, v = line.split(b":", 1)
headers[k.strip().lower()] = v.strip()
n = int(headers[b"content-length"])
return json.loads(sys.stdin.buffer.read(n))
def send(msg):
body = json.dumps(msg).encode()
sys.stdout.buffer.write(b"Content-Length: %d\r\n\r\n" % len(body) + body)
sys.stdout.buffer.flush()
while True:
frame = read_frame()
if frame is None:
break
if frame.get("type") != "request":
continue
cmd = frame.get("command")
seq = frame.get("seq")
sys.stderr.write("fake-dap: got %s seq=%s\n" % (cmd, seq))
sys.stderr.flush()
if cmd == "initialize":
send({"seq": 1, "type": "response", "request_seq": seq, "command": "initialize", "success": True, "body": {"supportsConfigurationDoneRequest": True}})
elif cmd == "launch":
send({"seq": 2, "type": "response", "request_seq": seq, "command": "launch", "success": True})
send({"seq": 3, "type": "event", "event": "process", "body": {"name": "fx", "isLocalProcess": True, "startMethod": "launch", "systemProcessId": 4242}})
send({"seq": 4, "type": "event", "event": "initialized", "body": {}})
else:
send({"seq": 6, "type": "response", "request_seq": seq, "command": cmd, "success": True, "body": {}})
"#;
#[test]
fn fake_adapter_full_handshake() {
let python_ok = std::process::Command::new("python3")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success());
if !python_ok {
eprintln!("skip: no python3");
return;
}
let temp = tempfile::tempdir().expect("tempdir");
let script = temp.path().join("fake_dap.py");
std::fs::write(&script, FAKE_ADAPTER_PY).expect("write fake adapter");
let transport = DapTransport::spawn(
"python3",
&[script.to_string_lossy().into_owned()],
&[],
temp.path(),
)
.expect("spawn fake adapter");
let runtime = asupersync::runtime::RuntimeBuilder::new()
.enable_parking(false)
.worker_threads(1)
.blocking_threads(1, 8)
.build()
.expect("runtime");
let result = (|| {
runtime.block_on(transport.request(
"initialize",
serde_json::json!({"clientID": "t", "adapterID": "pi-dap"}),
std::time::Duration::from_secs(5),
))?;
runtime.block_on(transport.request(
"launch",
serde_json::json!({"program": "/tmp/fx"}),
std::time::Duration::from_secs(5),
))
})();
let events: Vec<String> = transport
.drain_events()
.iter()
.map(|e| e.event.clone())
.collect();
let stderr = transport.stderr_tail();
transport.kill();
let body = result
.unwrap_or_else(|err| panic!("fake-adapter launch failed: {err}; stderr: {stderr}")); let _ = body;
assert!(
events.contains(&"initialized".to_string()),
"events: {events:?}"
);
}
#[test]
fn wire_bytes_capture() {
let temp = tempfile::tempdir().expect("tempdir");
let capture = temp.path().join("captured.bin");
let args = vec!["-c".to_string(), format!("cat > {}", capture.display())];
let transport = match DapTransport::spawn("sh", &args, &[], temp.path()) {
Ok(transport) => transport,
Err(err) => panic!("spawn sham: {err}"), };
let runtime = asupersync::runtime::RuntimeBuilder::new()
.enable_parking(false)
.worker_threads(1)
.blocking_threads(1, 8)
.build()
.expect("runtime");
let one = runtime.block_on(transport.request(
"initialize",
serde_json::json!({"clientID": "t", "adapterID": "pi-dap", "pathFormat": "path"}),
std::time::Duration::from_millis(300),
));
let two = runtime.block_on(transport.request(
"launch",
serde_json::json!({"program": "/tmp/fx", "args": [], "cwd": "/tmp", "console": "internalConsole", "stopOnEntry": false}),
std::time::Duration::from_millis(300),
));
assert!(one.is_err() && two.is_err(), "sham never answers");
transport.kill();
std::thread::sleep(std::time::Duration::from_millis(200));
let bytes = std::fs::read(&capture).expect("read capture");
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("Content-Length:"), "{text}");
assert!(text.contains("\"command\":\"initialize\""), "{text}");
assert!(text.contains("\"command\":\"launch\""), "{text}");
eprintln!("CAPTURED: {text:?}");
}
}