debug 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +116 -5
- data/ext/debug/debug.c +2 -1
- data/ext/debug/extconf.rb +2 -0
- data/lib/debug/client.rb +41 -17
- data/lib/debug/config.rb +30 -10
- data/lib/debug/console.rb +92 -25
- data/lib/debug/local.rb +4 -1
- data/lib/debug/prelude.rb +49 -0
- data/lib/debug/server.rb +161 -17
- data/lib/debug/server_cdp.rb +412 -0
- data/lib/debug/server_dap.rb +53 -23
- data/lib/debug/session.rb +397 -120
- data/lib/debug/thread_client.rb +4 -2
- data/lib/debug/version.rb +1 -1
- data/misc/README.md.erb +95 -1
- metadata +4 -2
data/lib/debug/server.rb
CHANGED
@@ -15,6 +15,8 @@ module DEBUGGER__
|
|
15
15
|
@q_ans = nil
|
16
16
|
@unsent_messages = []
|
17
17
|
@width = 80
|
18
|
+
@repl = true
|
19
|
+
@session = nil
|
18
20
|
end
|
19
21
|
|
20
22
|
class Terminate < StandardError
|
@@ -22,6 +24,7 @@ module DEBUGGER__
|
|
22
24
|
|
23
25
|
def deactivate
|
24
26
|
@reader_thread.raise Terminate
|
27
|
+
@reader_thread.join
|
25
28
|
end
|
26
29
|
|
27
30
|
def accept
|
@@ -36,9 +39,11 @@ module DEBUGGER__
|
|
36
39
|
end
|
37
40
|
|
38
41
|
def activate session, on_fork: false
|
42
|
+
@session = session
|
39
43
|
@reader_thread = Thread.new do
|
40
44
|
# An error on this thread should break the system.
|
41
45
|
Thread.current.abort_on_exception = true
|
46
|
+
Thread.current.name = 'DEBUGGER__::Server::reader'
|
42
47
|
|
43
48
|
accept do |server, already_connected: false|
|
44
49
|
DEBUGGER__.warn "Connected."
|
@@ -52,7 +57,7 @@ module DEBUGGER__
|
|
52
57
|
# flush unsent messages
|
53
58
|
@unsent_messages.each{|m|
|
54
59
|
@sock.puts m
|
55
|
-
}
|
60
|
+
} if @repl
|
56
61
|
@unsent_messages.clear
|
57
62
|
|
58
63
|
@q_msg = Queue.new
|
@@ -68,6 +73,7 @@ module DEBUGGER__
|
|
68
73
|
raise # should catch at outer scope
|
69
74
|
rescue => e
|
70
75
|
DEBUGGER__.warn "ReaderThreadError: #{e}"
|
76
|
+
pp e.backtrace
|
71
77
|
ensure
|
72
78
|
DEBUGGER__.warn "Disconnected."
|
73
79
|
@sock = nil
|
@@ -103,23 +109,58 @@ module DEBUGGER__
|
|
103
109
|
|
104
110
|
raise unless @sock.read(2) == "\r\n"
|
105
111
|
self.extend(UI_DAP)
|
112
|
+
@repl = false
|
106
113
|
dap_setup @sock.read($1.to_i)
|
114
|
+
when /^GET \/ HTTP\/1.1/
|
115
|
+
require_relative 'server_cdp'
|
116
|
+
|
117
|
+
self.extend(UI_CDP)
|
118
|
+
@repl = false
|
119
|
+
@web_sock = UI_CDP::WebSocket.new(@sock)
|
120
|
+
@web_sock.handshake
|
107
121
|
else
|
108
122
|
raise "Greeting message error: #{g}"
|
109
123
|
end
|
110
124
|
end
|
111
125
|
|
112
126
|
def process
|
113
|
-
while
|
127
|
+
while true
|
128
|
+
DEBUGGER__.info "sleep IO.select"
|
129
|
+
r = IO.select([@sock])
|
130
|
+
DEBUGGER__.info "wakeup IO.select"
|
131
|
+
|
132
|
+
line = @session.process_group.sync do
|
133
|
+
unless IO.select([@sock], nil, nil, 0)
|
134
|
+
DEBUGGER__.info "UI_Server can not read"
|
135
|
+
break :can_not_read
|
136
|
+
end
|
137
|
+
@sock.gets&.chomp.tap{|line|
|
138
|
+
DEBUGGER__.info "UI_Server received: #{line}"
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
next if line == :can_not_read
|
143
|
+
|
114
144
|
case line
|
115
145
|
when /\Apause/
|
116
146
|
pause
|
117
|
-
when /\Acommand ?(.+)/
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
147
|
+
when /\Acommand (\d+) (\d+) ?(.+)/
|
148
|
+
raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
|
149
|
+
|
150
|
+
if $1.to_i == Process.pid
|
151
|
+
@width = $2.to_i
|
152
|
+
@q_msg << $3
|
153
|
+
else
|
154
|
+
raise "pid:#{Process.pid} but get #{line}"
|
155
|
+
end
|
156
|
+
when /\Aanswer (\d+) (.*)/
|
157
|
+
raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
|
158
|
+
|
159
|
+
if $1.to_i == Process.pid
|
160
|
+
@q_ans << $2
|
161
|
+
else
|
162
|
+
raise "pid:#{Process.pid} but get #{line}"
|
163
|
+
end
|
123
164
|
else
|
124
165
|
STDERR.puts "unsupported: #{line}"
|
125
166
|
exit!
|
@@ -135,6 +176,21 @@ module DEBUGGER__
|
|
135
176
|
@width
|
136
177
|
end
|
137
178
|
|
179
|
+
def sigurg_overridden? prev_handler
|
180
|
+
case prev_handler
|
181
|
+
when "SYSTEM_DEFAULT"
|
182
|
+
false
|
183
|
+
when Proc
|
184
|
+
if prev_handler.source_location[0] == __FILE__
|
185
|
+
false
|
186
|
+
else
|
187
|
+
true
|
188
|
+
end
|
189
|
+
else
|
190
|
+
true
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
138
194
|
def setup_interrupt
|
139
195
|
prev_handler = trap(:SIGURG) do
|
140
196
|
# $stderr.puts "trapped SIGINT"
|
@@ -148,8 +204,8 @@ module DEBUGGER__
|
|
148
204
|
end
|
149
205
|
end
|
150
206
|
|
151
|
-
if prev_handler
|
152
|
-
DEBUGGER__.warn "SIGURG handler is
|
207
|
+
if sigurg_overridden?(prev_handler)
|
208
|
+
DEBUGGER__.warn "SIGURG handler is overridden by the debugger."
|
153
209
|
end
|
154
210
|
yield
|
155
211
|
ensure
|
@@ -191,7 +247,7 @@ module DEBUGGER__
|
|
191
247
|
|
192
248
|
def ask prompt
|
193
249
|
sock do |s|
|
194
|
-
s.puts "ask #{prompt}"
|
250
|
+
s.puts "ask #{Process.pid} #{prompt}"
|
195
251
|
@q_ans.pop
|
196
252
|
end
|
197
253
|
end
|
@@ -220,13 +276,23 @@ module DEBUGGER__
|
|
220
276
|
|
221
277
|
def readline prompt
|
222
278
|
input = (sock do |s|
|
223
|
-
|
279
|
+
if @repl
|
280
|
+
raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
|
281
|
+
line = "input #{Process.pid}"
|
282
|
+
DEBUGGER__.info "send: #{line}"
|
283
|
+
s.puts line
|
284
|
+
end
|
224
285
|
sleep 0.01 until @q_msg
|
286
|
+
@q_msg.pop.tap{|msg|
|
287
|
+
DEBUGGER__.info "readline: #{msg.inspect}"
|
288
|
+
}
|
289
|
+
end || 'continue')
|
225
290
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
291
|
+
if input.is_a?(String)
|
292
|
+
input.strip
|
293
|
+
else
|
294
|
+
input
|
295
|
+
end
|
230
296
|
end
|
231
297
|
|
232
298
|
def pause
|
@@ -244,6 +310,7 @@ module DEBUGGER__
|
|
244
310
|
|
245
311
|
class UI_TcpServer < UI_ServerBase
|
246
312
|
def initialize host: nil, port: nil
|
313
|
+
@addr = nil
|
247
314
|
@host = host || CONFIG[:host] || '127.0.0.1'
|
248
315
|
@port = port || begin
|
249
316
|
port_str = CONFIG[:port] || raise("Specify listening port by RUBY_DEBUG_PORT environment variable.")
|
@@ -263,7 +330,24 @@ module DEBUGGER__
|
|
263
330
|
|
264
331
|
begin
|
265
332
|
Socket.tcp_server_sockets @host, @port do |socks|
|
266
|
-
|
333
|
+
addr = socks[0].local_address.inspect_sockaddr # Change this part if `socks` are multiple.
|
334
|
+
rdbg = File.expand_path('../../exe/rdbg', __dir__)
|
335
|
+
|
336
|
+
DEBUGGER__.warn "Debugger can attach via TCP/IP (#{addr})"
|
337
|
+
DEBUGGER__.info <<~EOS
|
338
|
+
With rdbg, use the following command line:
|
339
|
+
#
|
340
|
+
# #{rdbg} --attach #{addr.split(':').join(' ')}
|
341
|
+
#
|
342
|
+
EOS
|
343
|
+
|
344
|
+
DEBUGGER__.warn <<~EOS if CONFIG[:open_frontend] == 'chrome'
|
345
|
+
With Chrome browser, type the following URL in the address-bar:
|
346
|
+
|
347
|
+
devtools://devtools/bundled/inspector.html?ws=#{addr}
|
348
|
+
|
349
|
+
EOS
|
350
|
+
|
267
351
|
Socket.accept_loop(socks) do |sock, client|
|
268
352
|
@client_addr = client
|
269
353
|
yield @sock_for_fork = sock
|
@@ -298,6 +382,64 @@ module DEBUGGER__
|
|
298
382
|
super()
|
299
383
|
end
|
300
384
|
|
385
|
+
def vscode_setup
|
386
|
+
require 'tmpdir'
|
387
|
+
require 'json'
|
388
|
+
require 'fileutils'
|
389
|
+
|
390
|
+
dir = Dir.mktmpdir("ruby-debug-vscode-")
|
391
|
+
at_exit{
|
392
|
+
FileUtils.rm_rf dir
|
393
|
+
}
|
394
|
+
Dir.chdir(dir) do
|
395
|
+
Dir.mkdir('.vscode')
|
396
|
+
open('README.rb', 'w'){|f|
|
397
|
+
f.puts <<~MSG
|
398
|
+
# Wait for starting the attaching to the Ruby process
|
399
|
+
# This file will be removed at the end of the debuggee process.
|
400
|
+
#
|
401
|
+
# Note that vscode-rdbg extension is needed. Please install if you don't have.
|
402
|
+
MSG
|
403
|
+
}
|
404
|
+
open('.vscode/launch.json', 'w'){|f|
|
405
|
+
f.puts JSON.pretty_generate({
|
406
|
+
version: '0.2.0',
|
407
|
+
configurations: [
|
408
|
+
{
|
409
|
+
type: "rdbg",
|
410
|
+
name: "Attach with rdbg",
|
411
|
+
request: "attach",
|
412
|
+
rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
|
413
|
+
debugPort: @sock_path,
|
414
|
+
autoAttach: true,
|
415
|
+
}
|
416
|
+
]
|
417
|
+
})
|
418
|
+
}
|
419
|
+
end
|
420
|
+
|
421
|
+
cmds = ['code', "#{dir}/", "#{dir}/README.rb"]
|
422
|
+
cmdline = cmds.join(' ')
|
423
|
+
ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/ #{dir}/README.rb"
|
424
|
+
|
425
|
+
STDERR.puts "Launching: #{cmdline}"
|
426
|
+
env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
|
427
|
+
|
428
|
+
unless system(env, *cmds)
|
429
|
+
DEBUGGER__.warn <<~MESSAGE
|
430
|
+
Can not invoke the command.
|
431
|
+
Use the command-line on your terminal (with modification if you need).
|
432
|
+
|
433
|
+
#{cmdline}
|
434
|
+
|
435
|
+
If your application is running on a SSH remote host, please try:
|
436
|
+
|
437
|
+
#{ssh_cmdline}
|
438
|
+
|
439
|
+
MESSAGE
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
301
443
|
def accept
|
302
444
|
super # for fork
|
303
445
|
|
@@ -310,6 +452,8 @@ module DEBUGGER__
|
|
310
452
|
end
|
311
453
|
|
312
454
|
::DEBUGGER__.warn "Debugger can attach via UNIX domain socket (#{@sock_path})"
|
455
|
+
vscode_setup if CONFIG[:open_frontend] == 'vscode'
|
456
|
+
|
313
457
|
Socket.unix_server_loop @sock_path do |sock, client|
|
314
458
|
@sock_for_fork = sock
|
315
459
|
@client_addr = client
|
@@ -0,0 +1,412 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'base64'
|
6
|
+
require 'securerandom'
|
7
|
+
|
8
|
+
module DEBUGGER__
|
9
|
+
module UI_CDP
|
10
|
+
SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
|
11
|
+
|
12
|
+
class WebSocket
|
13
|
+
def initialize s
|
14
|
+
@sock = s
|
15
|
+
end
|
16
|
+
|
17
|
+
def handshake
|
18
|
+
CONFIG.set_config no_color: true
|
19
|
+
|
20
|
+
req = @sock.readpartial 4096
|
21
|
+
$stderr.puts '[>]' + req if SHOW_PROTOCOL
|
22
|
+
|
23
|
+
if req.match /^Sec-WebSocket-Key: (.*)\r\n/
|
24
|
+
accept = Base64.strict_encode64 Digest::SHA1.digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
25
|
+
@sock.print "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
|
26
|
+
else
|
27
|
+
"Unknown request: #{req}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def send **msg
|
32
|
+
msg = JSON.generate(msg)
|
33
|
+
frame = []
|
34
|
+
fin = 0b10000000
|
35
|
+
opcode = 0b00000001
|
36
|
+
frame << fin + opcode
|
37
|
+
|
38
|
+
mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
|
39
|
+
bytesize = msg.bytesize
|
40
|
+
if bytesize < 126
|
41
|
+
payload_len = bytesize
|
42
|
+
elsif bytesize < 2 ** 16
|
43
|
+
payload_len = 0b01111110
|
44
|
+
ex_payload_len = [bytesize].pack('n*').bytes
|
45
|
+
else
|
46
|
+
payload_len = 0b01111111
|
47
|
+
ex_payload_len = [bytesize].pack('Q>').bytes
|
48
|
+
end
|
49
|
+
|
50
|
+
frame << mask + payload_len
|
51
|
+
frame.push *ex_payload_len if ex_payload_len
|
52
|
+
frame.push *msg.bytes
|
53
|
+
@sock.print frame.pack 'c*'
|
54
|
+
end
|
55
|
+
|
56
|
+
def extract_data
|
57
|
+
first_group = @sock.getbyte
|
58
|
+
fin = first_group & 0b10000000 != 128
|
59
|
+
raise 'Unsupported' if fin
|
60
|
+
opcode = first_group & 0b00001111
|
61
|
+
raise "Unsupported: #{opcode}" unless opcode == 1
|
62
|
+
|
63
|
+
second_group = @sock.getbyte
|
64
|
+
mask = second_group & 0b10000000 == 128
|
65
|
+
raise 'The client must mask all frames' unless mask
|
66
|
+
payload_len = second_group & 0b01111111
|
67
|
+
# TODO: Support other payload_lengths
|
68
|
+
if payload_len == 126
|
69
|
+
payload_len = @sock.gets(2).unpack('n*')[0]
|
70
|
+
end
|
71
|
+
|
72
|
+
masking_key = []
|
73
|
+
4.times { masking_key << @sock.getbyte }
|
74
|
+
unmasked = []
|
75
|
+
payload_len.times do |n|
|
76
|
+
masked = @sock.getbyte
|
77
|
+
unmasked << (masked ^ masking_key[n % 4])
|
78
|
+
end
|
79
|
+
JSON.parse unmasked.pack 'c*'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def send_response req, **res
|
84
|
+
if res.empty?
|
85
|
+
@web_sock.send id: req['id'], result: {}
|
86
|
+
else
|
87
|
+
@web_sock.send id: req['id'], result: res
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def send_event method, **params
|
92
|
+
if params.empty?
|
93
|
+
@web_sock.send method: method, params: {}
|
94
|
+
else
|
95
|
+
@web_sock.send method: method, params: params
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def process
|
100
|
+
loop do
|
101
|
+
req = @web_sock.extract_data
|
102
|
+
$stderr.puts '[>]' + req.inspect if SHOW_PROTOCOL
|
103
|
+
bps = []
|
104
|
+
|
105
|
+
case req['method']
|
106
|
+
|
107
|
+
## boot/configuration
|
108
|
+
when 'Page.getResourceTree'
|
109
|
+
abs = File.absolute_path($0)
|
110
|
+
src = File.read(abs)
|
111
|
+
send_response req,
|
112
|
+
frameTree: {
|
113
|
+
frame: {
|
114
|
+
id: SecureRandom.hex(16),
|
115
|
+
loaderId: SecureRandom.hex(16),
|
116
|
+
url: 'http://debuggee/',
|
117
|
+
securityOrigin: 'http://debuggee',
|
118
|
+
mimeType: 'text/plain' },
|
119
|
+
resources: [
|
120
|
+
]
|
121
|
+
}
|
122
|
+
send_event 'Debugger.scriptParsed',
|
123
|
+
scriptId: abs,
|
124
|
+
url: "http://debuggee#{abs}",
|
125
|
+
startLine: 0,
|
126
|
+
startColumn: 0,
|
127
|
+
endLine: src.count('\n'),
|
128
|
+
endColumn: 0,
|
129
|
+
executionContextId: 1,
|
130
|
+
hash: src.hash
|
131
|
+
send_event 'Runtime.executionContextCreated',
|
132
|
+
context: {
|
133
|
+
id: SecureRandom.hex(16),
|
134
|
+
origin: "http://#{@addr}",
|
135
|
+
name: ''
|
136
|
+
}
|
137
|
+
when 'Debugger.getScriptSource'
|
138
|
+
s_id = req.dig('params', 'scriptId')
|
139
|
+
src = File.read(s_id)
|
140
|
+
send_response req, scriptSource: src
|
141
|
+
@q_msg << req
|
142
|
+
when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
|
143
|
+
'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
|
144
|
+
'Debugger.setBreakpointsActive', 'Runtime.releaseObjectGroup'
|
145
|
+
send_response req
|
146
|
+
|
147
|
+
## control
|
148
|
+
when 'Debugger.resume'
|
149
|
+
@q_msg << 'c'
|
150
|
+
@q_msg << req
|
151
|
+
send_response req
|
152
|
+
send_event 'Debugger.resumed'
|
153
|
+
when 'Debugger.stepOver'
|
154
|
+
@q_msg << 'n'
|
155
|
+
@q_msg << req
|
156
|
+
send_response req
|
157
|
+
send_event 'Debugger.resumed'
|
158
|
+
when 'Debugger.stepInto'
|
159
|
+
@q_msg << 's'
|
160
|
+
@q_msg << req
|
161
|
+
send_response req
|
162
|
+
send_event 'Debugger.resumed'
|
163
|
+
when 'Debugger.stepOut'
|
164
|
+
@q_msg << 'fin'
|
165
|
+
@q_msg << req
|
166
|
+
send_response req
|
167
|
+
send_event 'Debugger.resumed'
|
168
|
+
|
169
|
+
# breakpoint
|
170
|
+
when 'Debugger.getPossibleBreakpoints'
|
171
|
+
s_id = req.dig('params', 'start', 'scriptId')
|
172
|
+
line = req.dig('params', 'lineNumber')
|
173
|
+
send_response req,
|
174
|
+
locations: [
|
175
|
+
{ scriptId: s_id,
|
176
|
+
lineNumber: line,
|
177
|
+
}
|
178
|
+
]
|
179
|
+
when 'Debugger.setBreakpointByUrl'
|
180
|
+
line = req.dig('params', 'lineNumber')
|
181
|
+
path = req.dig('params', 'url').match('http://debuggee(.*)')[1]
|
182
|
+
cond = req.dig('params', 'condition')
|
183
|
+
if cond != ''
|
184
|
+
bps << SESSION.add_line_breakpoint(path, line + 1, cond: cond)
|
185
|
+
else
|
186
|
+
bps << SESSION.add_line_breakpoint(path, line + 1)
|
187
|
+
end
|
188
|
+
send_response req,
|
189
|
+
breakpointId: (bps.size - 1).to_s,
|
190
|
+
locations: [
|
191
|
+
scriptId: path,
|
192
|
+
lineNumber: line
|
193
|
+
]
|
194
|
+
when 'Debugger.removeBreakpoint'
|
195
|
+
b_id = req.dig('params', 'breakpointId')
|
196
|
+
@q_msg << "del #{b_id}"
|
197
|
+
send_response req
|
198
|
+
|
199
|
+
when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
|
200
|
+
@q_msg << req
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
## Called by the SESSION thread
|
206
|
+
|
207
|
+
def readline prompt
|
208
|
+
@q_msg.pop || 'kill!'
|
209
|
+
end
|
210
|
+
|
211
|
+
def respond req, **result
|
212
|
+
send_response req, **result
|
213
|
+
end
|
214
|
+
|
215
|
+
def fire_event event, **result
|
216
|
+
if result.empty?
|
217
|
+
send_event event
|
218
|
+
else
|
219
|
+
send_event event, **result
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def sock skip: false
|
224
|
+
yield $stderr
|
225
|
+
end
|
226
|
+
|
227
|
+
def puts result
|
228
|
+
# STDERR.puts "puts: #{result}"
|
229
|
+
# send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
class Session
|
234
|
+
def process_protocol_request req
|
235
|
+
case req['method']
|
236
|
+
when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource'
|
237
|
+
@tc << [:cdp, :backtrace, req]
|
238
|
+
when 'Debugger.evaluateOnCallFrame'
|
239
|
+
expr = req.dig('params', 'expression')
|
240
|
+
@tc << [:cdp, :evaluate, req, expr]
|
241
|
+
when 'Runtime.getProperties'
|
242
|
+
oid = req.dig('params', 'objectId')
|
243
|
+
case oid
|
244
|
+
when /(\d?):local/
|
245
|
+
@tc << [:cdp, :properties, req, $1.to_i]
|
246
|
+
when /\d?:script/
|
247
|
+
# TODO: Support a script type
|
248
|
+
@ui.respond req
|
249
|
+
return :retry
|
250
|
+
when /\d?:global/
|
251
|
+
# TODO: Support a global type
|
252
|
+
@ui.respond req
|
253
|
+
return :retry
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def cdp_event args
|
259
|
+
type, req, result = args
|
260
|
+
|
261
|
+
case type
|
262
|
+
when :backtrace
|
263
|
+
result[:callFrames].each do |frame|
|
264
|
+
s_id = frame.dig(:location, :scriptId)
|
265
|
+
if File.exist?(s_id) && !@script_paths.include?(s_id)
|
266
|
+
src = File.read(s_id)
|
267
|
+
@ui.fire_event 'Debugger.scriptParsed',
|
268
|
+
scriptId: s_id,
|
269
|
+
url: frame[:url],
|
270
|
+
startLine: 0,
|
271
|
+
startColumn: 0,
|
272
|
+
endLine: src.count('\n'),
|
273
|
+
endColumn: 0,
|
274
|
+
executionContextId: @script_paths.size + 1,
|
275
|
+
hash: src.hash
|
276
|
+
@script_paths << s_id
|
277
|
+
end
|
278
|
+
end
|
279
|
+
result[:reason] = 'other'
|
280
|
+
@ui.fire_event 'Debugger.paused', **result
|
281
|
+
when :evaluate
|
282
|
+
@ui.respond req, result: result
|
283
|
+
when :properties
|
284
|
+
@ui.respond req, result: result
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
class ThreadClient
|
290
|
+
def process_cdp args
|
291
|
+
type = args.shift
|
292
|
+
req = args.shift
|
293
|
+
|
294
|
+
case type
|
295
|
+
when :backtrace
|
296
|
+
event! :cdp_result, :backtrace, req, {
|
297
|
+
callFrames: @target_frames.map.with_index{|frame, i|
|
298
|
+
path = frame.realpath || frame.path
|
299
|
+
if path.match /<internal:(.*)>/
|
300
|
+
abs = $1
|
301
|
+
else
|
302
|
+
abs = path
|
303
|
+
end
|
304
|
+
|
305
|
+
local_scope = {
|
306
|
+
callFrameId: SecureRandom.hex(16),
|
307
|
+
functionName: frame.name,
|
308
|
+
location: {
|
309
|
+
scriptId: abs,
|
310
|
+
lineNumber: frame.location.lineno - 1 # The line number is 0-based.
|
311
|
+
},
|
312
|
+
url: "http://debuggee#{abs}",
|
313
|
+
scopeChain: [
|
314
|
+
{
|
315
|
+
type: 'local',
|
316
|
+
object: {
|
317
|
+
type: 'object',
|
318
|
+
objectId: "#{i}:local"
|
319
|
+
}
|
320
|
+
},
|
321
|
+
{
|
322
|
+
type: 'script',
|
323
|
+
object: {
|
324
|
+
type: 'object',
|
325
|
+
objectId: "#{i}:script"
|
326
|
+
}
|
327
|
+
},
|
328
|
+
{
|
329
|
+
type: 'global',
|
330
|
+
object: {
|
331
|
+
type: 'object',
|
332
|
+
objectId: "#{i}:global"
|
333
|
+
}
|
334
|
+
}
|
335
|
+
],
|
336
|
+
this: {
|
337
|
+
type: 'object'
|
338
|
+
}
|
339
|
+
}
|
340
|
+
}
|
341
|
+
}
|
342
|
+
when :evaluate
|
343
|
+
expr = args.shift
|
344
|
+
begin
|
345
|
+
result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
|
346
|
+
rescue Exception => e
|
347
|
+
result = e
|
348
|
+
end
|
349
|
+
event! :cdp_result, :evaluate, req, evaluate_result(result)
|
350
|
+
when :properties
|
351
|
+
fid = args.shift
|
352
|
+
frame = @target_frames[fid]
|
353
|
+
if b = frame.binding
|
354
|
+
vars = b.local_variables.map{|name|
|
355
|
+
v = b.local_variable_get(name)
|
356
|
+
variable(name, v)
|
357
|
+
}
|
358
|
+
vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
|
359
|
+
vars.unshift variable('%return', frame.return_value) if frame.has_return_value
|
360
|
+
vars.unshift variable('%self', b.receiver)
|
361
|
+
elsif lvars = frame.local_variables
|
362
|
+
vars = lvars.map{|var, val|
|
363
|
+
variable(var, val)
|
364
|
+
}
|
365
|
+
else
|
366
|
+
vars = [variable('%self', frame.self)]
|
367
|
+
vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
|
368
|
+
vars.push variable('%return', frame.return_value) if frame.has_return_value
|
369
|
+
end
|
370
|
+
event! :cdp_result, :properties, req, vars
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
def evaluate_result r
|
375
|
+
v = variable nil, r
|
376
|
+
v[:value]
|
377
|
+
end
|
378
|
+
|
379
|
+
def variable_ name, obj, type, use_short: true
|
380
|
+
{
|
381
|
+
name: name,
|
382
|
+
value: {
|
383
|
+
type: type,
|
384
|
+
value: DEBUGGER__.short_inspect(obj, use_short)
|
385
|
+
},
|
386
|
+
configurable: true,
|
387
|
+
enumerable: true
|
388
|
+
}
|
389
|
+
end
|
390
|
+
|
391
|
+
def variable name, obj
|
392
|
+
case obj
|
393
|
+
when Array, Hash, Range, NilClass, Time
|
394
|
+
variable_ name, obj, 'object'
|
395
|
+
when String
|
396
|
+
variable_ name, obj, 'string', use_short: false
|
397
|
+
when Class, Module, Struct
|
398
|
+
variable_ name, obj, 'function'
|
399
|
+
when TrueClass, FalseClass
|
400
|
+
variable_ name, obj, 'boolean'
|
401
|
+
when Symbol
|
402
|
+
variable_ name, obj, 'symbol'
|
403
|
+
when Float
|
404
|
+
variable_ name, obj, 'number'
|
405
|
+
when Integer
|
406
|
+
variable_ name, obj, 'number'
|
407
|
+
else
|
408
|
+
variable_ name, obj, 'undefined'
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|