feat: forward adapter-process stdio as debuggee output events — Ruby all platforms, Rust on Windows (#222, #223) - #254
Merged
Conversation
…eLLDB's canonical terminal attribute (#225, #223) Go (#225): the launch transform now sets outputMode: 'remote' (user override wins), so Delve forwards the target's stdout/stderr as DAP output events instead of writing them to dlv's own stdio where they never reached get_output. Verified against dlv 1.26.3: the exec-mode e2e smoke test now asserts 'Hello, World!' arrives with category stdout. Rust (#223): the launch transform emitted a debugpy-style 'console' key; CodeLLDB deserializes that only as a legacy alias of its real 'terminal' attribute. Emit terminal: 'console' (translating legacy console values, explicit terminal wins). Investigation with a DAP trace showed the key rename does not by itself fix output capture on Windows: CodeLLDB's TerminalKind::Console performs no stdio redirection (launch.rs:460), so the debuggee inherits the adapter process's pipes — same topology as Ruby (#222). That gap is fixed by the adapter-stdio forwarding follow-up; docs updated to scope the known issue to Windows. Also: comprehensive-mcp-tools gains an outputMarker per-language check (enabled for go), and the go/rust skill references reflect the new state. Fixes #225 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…222, fixes #223) Some adapters hand the debuggee their own stdio instead of converting it to DAP output events: rdbg -c runs the target as a child of the adapter with inherited pipes on every platform, and CodeLLDB's console mode performs no stdio redirection at all — on Windows (where LLDB cannot intercept via its own pipes) the target inherits the adapter's handles. In both cases the program's output only ever reached the proxy log, never get_output. Mechanism (mirrors the #247 worker-side synthesis pattern): - AdapterSpawnConfig's spawn variant gains an opt-in forwardStdio field; RubyAdapterPolicy sets it for launch (attach has no adapter process by construction) with a /^DEBUGGER: / stderr exclusion for rdbg's banners; RustAdapterPolicy sets it on win32 only (POSIX gets output via CodeLLDB's own DAP events from LLDB's STDOUT/STDERR broadcasts — the channels are mutually exclusive). - GenericAdapterManager fans each stream's LineBuffer out to the new raw callback (all lines, blank included, unsanitized — the client must see the program's real output, parity with debugpy/js-debug) while the persisted-log path keeps its blank-drop + sanitizeStderr redaction byte-for-byte. - DapProxyWorker synthesizes sendDapEvent('output', {category, output}) from the callback, so entries land in the existing per-session ring buffer with no changes above the proxy. Exit-flush race (found in live verification): a debuggee printing to a block-buffered pipe flushes everything at exit, milliseconds AFTER the adapter's terminated event and socket close — and the SessionManager reacts to either by stripping listeners and stopping the proxy, so the flushed output was dropped. When forwarding is active the worker now holds exited/terminated forwarding and the dap_connection_closed status behind a stdio-drain barrier (streams' close events, 2s backstop). Stream data fires before close and IPC is FIFO, so the output deterministically wins the race. Verified live on Windows: Ruby fizzbuzz yields all 15 stdout lines + the stderr marker with rdbg banners excluded; Rust hello_world yields the full program output including the exit-time tail. Also: fizzbuzz.rb gains a warn marker (appended — all breakpoint line numbers preserved), Ruby/Rust e2e smoke tests assert get_output, the comprehensive matrix gains outputMarkers for ruby+rust, and docs/skill references now describe launch-works/attach-doesn't for Ruby. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
debugmcpdev
added a commit
that referenced
this pull request
Aug 5, 2026
…h sessions end 'stopped', not 'error' (fixes #258) (#263) Two defects, both exposed by the #254 stdio drain barrier: 1. Ordering: onExited/onTerminated/onClose all await the same drain barrier, and onTerminated's extra await (#252 synthesis) let onClose win by a microtask - the codeless dap_connection_closed status reached the parent before the terminated DAP event, stripping the handlers that would have marked the session stopped. A FIFO terminal-signal queue in the proxy worker now preserves arrival order, and the adapter process 'exit' status rides the same queue. 2. Fabricated exit code: ProxyManager emitted 'exit' with `message.code ?? 1` (and dap-core with `message.code || 1`, mangling a real 0), so every codeless closure became exit code 1 and SessionManager mapped it to ERROR - even for a clean run. Terminal statuses now carry an explicit `expected` flag (terminal DAP event already forwarded, or shutdown underway) and pass the code through untouched. SessionManager maps expected teardowns to STOPPED (recording the debuggee exit code), unexpected closures to ERROR, and keeps the legacy rule for real proxy-process exits. The duplicate unlatched 'exit' emit from the functional core is suppressed. Ruby also gains adapterExitCodeIsDebuggeeExitCode: rdbg -c propagates the debuggee's exit status but never sends a DAP exited event, so the worker now synthesizes one - clean run records exitCode 0, unhandled raise records exitCode 1, matching Python/js. Deferred (cosmetic): Ruby's isSessionReady still resolves the start_debugging ready-wait via the exit path for quick scripts, logging 'proxy exited during startup'; the reported state is now accurate. Co-authored-by: JF <john.franklin@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 12, 2026
debugmcpdev
added a commit
that referenced
this pull request
Aug 12, 2026
…317) (#319) * fix(ruby): stream debuggee stdout mid-run via injected sync prelude (#317) Ruby block-buffers $stdout when it is a pipe, and rdbg -c hands the debuggee the adapter process's piped stdio — so puts output only reached the proxy's stdio scraper at process exit (the #254/#258 exit-flush drain), never mid-run. The comprehensive matrix's ruby get_output cell soft-failed on exactly this: the session sits paused at a re-armed loop breakpoint, the process never exits, nothing flushes. Fix: buildTargetCommand now materializes a two-line prelude ($stdout.sync = true; $stderr.sync = true) in the session log dir and injects it into the target ruby argv as a single -r<path> element (both plain and bundler branches). Argv insertion is space-safe, unlike RUBYOPT which splits on whitespace; the log dir is product-owned, avoiding a predictable require path in shared /tmp. On write failure the launch proceeds without the prelude (exit-only flushing, as before). Launch mode only — attach connects to a process we did not start. Verified: rdbg's stop-at-load still lands on the main script, not the prelude. Regression coverage: hard e2e test asserting the marker arrives while the session is still paused (cannot be satisfied by the exit flush), plus unit tests for prelude materialization and argv shape. The matrix cell flips FAIL->PASS (175/0/6, was 174/1/6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: dedupe pollUntil into smoke-test-utils; hermetic logDir in ruby integration smoke Review follow-ups on #317: the new e2e test added a third verbatim copy of pollUntil — promote it to smoke-test-utils.ts and point all three suites at it. The ruby integration smoke test's logDir was <cwd>/logs/tests, which buildAdapterCommand now really writes into (sync prelude); use a per-test temp dir so the test stays hermetic and the -r assertion never depends on repo-tree writability. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: JF <john.franklin@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the
cluster:output-capturecluster: adapters whose debuggee inherits the adapter process's stdio now surface the program's output throughget_output. Stacked on #253.Two adapters have this topology:
rdbg -cruns the target as a child of the adapter process; DAP travels over a separate TCP socket and the program's stdio lands on the adapter's pipes, which were drained into the proxy log only.TerminalKind::Consoleperforms no stdio redirection (launch.rs:460). On POSIX, LLDB holds the debuggee's pipes and CodeLLDB emits DAP output events from theSTDOUT/STDERRprocess broadcasts (debug_session.rs:1478-1495); on Windows LLDB can't intercept, so the target inherits the adapter's handles. The channels are mutually exclusive, so the Windows-only opt-in cannot double-report on CI.Mechanism
Mirrors the #247 worker-side synthesis pattern:
AdapterSpawnConfig'sspawnvariant gains an opt-inforwardStdio?: { excludeStderrLinePattern?: RegExp }.RubyAdapterPolicysets it for launch — attach returnsmode: 'connect'(no adapter process), so launch-only is enforced by construction — with a/^DEBUGGER: /exclusion for rdbg's stderr banners.RustAdapterPolicysets it onwin32only. No other policy sets it → zero behavior change elsewhere (debugpy/js-debug adapter stdio is never forwarded).GenericAdapterManagerfans each stream's singleLineBufferout to the raw callback (all lines, blank included, unsanitized — the client must see the program's real output, parity with how debugpy/js-debug output reaches the buffer) while the persisted-log path keeps its blank-drop +sanitizeStderrredaction byte-for-byte.DapProxyWorkersynthesizessendDapEvent('output', { category, output }), so entries land in the existing per-session ring buffer with no changes above the proxy.The exit-flush race (found in live verification, not in planning)
A debuggee printing to a block-buffered pipe (Ruby's default) flushes everything at exit — milliseconds after the adapter's
terminatedevent and DAP socket close. The SessionManager reacts to either signal by stripping listeners and stopping the proxy, so the first live run captured the stderr marker but lost all 15 stdout lines. When forwarding is active, the worker now holdsexited/terminatedforwarding and thedap_connection_closedstatus behind a stdio-drain barrier (both streams'closeevents, 2 s backstop). Streamdatafires beforecloseand IPC is FIFO, so the flushed output deterministically reaches the buffer first. A worker unit test pins the ordering.Verified live (Windows, dev proxy)
putslines asstdout(including the exit flush),warnmarker asstderr, zeroDEBUGGER:entries.stdout, including the post-Process exitedtail.While verifying, found a pre-existing Windows CodeLLDB quirk (unrelated to this change):
continue_executionfrom a breakpoint re-stops at the same breakpoint — filed separately. The Rust e2e output assertion is designed around it (both markers print before the breakpoint, no continue needed).Test plan
tests/unit/proxy/dap-proxy-adapter-manager.test.ts— fan-out: raw callback + redacted log copy (the load-bearing parity assertion), blank lines, chunk-straddle, single flush on end+close, no-callback unchangedtests/proxy/dap-proxy-worker.test.ts— Ruby launch wires the forwarder (banner exclusion, blank →"\n"); Python gets none; drain barrier holdsterminated/dap_connection_closeduntil streams close, output ordered firsttests/adapters/ruby/unit/adapter-policy-ruby.test.ts+packages/shared/tests/unit/adapter-policy-rust.test.ts— opt-in shape, banner pattern semantics, win32-only gatingtests/e2e/mcp-server-smoke-ruby.test.ts— asserts6: Fizz(stdout),fizzbuzz complete(stderr), noDEBUGGER:entriestests/e2e/mcp-server-smoke-rust.test.ts— asserts both hello_world markers as stdouttests/e2e/mcp-server-smoke-ruby-attach.test.ts— attach unchangedFixes #222. Fixes #223.
🤖 Generated with Claude Code