File: //proc/thread-self/root/usr/lib/ruby/gems/3.2.0/gems/debug-1.7.1/lib/debug/server_cdp.rb
# frozen_string_literal: true
require 'json'
require 'digest/sha1'
require 'base64'
require 'securerandom'
require 'stringio'
require 'open3'
require 'tmpdir'
require 'tempfile'
require 'timeout'
module DEBUGGER__
module UI_CDP
SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
class UnsupportedError < StandardError; end
class NotFoundChromeEndpointError < StandardError; end
class << self
def setup_chrome addr, uuid
return if CONFIG[:chrome_path] == ''
port, path, pid = run_new_chrome
begin
s = Socket.tcp '127.0.0.1', port
rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL
return
end
ws_client = WebSocketClient.new(s)
ws_client.handshake port, path
ws_client.send id: 1, method: 'Target.getTargets'
loop do
res = ws_client.extract_data
case res['id']
when 1
target_info = res.dig('result', 'targetInfos')
page = target_info.find{|t| t['type'] == 'page'}
ws_client.send id: 2, method: 'Target.attachToTarget',
params: {
targetId: page['targetId'],
flatten: true
}
when 2
s_id = res.dig('result', 'sessionId')
# TODO: change id
ws_client.send sessionId: s_id, id: 100, method: 'Network.enable'
ws_client.send sessionId: s_id, id: 3,
method: 'Page.enable'
when 3
s_id = res['sessionId']
ws_client.send sessionId: s_id, id: 4,
method: 'Page.getFrameTree'
when 4
s_id = res['sessionId']
f_id = res.dig('result', 'frameTree', 'frame', 'id')
ws_client.send sessionId: s_id, id: 5,
method: 'Page.navigate',
params: {
url: "devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&ws=#{addr}/#{uuid}",
frameId: f_id
}
when 101
break
else
if res['method'] == 'Network.webSocketWillSendHandshakeRequest'
s_id = res['sessionId']
# Display the console by entering ESC key
ws_client.send sessionId: s_id, id: 101, # TODO: change id
method:"Input.dispatchKeyEvent",
params: {
type:"keyDown",
windowsVirtualKeyCode:27 # ESC key
}
end
end
end
pid
rescue Errno::ENOENT, UnsupportedError, NotFoundChromeEndpointError
nil
end
TIMEOUT_SEC = 5
def run_new_chrome
path = CONFIG[:chrome_path]
data = nil
port = nil
wait_thr = nil
# The process to check OS is based on `selenium` project.
case RbConfig::CONFIG['host_os']
when /mswin|msys|mingw|cygwin|emc/
if path.nil?
candidates = ['C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe']
path = get_chrome_path candidates
end
uuid = SecureRandom.uuid
# The path is based on https://github.com/sindresorhus/open/blob/v8.4.0/index.js#L128.
stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
tf = Tempfile.create(['debug-', '.txt'])
stdin.puts("Start-process '#{path}' -Argumentlist '--remote-debugging-port=0', '--no-first-run', '--no-default-browser-check', '--user-data-dir=C:\\temp' -Wait -RedirectStandardError #{tf.path}")
stdin.close
stdout.close
stderr.close
port, path = get_devtools_endpoint(tf.path)
at_exit{
DEBUGGER__.skip_all
stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
stdin.puts("Stop-process -Name chrome")
stdin.close
stdout.close
stderr.close
tf.close
begin
File.unlink(tf)
rescue Errno::EACCES
end
}
when /darwin|mac os/
path = path || '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
dir = Dir.mktmpdir
# The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
stdin.close
stdout.close
data = stderr.readpartial 4096
stderr.close
if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
port = $1
path = $2
end
at_exit{
DEBUGGER__.skip_all
FileUtils.rm_rf dir
}
when /linux/
path = path || 'google-chrome'
dir = Dir.mktmpdir
# The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
stdin.close
stdout.close
data = ''
begin
Timeout.timeout(TIMEOUT_SEC) do
until data.match?(/DevTools listening on ws:\/\/127.0.0.1:\d+.*/)
data = stderr.readpartial 4096
end
end
rescue Exception
raise NotFoundChromeEndpointError
end
stderr.close
if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
port = $1
path = $2
end
at_exit{
DEBUGGER__.skip_all
FileUtils.rm_rf dir
}
else
raise UnsupportedError
end
[port, path, wait_thr.pid]
end
def get_chrome_path candidates
candidates.each{|c|
if File.exist? c
return c
end
}
raise UnsupportedError
end
ITERATIONS = 50
def get_devtools_endpoint tf
i = 1
while i < ITERATIONS
i += 1
if File.exist?(tf) && data = File.read(tf)
if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
port = $1
path = $2
return [port, path]
end
end
sleep 0.1
end
raise NotFoundChromeEndpointError
end
end
def send_chrome_response req
@repl = false
case req
when /^GET\s\/json\/version\sHTTP\/1.1/
body = {
Browser: "ruby/v#{RUBY_VERSION}",
'Protocol-Version': "1.1"
}
send_http_res body
raise UI_ServerBase::RetryConnection
when /^GET\s\/json\sHTTP\/1.1/
@uuid = @uuid || SecureRandom.uuid
addr = @local_addr.inspect_sockaddr
body = [{
description: "ruby instance",
devtoolsFrontendUrl: "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=#{addr}/#{@uuid}",
id: @uuid,
title: $0,
type: "node",
url: "file://#{File.absolute_path($0)}",
webSocketDebuggerUrl: "ws://#{addr}/#{@uuid}"
}]
send_http_res body
raise UI_ServerBase::RetryConnection
when /^GET\s\/(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\sHTTP\/1.1/
raise 'Incorrect uuid' unless $1 == @uuid
@need_pause_at_first = false
CONFIG.set_config no_color: true
@ws_server = WebSocketServer.new(@sock)
@ws_server.handshake
end
end
def send_http_res body
json = JSON.generate body
header = "HTTP/1.0 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\nCache-Control: no-cache\r\nContent-Length: #{json.bytesize}\r\n\r\n"
@sock.puts "#{header}#{json}"
end
module WebSocketUtils
class Frame
attr_reader :b
def initialize
@b = ''.b
end
def << obj
case obj
when String
@b << obj.b
when Enumerable
obj.each{|e| self << e}
end
end
def char bytes
@b << bytes
end
def ulonglong bytes
@b << [bytes].pack('Q>')
end
def uint16 bytes
@b << [bytes].pack('n*')
end
end
def show_protocol dir, msg
if DEBUGGER__::UI_CDP::SHOW_PROTOCOL
$stderr.puts "\#[#{dir}] #{msg}"
end
end
end
class WebSocketClient
include WebSocketUtils
def initialize s
@sock = s
end
def handshake port, path
key = SecureRandom.hex(11)
req = "GET #{path} HTTP/1.1\r\nHost: 127.0.0.1:#{port}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: #{key}==\r\n\r\n"
show_protocol :>, req
@sock.print req
res = @sock.readpartial 4092
show_protocol :<, res
if res.match /^Sec-WebSocket-Accept: (.*)\r\n/
correct_key = Base64.strict_encode64 Digest::SHA1.digest "#{key}==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
raise "The Sec-WebSocket-Accept value: #{$1} is not valid" unless $1 == correct_key
else
raise "Unknown response: #{res}"
end
end
def send **msg
msg = JSON.generate(msg)
show_protocol :>, msg
frame = Frame.new
fin = 0b10000000
opcode = 0b00000001
frame.char fin + opcode
mask = 0b10000000 # A client must mask all frames in a WebSocket Protocol.
bytesize = msg.bytesize
if bytesize < 126
payload_len = bytesize
frame.char mask + payload_len
elsif bytesize < 2 ** 16
payload_len = 0b01111110
frame.char mask + payload_len
frame.uint16 bytesize
elsif bytesize < 2 ** 64
payload_len = 0b01111111
frame.char mask + payload_len
frame.ulonglong bytesize
else
raise 'Bytesize is too big.'
end
masking_key = 4.times.map{
key = rand(1..255)
frame.char key
key
}
msg.bytes.each_with_index do |b, i|
frame.char(b ^ masking_key[i % 4])
end
@sock.print frame.b
end
def extract_data
first_group = @sock.getbyte
fin = first_group & 0b10000000 != 128
raise 'Unsupported' if fin
opcode = first_group & 0b00001111
raise "Unsupported: #{opcode}" unless opcode == 1
second_group = @sock.getbyte
mask = second_group & 0b10000000 == 128
raise 'The server must not mask any frames' if mask
payload_len = second_group & 0b01111111
# TODO: Support other payload_lengths
if payload_len == 126
payload_len = @sock.read(2).unpack('n*')[0]
end
msg = @sock.read payload_len
show_protocol :<, msg
JSON.parse msg
end
end
class Detach < StandardError
end
class WebSocketServer
include WebSocketUtils
def initialize s
@sock = s
end
def handshake
req = @sock.readpartial 4096
show_protocol '>', req
if req.match /^Sec-WebSocket-Key: (.*)\r\n/
accept = Base64.strict_encode64 Digest::SHA1.digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
res = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
@sock.print res
show_protocol :<, res
else
"Unknown request: #{req}"
end
end
def send **msg
msg = JSON.generate(msg)
show_protocol :<, msg
frame = Frame.new
fin = 0b10000000
opcode = 0b00000001
frame.char fin + opcode
mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
bytesize = msg.bytesize
if bytesize < 126
payload_len = bytesize
frame.char mask + payload_len
elsif bytesize < 2 ** 16
payload_len = 0b01111110
frame.char mask + payload_len
frame.uint16 bytesize
elsif bytesize < 2 ** 64
payload_len = 0b01111111
frame.char mask + payload_len
frame.ulonglong bytesize
else
raise 'Bytesize is too big.'
end
frame << msg
@sock.print frame.b
end
def extract_data
first_group = @sock.getbyte
fin = first_group & 0b10000000 != 128
raise 'Unsupported' if fin
opcode = first_group & 0b00001111
raise Detach if opcode == 8
raise "Unsupported: #{opcode}" unless opcode == 1
second_group = @sock.getbyte
mask = second_group & 0b10000000 == 128
raise 'The client must mask all frames' unless mask
payload_len = second_group & 0b01111111
# TODO: Support other payload_lengths
if payload_len == 126
payload_len = @sock.gets(2).unpack('n*')[0]
end
masking_key = []
4.times { masking_key << @sock.getbyte }
unmasked = []
payload_len.times do |n|
masked = @sock.getbyte
unmasked << (masked ^ masking_key[n % 4])
end
msg = unmasked.pack 'c*'
show_protocol :>, msg
JSON.parse msg
end
end
def send_response req, **res
if res.empty?
@ws_server.send id: req['id'], result: {}
else
@ws_server.send id: req['id'], result: res
end
end
def send_fail_response req, **res
@ws_server.send id: req['id'], error: res
end
def send_event method, **params
if params.empty?
@ws_server.send method: method, params: {}
else
@ws_server.send method: method, params: params
end
end
INVALID_REQUEST = -32600
def process
bps = {}
@src_map = {}
loop do
req = @ws_server.extract_data
case req['method']
## boot/configuration
when 'Debugger.getScriptSource'
@q_msg << req
when 'Debugger.enable'
send_response req
@q_msg << req
when 'Runtime.enable'
send_response req
send_event 'Runtime.executionContextCreated',
context: {
id: SecureRandom.hex(16),
origin: "http://#{@local_addr.inspect_sockaddr}",
name: ''
}
when 'Runtime.getIsolateId'
send_response req,
id: SecureRandom.hex
when 'Runtime.terminateExecution'
send_response req
exit
when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
'Runtime.releaseObjectGroup', 'Runtime.discardConsoleEntries', 'Log.clear', 'Runtime.runIfWaitingForDebugger'
send_response req
## control
when 'Debugger.resume'
send_response req
send_event 'Debugger.resumed'
@q_msg << 'c'
@q_msg << req
when 'Debugger.stepOver'
begin
@session.check_postmortem
send_response req
send_event 'Debugger.resumed'
@q_msg << 'n'
rescue PostmortemError
send_fail_response req,
code: INVALID_REQUEST,
message: "'stepOver' is not supported while postmortem mode"
ensure
@q_msg << req
end
when 'Debugger.stepInto'
begin
@session.check_postmortem
send_response req
send_event 'Debugger.resumed'
@q_msg << 's'
rescue PostmortemError
send_fail_response req,
code: INVALID_REQUEST,
message: "'stepInto' is not supported while postmortem mode"
ensure
@q_msg << req
end
when 'Debugger.stepOut'
begin
@session.check_postmortem
send_response req
send_event 'Debugger.resumed'
@q_msg << 'fin'
rescue PostmortemError
send_fail_response req,
code: INVALID_REQUEST,
message: "'stepOut' is not supported while postmortem mode"
ensure
@q_msg << req
end
when 'Debugger.setSkipAllPauses'
skip = req.dig('params', 'skip')
if skip
deactivate_bp
else
activate_bp bps
end
send_response req
# breakpoint
when 'Debugger.getPossibleBreakpoints'
@q_msg << req
when 'Debugger.setBreakpointByUrl'
line = req.dig('params', 'lineNumber')
if regexp = req.dig('params', 'urlRegex')
path = regexp.match(/(.*)\|/)[1].gsub("\\", "")
cond = req.dig('params', 'condition')
src = get_source_code path
end_line = src.lines.count
line = end_line if line > end_line
b_id = "1:#{line}:#{regexp}"
if cond != ''
SESSION.add_line_breakpoint(path, line + 1, cond: cond)
else
SESSION.add_line_breakpoint(path, line + 1)
end
bps[b_id] = bps.size
# Because we need to return scriptId, responses are returned in SESSION thread.
req['params']['scriptId'] = path
req['params']['lineNumber'] = line
req['params']['breakpointId'] = b_id
@q_msg << req
elsif url = req.dig('params', 'url')
b_id = "#{line}:#{url}"
send_response req,
breakpointId: b_id,
locations: []
elsif hash = req.dig('params', 'scriptHash')
b_id = "#{line}:#{hash}"
send_response req,
breakpointId: b_id,
locations: []
else
raise 'Unsupported'
end
when 'Debugger.removeBreakpoint'
b_id = req.dig('params', 'breakpointId')
bps = del_bp bps, b_id
send_response req
when 'Debugger.setBreakpointsActive'
active = req.dig('params', 'active')
if active
activate_bp bps
else
deactivate_bp # TODO: Change this part because catch breakpoints should not be deactivated.
end
send_response req
when 'Debugger.setPauseOnExceptions'
state = req.dig('params', 'state')
ex = 'Exception'
case state
when 'none'
@q_msg << 'config postmortem = false'
bps = del_bp bps, ex
when 'uncaught'
@q_msg << 'config postmortem = true'
bps = del_bp bps, ex
when 'all'
@q_msg << 'config postmortem = false'
SESSION.add_catch_breakpoint ex
bps[ex] = bps.size
end
send_response req
when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
@q_msg << req
end
end
rescue Detach
@q_msg << 'continue'
end
def del_bp bps, k
return bps unless idx = bps[k]
bps.delete k
bps.each_key{|i| bps[i] -= 1 if bps[i] > idx}
@q_msg << "del #{idx}"
bps
end
def get_source_code path
return @src_map[path] if @src_map[path]
src = File.read(path)
@src_map[path] = src
src
end
def activate_bp bps
bps.each_key{|k|
if k.match /^\d+:(\d+):(.*)/
line = $1
path = $2
SESSION.add_line_breakpoint(path, line.to_i + 1)
else
SESSION.add_catch_breakpoint 'Exception'
end
}
end
def deactivate_bp
@q_msg << 'del'
@q_ans << 'y'
end
def cleanup_reader
super
Process.kill :KILL, @chrome_pid if @chrome_pid
end
## Called by the SESSION thread
def respond req, **result
send_response req, **result
end
def respond_fail req, **result
send_fail_response req, **result
end
def fire_event event, **result
if result.empty?
send_event event
else
send_event event, **result
end
end
def sock skip: false
yield $stderr
end
def puts result
# STDERR.puts "puts: #{result}"
# send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
end
end
class Session
include GlobalVariablesHelper
# FIXME: unify this method with ThreadClient#propertyDescriptor.
def get_type obj
case obj
when Array
['object', 'array']
when Hash
['object', 'map']
when String
['string']
when TrueClass, FalseClass
['boolean']
when Symbol
['symbol']
when Integer, Float
['number']
when Exception
['object', 'error']
else
['object']
end
end
def fail_response req, **result
@ui.respond_fail req, **result
return :retry
end
INVALID_PARAMS = -32602
INTERNAL_ERROR = -32603
def process_protocol_request req
case req['method']
when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.enable'
request_tc [:cdp, :backtrace, req]
when 'Debugger.evaluateOnCallFrame'
frame_id = req.dig('params', 'callFrameId')
group = req.dig('params', 'objectGroup')
if fid = @frame_map[frame_id]
expr = req.dig('params', 'expression')
request_tc [:cdp, :evaluate, req, fid, expr, group]
else
fail_response req,
code: INVALID_PARAMS,
message: "'callFrameId' is an invalid"
end
when 'Runtime.getProperties', 'Runtime.getExceptionDetails'
oid = req.dig('params', 'objectId') || req.dig('params', 'errorObjectId')
if ref = @obj_map[oid]
case ref[0]
when 'local'
frame_id = ref[1]
fid = @frame_map[frame_id]
request_tc [:cdp, :scope, req, fid]
when 'global'
vars = safe_global_variables.sort.map do |name|
gv = eval(name.to_s)
prop = {
name: name,
value: {
description: gv.inspect
},
configurable: true,
enumerable: true
}
type, subtype = get_type(gv)
prop[:value][:type] = type
prop[:value][:subtype] = subtype if subtype
prop
end
@ui.respond req, result: vars
return :retry
when 'properties'
request_tc [:cdp, :properties, req, oid]
when 'exception'
request_tc [:cdp, :exception, req, oid]
when 'script'
# TODO: Support script and global types
@ui.respond req, result: []
return :retry
else
raise "Unknown type: #{ref.inspect}"
end
else
fail_response req,
code: INVALID_PARAMS,
message: "'objectId' is an invalid"
end
when 'Debugger.getScriptSource'
s_id = req.dig('params', 'scriptId')
if src = @src_map[s_id]
@ui.respond req, scriptSource: src
else
fail_response req,
code: INVALID_PARAMS,
message: "'scriptId' is an invalid"
end
return :retry
when 'Debugger.getPossibleBreakpoints'
s_id = req.dig('params', 'start', 'scriptId')
if src = @src_map[s_id]
lineno = req.dig('params', 'start', 'lineNumber')
end_line = src.lines.count
lineno = end_line if lineno > end_line
@ui.respond req,
locations: [{
scriptId: s_id,
lineNumber: lineno
}]
else
fail_response req,
code: INVALID_PARAMS,
message: "'scriptId' is an invalid"
end
return :retry
when 'Debugger.setBreakpointByUrl'
path = req.dig('params', 'scriptId')
if s_id = @scr_id_map[path]
lineno = req.dig('params', 'lineNumber')
b_id = req.dig('params', 'breakpointId')
@ui.respond req,
breakpointId: b_id,
locations: [{
scriptId: s_id,
lineNumber: lineno
}]
else
fail_response req,
code: INTERNAL_ERROR,
message: 'The target script is not found...'
end
return :retry
end
end
def cdp_event args
type, req, result = args
case type
when :backtrace
result[:callFrames].each.with_index do |frame, i|
frame_id = frame[:callFrameId]
@frame_map[frame_id] = i
path = frame[:url]
unless s_id = @scr_id_map[path]
s_id = (@scr_id_map.size + 1).to_s
@scr_id_map[path] = s_id
if path && File.exist?(path)
src = File.read(path)
end
@src_map[s_id] = src
end
if src = @src_map[s_id]
lineno = src.lines.count
else
lineno = 0
end
frame[:location][:scriptId] = s_id
frame[:functionLocation][:scriptId] = s_id
@ui.fire_event 'Debugger.scriptParsed',
scriptId: s_id,
url: frame[:url],
startLine: 0,
startColumn: 0,
endLine: lineno,
endColumn: 0,
executionContextId: 1,
hash: src.hash.inspect
frame[:scopeChain].each {|s|
oid = s.dig(:object, :objectId)
@obj_map[oid] = [s[:type], frame_id]
}
end
if oid = result.dig(:data, :objectId)
@obj_map[oid] = ['properties']
end
@ui.fire_event 'Debugger.paused', **result
when :evaluate
message = result.delete :message
if message
fail_response req,
code: INVALID_PARAMS,
message: message
else
src = req.dig('params', 'expression')
s_id = (@src_map.size + 1).to_s
@src_map[s_id] = src
lineno = src.lines.count
@ui.fire_event 'Debugger.scriptParsed',
scriptId: s_id,
url: '',
startLine: 0,
startColumn: 0,
endLine: lineno,
endColumn: 0,
executionContextId: 1,
hash: src.hash.inspect
if exc = result.dig(:response, :exceptionDetails)
exc[:stackTrace][:callFrames].each{|frame|
if frame[:url].empty?
frame[:scriptId] = s_id
else
path = frame[:url]
unless s_id = @scr_id_map[path]
s_id = (@scr_id_map.size + 1).to_s
@scr_id_map[path] = s_id
end
frame[:scriptId] = s_id
end
}
if oid = exc[:exception][:objectId]
@obj_map[oid] = ['exception']
end
end
rs = result.dig(:response, :result)
[rs].each{|obj|
if oid = obj[:objectId]
@obj_map[oid] = ['properties']
end
}
@ui.respond req, **result[:response]
out = result[:output]
if out && !out.empty?
@ui.fire_event 'Runtime.consoleAPICalled',
type: 'log',
args: [
type: out.class,
value: out
],
executionContextId: 1, # Change this number if something goes wrong.
timestamp: Time.now.to_f
end
end
when :scope
result.each{|obj|
if oid = obj.dig(:value, :objectId)
@obj_map[oid] = ['properties']
end
}
@ui.respond req, result: result
when :properties
result.each_value{|v|
v.each{|obj|
if oid = obj.dig(:value, :objectId)
@obj_map[oid] = ['properties']
end
}
}
@ui.respond req, **result
when :exception
@ui.respond req, **result
end
end
end
class ThreadClient
def process_cdp args
type = args.shift
req = args.shift
case type
when :backtrace
exception = nil
result = {
reason: 'other',
callFrames: @target_frames.map.with_index{|frame, i|
exception = frame.raised_exception if frame == current_frame && frame.has_raised_exception
path = frame.realpath || frame.path
if frame.iseq.nil?
lineno = 0
else
lineno = frame.iseq.first_line - 1
end
{
callFrameId: SecureRandom.hex(16),
functionName: frame.name,
functionLocation: {
# scriptId: N, # filled by SESSION
lineNumber: lineno
},
location: {
# scriptId: N, # filled by SESSION
lineNumber: frame.location.lineno - 1 # The line number is 0-based.
},
url: path,
scopeChain: [
{
type: 'local',
object: {
type: 'object',
objectId: rand.to_s
}
},
{
type: 'script',
object: {
type: 'object',
objectId: rand.to_s
}
},
{
type: 'global',
object: {
type: 'object',
objectId: rand.to_s
}
}
],
this: {
type: 'object'
}
}
}
}
if exception
result[:data] = evaluate_result exception
result[:reason] = 'exception'
end
event! :cdp_result, :backtrace, req, result
when :evaluate
res = {}
fid, expr, group = args
frame = @target_frames[fid]
message = nil
if frame && (b = frame.eval_binding)
special_local_variables frame do |name, var|
b.local_variable_set(name, var) if /\%/ !~name
end
result = nil
case group
when 'popover'
case expr
# Chrome doesn't read instance variables
when /\A\$\S/
safe_global_variables.each{|gvar|
if gvar.to_s == expr
result = eval(gvar.to_s)
break false
end
} and (message = "Error: Not defined global variable: #{expr.inspect}")
when /(\A((::[A-Z]|[A-Z])\w*)+)/
unless result = search_const(b, $1)
message = "Error: Not defined constant: #{expr.inspect}"
end
else
begin
result = b.local_variable_get(expr)
rescue NameError
# try to check method
if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true)
result = M_METHOD.bind_call(b.receiver, expr)
else
message = "Error: Can not evaluate: #{expr.inspect}"
end
end
end
when 'console', 'watch-group'
begin
orig_stdout = $stdout
$stdout = StringIO.new
result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
rescue Exception => e
result = e
res[:exceptionDetails] = exceptionDetails(e, 'Uncaught')
ensure
output = $stdout.string
$stdout = orig_stdout
end
else
message = "Error: unknown objectGroup: #{group}"
end
else
result = Exception.new("Error: Can not evaluate on this frame")
end
res[:result] = evaluate_result(result)
event! :cdp_result, :evaluate, req, message: message, response: res, output: output
when :scope
fid = args.shift
frame = @target_frames[fid]
if b = frame.binding
vars = b.local_variables.map{|name|
v = b.local_variable_get(name)
variable(name, v)
}
special_local_variables frame do |name, val|
vars.unshift variable(name, val)
end
vars.unshift variable('%self', b.receiver)
elsif lvars = frame.local_variables
vars = lvars.map{|var, val|
variable(var, val)
}
else
vars = [variable('%self', frame.self)]
special_local_variables frame do |name, val|
vars.unshift variable(name, val)
end
end
event! :cdp_result, :scope, req, vars
when :properties
oid = args.shift
result = []
prop = []
if obj = @obj_map[oid]
case obj
when Array
result = obj.map.with_index{|o, i|
variable i.to_s, o
}
when Hash
result = obj.map{|k, v|
variable(k, v)
}
when Struct
result = obj.members.map{|m|
variable(m, obj[m])
}
when String
prop = [
internalProperty('#length', obj.length),
internalProperty('#encoding', obj.encoding)
]
when Class, Module
result = obj.instance_variables.map{|iv|
variable(iv, obj.instance_variable_get(iv))
}
prop = [internalProperty('%ancestors', obj.ancestors[1..])]
when Range
prop = [
internalProperty('#begin', obj.begin),
internalProperty('#end', obj.end),
]
end
result += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
}
prop += [internalProperty('#class', M_CLASS.bind_call(obj))]
end
event! :cdp_result, :properties, req, result: result, internalProperties: prop
when :exception
oid = args.shift
exc = nil
if obj = @obj_map[oid]
exc = exceptionDetails obj, obj.to_s
end
event! :cdp_result, :exception, req, exceptionDetails: exc
end
end
def exceptionDetails exc, text
frames = [
{
columnNumber: 0,
functionName: 'eval',
lineNumber: 0,
url: ''
}
]
exc.backtrace_locations&.each do |loc|
break if loc.path == __FILE__
path = loc.absolute_path || loc.path
frames << {
columnNumber: 0,
functionName: loc.base_label,
lineNumber: loc.lineno - 1,
url: path
}
end
{
exceptionId: 1,
text: text,
lineNumber: 0,
columnNumber: 0,
exception: evaluate_result(exc),
stackTrace: {
callFrames: frames
}
}
end
def search_const b, expr
cs = expr.delete_prefix('::').split('::')
[Object, *b.eval('::Module.nesting')].reverse_each{|mod|
if cs.all?{|c|
if mod.const_defined?(c)
begin
mod = mod.const_get(c)
rescue Exception
false
end
else
false
end
}
# if-body
return mod
end
}
false
end
def evaluate_result r
v = variable nil, r
v[:value]
end
def internalProperty name, obj
v = variable name, obj
v.delete :configurable
v.delete :enumerable
v
end
def propertyDescriptor_ name, obj, type, description: nil, subtype: nil
description = DEBUGGER__.safe_inspect(obj, short: true) if description.nil?
oid = rand.to_s
@obj_map[oid] = obj
prop = {
name: name,
value: {
type: type,
description: description,
value: obj,
objectId: oid
},
configurable: true, # TODO: Change these parts because
enumerable: true # they are not necessarily `true`.
}
if type == 'object'
v = prop[:value]
v.delete :value
v[:subtype] = subtype if subtype
v[:className] = (klass = M_CLASS.bind_call(obj)).name || klass.to_s
end
prop
end
def preview_ value, hash, overflow
# The reason for not using "map" method is to prevent the object overriding it from causing bugs.
# https://github.com/ruby/debug/issues/781
props = []
hash.each{|k, v|
pd = propertyDescriptor k, v
props << {
name: pd[:name],
type: pd[:value][:type],
value: pd[:value][:description]
}
}
{
type: value[:type],
subtype: value[:subtype],
description: value[:description],
overflow: overflow,
properties: props
}
end
def variable name, obj
pd = propertyDescriptor name, obj
case obj
when Array
pd[:value][:preview] = preview name, obj
obj.each_with_index{|item, idx|
if valuePreview = preview(idx.to_s, item)
pd[:value][:preview][:properties][idx][:valuePreview] = valuePreview
end
}
when Hash
pd[:value][:preview] = preview name, obj
obj.each_with_index{|item, idx|
key, val = item
if valuePreview = preview(key, val)
pd[:value][:preview][:properties][idx][:valuePreview] = valuePreview
end
}
end
pd
end
def preview name, obj
case obj
when Array
pd = propertyDescriptor name, obj
overflow = false
if obj.size > 100
obj = obj[0..99]
overflow = true
end
hash = obj.each_with_index.to_h{|o, i| [i.to_s, o]}
preview_ pd[:value], hash, overflow
when Hash
pd = propertyDescriptor name, obj
overflow = false
if obj.size > 100
obj = obj.to_a[0..99].to_h
overflow = true
end
preview_ pd[:value], obj, overflow
else
nil
end
end
def propertyDescriptor name, obj
case obj
when Array
propertyDescriptor_ name, obj, 'object', subtype: 'array'
when Hash
propertyDescriptor_ name, obj, 'object', subtype: 'map'
when String
propertyDescriptor_ name, obj, 'string', description: obj
when TrueClass, FalseClass
propertyDescriptor_ name, obj, 'boolean'
when Symbol
propertyDescriptor_ name, obj, 'symbol'
when Integer, Float
propertyDescriptor_ name, obj, 'number'
when Exception
bt = ''
if log = obj.backtrace_locations
log.each do |loc|
break if loc.path == __FILE__
bt += " #{loc}\n"
end
end
propertyDescriptor_ name, obj, 'object', description: "#{obj.inspect}\n#{bt}", subtype: 'error'
else
propertyDescriptor_ name, obj, 'object'
end
end
end
end