Skip to content

Commit f1306fa

Browse files
committed
feat(dap): capture debuggee stdio
1 parent 6510cfb commit f1306fa

2 files changed

Lines changed: 94 additions & 1 deletion

File tree

lib/debug/server_dap.rb

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,6 @@ def dap_setup bytes
202202
send_event 'initialized'
203203
puts <<~WELCOME
204204
Ruby REPL: You can run any Ruby expression here.
205-
Note that output to the STDOUT/ERR printed on the TERMINAL.
206205
[experimental]
207206
`,COMMAND` runs `COMMAND` debug command (ex: `,info`).
208207
`,help` to list all debug commands.
@@ -298,6 +297,7 @@ def process
298297
process_request(req)
299298
end
300299
ensure
300+
restore_debuggee_stdio
301301
send_event :terminated unless @sock.closed?
302302
end
303303

@@ -314,6 +314,7 @@ def process_request req
314314
UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap') || true
315315
@nonstop = true
316316

317+
capture_debuggee_stdio
317318
load_extensions req
318319

319320
when 'attach'
@@ -326,6 +327,7 @@ def process_request req
326327
@nonstop = false
327328
end
328329

330+
capture_debuggee_stdio
329331
load_extensions req
330332

331333
when 'configurationDone'
@@ -397,6 +399,7 @@ def process_request req
397399
terminate = args.fetch("terminateDebuggee", false)
398400

399401
SESSION.clear_all_breakpoints
402+
restore_debuggee_stdio
400403
send_response req
401404

402405
if SESSION.in_subsession?
@@ -506,6 +509,56 @@ def puts result = ""
506509
send_event 'output', category: 'console', output: "#{result&.chomp}\n"
507510
end
508511

512+
def capture_debuggee_stdio
513+
return if @stdio_captured
514+
515+
@original_stdout = $stdout
516+
@original_stderr = $stderr
517+
518+
@stdout_reader, @stdout_writer = IO.pipe
519+
@stderr_reader, @stderr_writer = IO.pipe
520+
521+
@stdout_writer.sync = true
522+
@stderr_writer.sync = true
523+
524+
$stdout = @stdout_writer
525+
$stderr = @stderr_writer
526+
@stdio_captured = true
527+
528+
@stdout_monitor = start_monitor_thread(@stdout_reader, 'stdout')
529+
@stderr_monitor = start_monitor_thread(@stderr_reader, 'stderr')
530+
end
531+
532+
def restore_debuggee_stdio
533+
return unless @stdio_captured
534+
535+
$stdout = @original_stdout if @original_stdout
536+
$stderr = @original_stderr if @original_stderr
537+
538+
[@stdout_writer, @stderr_writer]
539+
.filter { |writer| !writer&.closed? }
540+
.each(&:close)
541+
542+
[@stdout_monitor, @stderr_monitor].each { |monitor| monitor&.join }
543+
[@stdout_reader, @stderr_reader].each { |reader| reader&.close unless reader&.closed? }
544+
545+
@stdio_captured = false
546+
end
547+
548+
private
549+
550+
def start_monitor_thread(reader, category)
551+
Thread.new do
552+
reader.each_line do |line|
553+
send_event 'output', category: category, output: line
554+
end
555+
rescue IOError, Errno::EBADF
556+
# Pipe closed, exit gracefully
557+
end
558+
end
559+
560+
public
561+
509562
def ignore_output_on_suspend?
510563
true
511564
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)