Skip to content

Commit 11c0d3c

Browse files
committed
feat(dap): capture debuggee stdio
1 parent 14c8a54 commit 11c0d3c

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed

lib/debug/server_dap.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ def process
298298
process_request(req)
299299
end
300300
ensure
301+
restore_debuggee_stdio
301302
send_event :terminated unless @sock.closed?
302303
end
303304

@@ -314,6 +315,7 @@ def process_request req
314315
UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap') || true
315316
@nonstop = true
316317

318+
capture_debuggee_stdio
317319
load_extensions req
318320

319321
when 'attach'
@@ -326,6 +328,7 @@ def process_request req
326328
@nonstop = false
327329
end
328330

331+
capture_debuggee_stdio
329332
load_extensions req
330333

331334
when 'configurationDone'
@@ -397,6 +400,7 @@ def process_request req
397400
terminate = args.fetch("terminateDebuggee", false)
398401

399402
SESSION.clear_all_breakpoints
403+
restore_debuggee_stdio
400404
send_response req
401405

402406
if SESSION.in_subsession?
@@ -506,6 +510,56 @@ def puts result = ""
506510
send_event 'output', category: 'console', output: "#{result&.chomp}\n"
507511
end
508512

513+
def capture_debuggee_stdio
514+
return if @stdio_captured
515+
516+
@original_stdout = $stdout
517+
@original_stderr = $stderr
518+
519+
@stdout_reader, @stdout_writer = IO.pipe
520+
@stderr_reader, @stderr_writer = IO.pipe
521+
522+
@stdout_writer.sync = true
523+
@stderr_writer.sync = true
524+
525+
$stdout = @stdout_writer
526+
$stderr = @stderr_writer
527+
@stdio_captured = true
528+
529+
@stdout_monitor = start_monitor_thread(@stdout_reader, 'stdout')
530+
@stderr_monitor = start_monitor_thread(@stderr_reader, 'stderr')
531+
end
532+
533+
def restore_debuggee_stdio
534+
return unless @stdio_captured
535+
536+
$stdout = @original_stdout if @original_stdout
537+
$stderr = @original_stderr if @original_stderr
538+
539+
[@stdout_writer, @stderr_writer]
540+
.filter { |writer| !writer&.closed? }
541+
.each(&:close)
542+
543+
[@stdout_monitor, @stderr_monitor].each { |monitor| monitor&.join }
544+
[@stdout_reader, @stderr_reader].each { |reader| reader&.close unless reader&.closed? }
545+
546+
@stdio_captured = false
547+
end
548+
549+
private
550+
551+
def start_monitor_thread(reader, category)
552+
Thread.new do
553+
reader.each_line do |line|
554+
send_event 'output', category: category, output: line
555+
end
556+
rescue IOError, Errno::EBADF
557+
# Pipe closed, exit gracefully
558+
end
559+
end
560+
561+
public
562+
509563
def ignore_output_on_suspend?
510564
true
511565
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../support/protocol_test_case'
4+
5+
module DEBUGGER__
6+
class StdioCaptureDAPTest < ProtocolTestCase
7+
PROGRAM = <<~RUBY
8+
1| $stdout.puts "stdout message"
9+
2| $stderr.puts "stderr message"
10+
3| a = 1
11+
RUBY
12+
13+
def test_stdout_captured_as_output_event
14+
run_protocol_scenario PROGRAM, cdp: false do
15+
req_add_breakpoint 3
16+
req_continue
17+
18+
stdout_event = find_response :event, 'output', 'V<D'
19+
assert_equal 'stdout', stdout_event.dig(:body, :category)
20+
assert_match(/stdout message/, stdout_event.dig(:body, :output))
21+
22+
req_terminate_debuggee
23+
end
24+
end
25+
26+
def test_stderr_captured_as_output_event
27+
run_protocol_scenario PROGRAM, cdp: false do
28+
req_add_breakpoint 3
29+
req_continue
30+
31+
find_response :event, 'output', 'V<D'
32+
stderr_event = find_response :event, 'output', 'V<D'
33+
assert_equal 'stderr', stderr_event.dig(:body, :category)
34+
assert_match(/stderr message/, stderr_event.dig(:body, :output))
35+
36+
req_terminate_debuggee
37+
end
38+
end
39+
end
40+
end

0 commit comments

Comments
 (0)