feat: add configurable TCP listener to debug adapter
Support --listen and RUNA_DAP_LISTEN for loopback TCP sessions while preserving stdio by default. Test framed exchanges, sequential connections and recovery from invalid framing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
513415f21c
commit
21fe58ba09
|
|
@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
### Added
|
||||
|
||||
- Add `runa-dap --listen` / `RUNA_DAP_LISTEN` for loopback TCP DAP sessions,
|
||||
retaining stdio as the default transport.
|
||||
- Add `mise run install-extension` to build the debug adapter, package the VS Code
|
||||
extension, and install it locally using `code` (or `VSCODE_CLI`).
|
||||
- Add a standalone Rust `runa-dap` stdio debug adapter for VS Code actor debugging.
|
||||
|
|
|
|||
|
|
@ -300,6 +300,9 @@ Set breakpoints there and use **Debug Actor** to debug that actor with VS Code's
|
|||
Run and Debug controls. Build the standalone Rust DAP server with
|
||||
`cargo build -p runa-dap` and set `runa.debugAdapterPath` to its absolute path,
|
||||
or install it on PATH with `cargo install --path crates/runa-dap`.
|
||||
For TCP DAP clients, use `runa-dap --listen 127.0.0.1:4711 --connect 127.0.0.1:3737`
|
||||
(or set `RUNA_DAP_LISTEN`). The DAP listener is loopback-only; omitting the
|
||||
listen option keeps stdio transport.
|
||||
**Reload Actor Source**
|
||||
clears the override, **Go to Behavior** opens the shared source, and **Update Behavior**
|
||||
asks before copying the temporary text into shared working source without committing.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::io::{self, BufReader};
|
||||
use std::net::SocketAddr;
|
||||
use std::io::{self, BufReader, Read, Write};
|
||||
use std::net::{Shutdown, SocketAddr, TcpListener};
|
||||
use std::sync::mpsc::{self, RecvTimeoutError};
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
@ -7,10 +7,13 @@ use clap::Parser;
|
|||
use runa_dap::{Adapter, WebSocketConnection, framing};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Stdio Debug Adapter Protocol server for one Runa actor")]
|
||||
#[command(about = "Stdio or loopback TCP Debug Adapter Protocol server for Runa actors")]
|
||||
struct Args {
|
||||
#[arg(long, env = "RUNA_CONNECT", default_value = "127.0.0.1:3737")]
|
||||
connect: SocketAddr,
|
||||
/// Listen for TCP DAP sessions instead of using stdin/stdout.
|
||||
#[arg(long, env = "RUNA_DAP_LISTEN")]
|
||||
listen: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
|
|
@ -21,9 +24,39 @@ fn main() -> io::Result<()> {
|
|||
"--connect must be a loopback address",
|
||||
));
|
||||
}
|
||||
if let Some(address) = args.listen {
|
||||
if !address.ip().is_loopback() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"--listen must be a loopback address",
|
||||
));
|
||||
}
|
||||
let listener = TcpListener::bind(address)?;
|
||||
eprintln!("runa-dap: listening on {}", listener.local_addr()?);
|
||||
for stream in listener.incoming() {
|
||||
let stream = stream?;
|
||||
let result = run_session(stream.try_clone()?, &stream, args.connect);
|
||||
if let Err(error) = stream.shutdown(Shutdown::Both) {
|
||||
eprintln!("runa-dap: could not close session socket: {error}");
|
||||
}
|
||||
if let Err(error) = result {
|
||||
eprintln!("runa-dap: session failed: {error}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
run_session(io::stdin(), io::stdout().lock(), args.connect)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_session(
|
||||
input: impl Read + Send + 'static,
|
||||
mut output: impl Write,
|
||||
connect: SocketAddr,
|
||||
) -> io::Result<()> {
|
||||
let (sender, receiver) = mpsc::sync_channel(16);
|
||||
std::thread::spawn(move || {
|
||||
let mut input = BufReader::new(io::stdin().lock());
|
||||
let mut input = BufReader::new(input);
|
||||
loop {
|
||||
let result = framing::read_frame(&mut input);
|
||||
let finished = !matches!(&result, Ok(Some(_)));
|
||||
|
|
@ -32,8 +65,7 @@ fn main() -> io::Result<()> {
|
|||
}
|
||||
}
|
||||
});
|
||||
let mut adapter = Adapter::new(WebSocketConnection::new(args.connect));
|
||||
let mut output = io::stdout().lock();
|
||||
let mut adapter = Adapter::new(WebSocketConnection::new(connect));
|
||||
while !adapter.is_closed() {
|
||||
let messages = match receiver.recv_timeout(Duration::from_millis(150)) {
|
||||
Ok(Ok(Some(message))) => adapter.handle(message),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use std::io::{Cursor, Write};
|
||||
use std::io::{BufRead, BufReader, Cursor, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
use runa_dap::framing::{read_frame, write_frame};
|
||||
use serde_json::{Value, json};
|
||||
|
|
@ -8,6 +10,7 @@ fn run(input: &[u8], args: &[&str]) -> std::process::Output {
|
|||
let mut child = Command::new(env!("CARGO_BIN_EXE_runa-dap"))
|
||||
.args(args)
|
||||
.env_remove("RUNA_CONNECT")
|
||||
.env_remove("RUNA_DAP_LISTEN")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
|
|
@ -82,9 +85,77 @@ fn cli_documents_connect_and_rejects_nonloopback_addresses() {
|
|||
let help = String::from_utf8(help.stdout).unwrap();
|
||||
assert!(help.contains("RUNA_CONNECT"));
|
||||
assert!(help.contains("127.0.0.1:3737"));
|
||||
assert!(help.contains("--listen"));
|
||||
assert!(help.contains("RUNA_DAP_LISTEN"));
|
||||
for address in ["0.0.0.0:3737", "192.0.2.1:3737", "invalid"] {
|
||||
let output = run(&[], &["--connect", address]);
|
||||
assert!(!output.status.success());
|
||||
assert!(!output.stderr.is_empty());
|
||||
}
|
||||
for address in ["0.0.0.0:0", "[::]:0", "192.0.2.1:3737", "invalid"] {
|
||||
let output = run(&[], &["--listen", address]);
|
||||
assert!(!output.status.success());
|
||||
assert!(!output.stderr.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcp_listener_accepts_framed_sessions_and_survives_invalid_framing() {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_runa-dap"))
|
||||
.args(["--listen", "127.0.0.1:0"])
|
||||
.env_remove("RUNA_CONNECT")
|
||||
.env_remove("RUNA_DAP_LISTEN")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let mut line = String::new();
|
||||
let mut diagnostics = BufReader::new(child.stderr.take().unwrap());
|
||||
diagnostics.read_line(&mut line).unwrap();
|
||||
let address = line.trim().strip_prefix("runa-dap: listening on ").unwrap();
|
||||
for malformed in [true, false, false] {
|
||||
let mut socket = TcpStream::connect(address).unwrap();
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
if malformed {
|
||||
socket
|
||||
.write_all(b"Content-Length: 1048577\r\n\r\n")
|
||||
.unwrap();
|
||||
let mut reader = BufReader::new(socket);
|
||||
assert_eq!(read_frame(&mut reader).unwrap().unwrap()["event"], "output");
|
||||
assert_eq!(
|
||||
read_frame(&mut reader).unwrap().unwrap()["event"],
|
||||
"terminated"
|
||||
);
|
||||
} else {
|
||||
write_frame(
|
||||
&mut socket,
|
||||
&json!({
|
||||
"seq": 1, "type": "request", "command": "initialize"
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let mut reader = BufReader::new(socket.try_clone().unwrap());
|
||||
assert_eq!(read_frame(&mut reader).unwrap().unwrap()["success"], true);
|
||||
write_frame(
|
||||
&mut socket,
|
||||
&json!({
|
||||
"seq": 2, "type": "request", "command": "disconnect"
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(read_frame(&mut reader).unwrap().unwrap()["success"], true);
|
||||
assert_eq!(
|
||||
read_frame(&mut reader).unwrap().unwrap()["event"],
|
||||
"terminated"
|
||||
);
|
||||
}
|
||||
}
|
||||
}));
|
||||
child.kill().unwrap();
|
||||
child.wait().unwrap();
|
||||
result.unwrap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,7 +229,16 @@ explicit debugger step operation advances only its selected paused actor.
|
|||
`runa-dap --connect 127.0.0.1:3737` speaks standard Content-Length-framed DAP
|
||||
on stdin/stdout and connects to an existing trusted loopback image using the
|
||||
WebSocket operations above. Diagnostics go to stderr. It does not start an
|
||||
image, tick the scheduler implicitly, or expose another network listener.
|
||||
image or tick the scheduler implicitly. Stdio mode opens no DAP network listener.
|
||||
|
||||
To accept DAP over TCP instead of stdio, use
|
||||
`runa-dap --listen 127.0.0.1:4711 --connect 127.0.0.1:3737`.
|
||||
`RUNA_DAP_LISTEN` also selects the DAP bind address; `--listen` overrides it.
|
||||
Only loopback addresses are accepted because the API is unauthenticated.
|
||||
Port `0` chooses an available port, reported on stderr. TCP uses the same
|
||||
Content-Length framing, handles one session at a time, and accepts a new
|
||||
session after disconnect or a malformed session. Without `--listen` or
|
||||
`RUNA_DAP_LISTEN`, the existing stdio behavior is unchanged.
|
||||
|
||||
An attach request supplies `actorRef`, optionally `behaviorSourceUri` (the
|
||||
actor's `behavior.rn` editor URI) and `workingSourceUri` (its shared image source
|
||||
|
|
|
|||
|
|
@ -274,6 +274,13 @@ invocation if any. An idle actor can be attached, but has no frames until it
|
|||
starts a handler. An already-failed actor can also be attached without changing
|
||||
its status, incarnation, state or mailbox.
|
||||
|
||||
For a DAP client connecting over TCP, start
|
||||
`runa-dap --listen 127.0.0.1:4711 --connect 127.0.0.1:3737`.
|
||||
The optional `RUNA_DAP_LISTEN` environment variable sets the same bind address.
|
||||
The listener is unauthenticated and restricted to loopback; it serves one
|
||||
session at a time. Port `0` selects an available port and prints it on stderr.
|
||||
The extension continues to launch its own stdio adapter by default.
|
||||
|
||||
- Set gutter breakpoints in each actor's `behavior.rn`, or its immutable loaded source, opened by the
|
||||
command or Call Stack view. The debugger serves the exact compiled text,
|
||||
including temporary/older implementations, using source references.
|
||||
|
|
|
|||
Loading…
Reference in New Issue