Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions lib/debug/server_dap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ def process
process_request(req)
end
ensure
restore_debuggee_stdio
send_event :terminated unless @sock.closed?
end

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

capture_debuggee_stdio
load_extensions req

when 'attach'
Expand All @@ -326,6 +328,7 @@ def process_request req
@nonstop = false
end

capture_debuggee_stdio
load_extensions req

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

SESSION.clear_all_breakpoints
restore_debuggee_stdio
send_response req

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

def capture_debuggee_stdio
return if @stdio_captured

@original_stdout = $stdout
@original_stderr = $stderr

@stdout_reader, @stdout_writer = IO.pipe
@stderr_reader, @stderr_writer = IO.pipe

@stdout_writer.sync = true
@stderr_writer.sync = true

$stdout = @stdout_writer
$stderr = @stderr_writer
@stdio_captured = true

@stdout_monitor = start_monitor_thread(@stdout_reader, 'stdout')
@stderr_monitor = start_monitor_thread(@stderr_reader, 'stderr')
end

def restore_debuggee_stdio
return unless @stdio_captured

$stdout = @original_stdout if @original_stdout
$stderr = @original_stderr if @original_stderr

[@stdout_writer, @stderr_writer]
.filter { |writer| !writer&.closed? }
.each(&:close)

[@stdout_monitor, @stderr_monitor].each { |monitor| monitor&.join }
[@stdout_reader, @stderr_reader].each { |reader| reader&.close unless reader&.closed? }

@stdio_captured = false
end

private

def start_monitor_thread(reader, category)
Thread.new do
reader.each_line do |line|
send_event 'output', category: category, output: line
end
rescue IOError, Errno::EBADF
# Pipe closed, exit gracefully
end
end

public

def ignore_output_on_suspend?
true
end
Expand Down
40 changes: 40 additions & 0 deletions test/protocol/stdio_capture_dap_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require_relative '../support/protocol_test_case'

module DEBUGGER__
class StdioCaptureDAPTest < ProtocolTestCase
PROGRAM = <<~RUBY
1| $stdout.puts "stdout message"
2| $stderr.puts "stderr message"
3| a = 1
RUBY

def test_stdout_captured_as_output_event
run_protocol_scenario PROGRAM, cdp: false do
req_add_breakpoint 3
req_continue

stdout_event = find_response :event, 'output', 'V<D'
assert_equal 'stdout', stdout_event.dig(:body, :category)
assert_match(/stdout message/, stdout_event.dig(:body, :output))

req_terminate_debuggee
end
end

def test_stderr_captured_as_output_event
run_protocol_scenario PROGRAM, cdp: false do
req_add_breakpoint 3
req_continue

find_response :event, 'output', 'V<D'
stderr_event = find_response :event, 'output', 'V<D'
assert_equal 'stderr', stderr_event.dig(:body, :category)
assert_match(/stderr message/, stderr_event.dig(:body, :output))

req_terminate_debuggee
end
end
end
end