debug 1.3.4 → 1.4.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/.github/pull_request_template.md +9 -0
- data/CONTRIBUTING.md +39 -6
- data/README.md +34 -7
- data/bin/gentest +12 -4
- data/ext/debug/debug.c +4 -1
- data/lib/debug/breakpoint.rb +61 -11
- data/lib/debug/client.rb +57 -16
- data/lib/debug/color.rb +29 -19
- data/lib/debug/config.rb +6 -3
- data/lib/debug/console.rb +17 -3
- data/lib/debug/frame_info.rb +11 -16
- data/lib/debug/prelude.rb +2 -2
- data/lib/debug/server.rb +45 -77
- data/lib/debug/server_cdp.rb +590 -93
- data/lib/debug/server_dap.rb +231 -53
- data/lib/debug/session.rb +114 -52
- data/lib/debug/source_repository.rb +4 -6
- data/lib/debug/thread_client.rb +67 -48
- data/lib/debug/tracer.rb +1 -1
- data/lib/debug/version.rb +1 -1
- data/misc/README.md.erb +12 -4
- metadata +3 -2
data/lib/debug/server_cdp.rb
CHANGED
@@ -4,20 +4,178 @@ require 'json'
|
|
4
4
|
require 'digest/sha1'
|
5
5
|
require 'base64'
|
6
6
|
require 'securerandom'
|
7
|
+
require 'stringio'
|
8
|
+
require 'open3'
|
9
|
+
require 'tmpdir'
|
7
10
|
|
8
11
|
module DEBUGGER__
|
9
12
|
module UI_CDP
|
10
13
|
SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
|
11
14
|
|
12
|
-
class
|
15
|
+
class << self
|
16
|
+
def setup_chrome addr
|
17
|
+
return if CONFIG[:chrome_path] == ''
|
18
|
+
|
19
|
+
port, path, pid = run_new_chrome
|
20
|
+
begin
|
21
|
+
s = Socket.tcp '127.0.0.1', port
|
22
|
+
rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL
|
23
|
+
return
|
24
|
+
end
|
25
|
+
|
26
|
+
ws_client = WebSocketClient.new(s)
|
27
|
+
ws_client.handshake port, path
|
28
|
+
ws_client.send id: 1, method: 'Target.getTargets'
|
29
|
+
|
30
|
+
4.times do
|
31
|
+
res = ws_client.extract_data
|
32
|
+
case
|
33
|
+
when res['id'] == 1 && target_info = res.dig('result', 'targetInfos')
|
34
|
+
page = target_info.find{|t| t['type'] == 'page'}
|
35
|
+
ws_client.send id: 2, method: 'Target.attachToTarget',
|
36
|
+
params: {
|
37
|
+
targetId: page['targetId'],
|
38
|
+
flatten: true
|
39
|
+
}
|
40
|
+
when res['id'] == 2
|
41
|
+
s_id = res.dig('result', 'sessionId')
|
42
|
+
sleep 0.1
|
43
|
+
ws_client.send sessionId: s_id, id: 1,
|
44
|
+
method: 'Page.navigate',
|
45
|
+
params: {
|
46
|
+
url: "devtools://devtools/bundled/inspector.html?ws=#{addr}"
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
pid
|
51
|
+
rescue Errno::ENOENT
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_chrome_path
|
56
|
+
return CONFIG[:chrome_path] if CONFIG[:chrome_path]
|
57
|
+
|
58
|
+
# The process to check OS is based on `selenium` project.
|
59
|
+
case RbConfig::CONFIG['host_os']
|
60
|
+
when /mswin|msys|mingw|cygwin|emc/
|
61
|
+
'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
|
62
|
+
when /darwin|mac os/
|
63
|
+
'/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
|
64
|
+
when /linux/
|
65
|
+
'google-chrome'
|
66
|
+
else
|
67
|
+
raise "Unsupported OS"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def run_new_chrome
|
72
|
+
dir = Dir.mktmpdir
|
73
|
+
# The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting
|
74
|
+
stdin, stdout, stderr, wait_thr = *Open3.popen3("#{get_chrome_path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
|
75
|
+
stdin.close
|
76
|
+
stdout.close
|
77
|
+
|
78
|
+
data = stderr.readpartial 4096
|
79
|
+
if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
|
80
|
+
port = $1
|
81
|
+
path = $2
|
82
|
+
end
|
83
|
+
stderr.close
|
84
|
+
|
85
|
+
at_exit{
|
86
|
+
CONFIG[:skip_path] = [//] # skip all
|
87
|
+
FileUtils.rm_rf dir
|
88
|
+
}
|
89
|
+
|
90
|
+
[port, path, wait_thr.pid]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class WebSocketClient
|
13
95
|
def initialize s
|
14
96
|
@sock = s
|
15
97
|
end
|
16
98
|
|
17
|
-
def handshake
|
99
|
+
def handshake port, path
|
100
|
+
key = SecureRandom.hex(11)
|
101
|
+
@sock.print "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"
|
102
|
+
res = @sock.readpartial 4092
|
103
|
+
$stderr.puts '[>]' + res if SHOW_PROTOCOL
|
104
|
+
|
105
|
+
if res.match /^Sec-WebSocket-Accept: (.*)\r\n/
|
106
|
+
correct_key = Base64.strict_encode64 Digest::SHA1.digest "#{key}==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
107
|
+
raise "The Sec-WebSocket-Accept value: #{$1} is not valid" unless $1 == correct_key
|
108
|
+
else
|
109
|
+
raise "Unknown response: #{res}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def send **msg
|
114
|
+
msg = JSON.generate(msg)
|
115
|
+
frame = []
|
116
|
+
fin = 0b10000000
|
117
|
+
opcode = 0b00000001
|
118
|
+
frame << fin + opcode
|
119
|
+
|
120
|
+
mask = 0b10000000 # A client must mask all frames in a WebSocket Protocol.
|
121
|
+
bytesize = msg.bytesize
|
122
|
+
if bytesize < 126
|
123
|
+
payload_len = bytesize
|
124
|
+
elsif bytesize < 2 ** 16
|
125
|
+
payload_len = 0b01111110
|
126
|
+
ex_payload_len = [bytesize].pack('n*').bytes
|
127
|
+
else
|
128
|
+
payload_len = 0b01111111
|
129
|
+
ex_payload_len = [bytesize].pack('Q>').bytes
|
130
|
+
end
|
131
|
+
|
132
|
+
frame << mask + payload_len
|
133
|
+
frame.push *ex_payload_len if ex_payload_len
|
134
|
+
|
135
|
+
frame.push *masking_key = 4.times.map{rand(1..255)}
|
136
|
+
masked = []
|
137
|
+
msg.bytes.each_with_index do |b, i|
|
138
|
+
masked << (b ^ masking_key[i % 4])
|
139
|
+
end
|
140
|
+
|
141
|
+
frame.push *masked
|
142
|
+
@sock.print frame.pack 'c*'
|
143
|
+
end
|
144
|
+
|
145
|
+
def extract_data
|
146
|
+
first_group = @sock.getbyte
|
147
|
+
fin = first_group & 0b10000000 != 128
|
148
|
+
raise 'Unsupported' if fin
|
149
|
+
opcode = first_group & 0b00001111
|
150
|
+
raise "Unsupported: #{opcode}" unless opcode == 1
|
151
|
+
|
152
|
+
second_group = @sock.getbyte
|
153
|
+
mask = second_group & 0b10000000 == 128
|
154
|
+
raise 'The server must not mask any frames' if mask
|
155
|
+
payload_len = second_group & 0b01111111
|
156
|
+
# TODO: Support other payload_lengths
|
157
|
+
if payload_len == 126
|
158
|
+
payload_len = @sock.read(2).unpack('n*')[0]
|
159
|
+
end
|
160
|
+
|
161
|
+
data = JSON.parse @sock.read payload_len
|
162
|
+
$stderr.puts '[>]' + data.inspect if SHOW_PROTOCOL
|
163
|
+
data
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class Detach < StandardError
|
168
|
+
end
|
169
|
+
|
170
|
+
class WebSocketServer
|
171
|
+
def initialize s
|
172
|
+
@sock = s
|
173
|
+
end
|
174
|
+
|
175
|
+
def handshake
|
18
176
|
req = @sock.readpartial 4096
|
19
177
|
$stderr.puts '[>]' + req if SHOW_PROTOCOL
|
20
|
-
|
178
|
+
|
21
179
|
if req.match /^Sec-WebSocket-Key: (.*)\r\n/
|
22
180
|
accept = Base64.strict_encode64 Digest::SHA1.digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
23
181
|
@sock.print "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
|
@@ -32,7 +190,7 @@ module DEBUGGER__
|
|
32
190
|
fin = 0b10000000
|
33
191
|
opcode = 0b00000001
|
34
192
|
frame << fin + opcode
|
35
|
-
|
193
|
+
|
36
194
|
mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
|
37
195
|
bytesize = msg.bytesize
|
38
196
|
if bytesize < 126
|
@@ -44,7 +202,7 @@ module DEBUGGER__
|
|
44
202
|
payload_len = 0b01111111
|
45
203
|
ex_payload_len = [bytesize].pack('Q>').bytes
|
46
204
|
end
|
47
|
-
|
205
|
+
|
48
206
|
frame << mask + payload_len
|
49
207
|
frame.push *ex_payload_len if ex_payload_len
|
50
208
|
frame.push *msg.bytes
|
@@ -55,7 +213,9 @@ module DEBUGGER__
|
|
55
213
|
first_group = @sock.getbyte
|
56
214
|
fin = first_group & 0b10000000 != 128
|
57
215
|
raise 'Unsupported' if fin
|
216
|
+
|
58
217
|
opcode = first_group & 0b00001111
|
218
|
+
raise Detach if opcode == 8
|
59
219
|
raise "Unsupported: #{opcode}" unless opcode == 1
|
60
220
|
|
61
221
|
second_group = @sock.getbyte
|
@@ -80,34 +240,40 @@ module DEBUGGER__
|
|
80
240
|
|
81
241
|
def send_response req, **res
|
82
242
|
if res.empty?
|
83
|
-
@
|
243
|
+
@ws_server.send id: req['id'], result: {}
|
84
244
|
else
|
85
|
-
@
|
245
|
+
@ws_server.send id: req['id'], result: res
|
86
246
|
end
|
87
247
|
end
|
88
248
|
|
249
|
+
def send_fail_response req, **res
|
250
|
+
@ws_server.send id: req['id'], error: res
|
251
|
+
end
|
252
|
+
|
89
253
|
def send_event method, **params
|
90
254
|
if params.empty?
|
91
|
-
@
|
255
|
+
@ws_server.send method: method, params: {}
|
92
256
|
else
|
93
|
-
@
|
257
|
+
@ws_server.send method: method, params: params
|
94
258
|
end
|
95
259
|
end
|
96
260
|
|
261
|
+
INVALID_REQUEST = -32600
|
262
|
+
|
97
263
|
def process
|
98
|
-
bps =
|
264
|
+
bps = {}
|
99
265
|
@src_map = {}
|
100
266
|
loop do
|
101
|
-
req = @
|
267
|
+
req = @ws_server.extract_data
|
102
268
|
$stderr.puts '[>]' + req.inspect if SHOW_PROTOCOL
|
103
269
|
|
104
270
|
case req['method']
|
105
271
|
|
106
272
|
## boot/configuration
|
107
273
|
when 'Page.getResourceTree'
|
108
|
-
|
109
|
-
src = File.read(
|
110
|
-
@src_map[
|
274
|
+
path = File.absolute_path($0)
|
275
|
+
src = File.read(path)
|
276
|
+
@src_map[path] = src
|
111
277
|
send_response req,
|
112
278
|
frameTree: {
|
113
279
|
frame: {
|
@@ -120,8 +286,8 @@ module DEBUGGER__
|
|
120
286
|
]
|
121
287
|
}
|
122
288
|
send_event 'Debugger.scriptParsed',
|
123
|
-
scriptId:
|
124
|
-
url: "http://debuggee#{
|
289
|
+
scriptId: path,
|
290
|
+
url: "http://debuggee#{path}",
|
125
291
|
startLine: 0,
|
126
292
|
startColumn: 0,
|
127
293
|
endLine: src.count("\n"),
|
@@ -141,7 +307,7 @@ module DEBUGGER__
|
|
141
307
|
@q_msg << req
|
142
308
|
when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
|
143
309
|
'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
|
144
|
-
'
|
310
|
+
'Runtime.releaseObjectGroup', 'Runtime.discardConsoleEntries', 'Log.clear'
|
145
311
|
send_response req
|
146
312
|
|
147
313
|
## control
|
@@ -151,20 +317,52 @@ module DEBUGGER__
|
|
151
317
|
send_response req
|
152
318
|
send_event 'Debugger.resumed'
|
153
319
|
when 'Debugger.stepOver'
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
320
|
+
begin
|
321
|
+
@session.check_postmortem
|
322
|
+
@q_msg << 'n'
|
323
|
+
send_response req
|
324
|
+
send_event 'Debugger.resumed'
|
325
|
+
rescue PostmortemError
|
326
|
+
send_fail_response req,
|
327
|
+
code: INVALID_REQUEST,
|
328
|
+
message: "'stepOver' is not supported while postmortem mode"
|
329
|
+
ensure
|
330
|
+
@q_msg << req
|
331
|
+
end
|
158
332
|
when 'Debugger.stepInto'
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
333
|
+
begin
|
334
|
+
@session.check_postmortem
|
335
|
+
@q_msg << 's'
|
336
|
+
send_response req
|
337
|
+
send_event 'Debugger.resumed'
|
338
|
+
rescue PostmortemError
|
339
|
+
send_fail_response req,
|
340
|
+
code: INVALID_REQUEST,
|
341
|
+
message: "'stepInto' is not supported while postmortem mode"
|
342
|
+
ensure
|
343
|
+
@q_msg << req
|
344
|
+
end
|
163
345
|
when 'Debugger.stepOut'
|
164
|
-
|
165
|
-
|
346
|
+
begin
|
347
|
+
@session.check_postmortem
|
348
|
+
@q_msg << 'fin'
|
349
|
+
send_response req
|
350
|
+
send_event 'Debugger.resumed'
|
351
|
+
rescue PostmortemError
|
352
|
+
send_fail_response req,
|
353
|
+
code: INVALID_REQUEST,
|
354
|
+
message: "'stepOut' is not supported while postmortem mode"
|
355
|
+
ensure
|
356
|
+
@q_msg << req
|
357
|
+
end
|
358
|
+
when 'Debugger.setSkipAllPauses'
|
359
|
+
skip = req.dig('params', 'skip')
|
360
|
+
if skip
|
361
|
+
deactivate_bp
|
362
|
+
else
|
363
|
+
activate_bp bps
|
364
|
+
end
|
166
365
|
send_response req
|
167
|
-
send_event 'Debugger.resumed'
|
168
366
|
|
169
367
|
# breakpoint
|
170
368
|
when 'Debugger.getPossibleBreakpoints'
|
@@ -181,31 +379,72 @@ module DEBUGGER__
|
|
181
379
|
]
|
182
380
|
when 'Debugger.setBreakpointByUrl'
|
183
381
|
line = req.dig('params', 'lineNumber')
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
382
|
+
url = req.dig('params', 'url')
|
383
|
+
locations = []
|
384
|
+
if url.match /http:\/\/debuggee(.*)/
|
385
|
+
path = $1
|
386
|
+
cond = req.dig('params', 'condition')
|
387
|
+
src = get_source_code path
|
388
|
+
end_line = src.count("\n")
|
389
|
+
line = end_line if line > end_line
|
390
|
+
b_id = "1:#{line}:#{path}"
|
391
|
+
if cond != ''
|
392
|
+
SESSION.add_line_breakpoint(path, line + 1, cond: cond)
|
393
|
+
else
|
394
|
+
SESSION.add_line_breakpoint(path, line + 1)
|
395
|
+
end
|
396
|
+
bps[b_id] = bps.size
|
397
|
+
locations << {scriptId: path, lineNumber: line}
|
191
398
|
else
|
192
|
-
|
399
|
+
b_id = "1:#{line}:#{url}"
|
193
400
|
end
|
194
401
|
send_response req,
|
195
|
-
breakpointId:
|
196
|
-
locations:
|
197
|
-
scriptId: path,
|
198
|
-
lineNumber: line
|
199
|
-
]
|
402
|
+
breakpointId: b_id,
|
403
|
+
locations: locations
|
200
404
|
when 'Debugger.removeBreakpoint'
|
201
405
|
b_id = req.dig('params', 'breakpointId')
|
202
|
-
|
406
|
+
bps = del_bp bps, b_id
|
407
|
+
send_response req
|
408
|
+
when 'Debugger.setBreakpointsActive'
|
409
|
+
active = req.dig('params', 'active')
|
410
|
+
if active
|
411
|
+
activate_bp bps
|
412
|
+
else
|
413
|
+
deactivate_bp # TODO: Change this part because catch breakpoints should not be deactivated.
|
414
|
+
end
|
415
|
+
send_response req
|
416
|
+
when 'Debugger.setPauseOnExceptions'
|
417
|
+
state = req.dig('params', 'state')
|
418
|
+
ex = 'Exception'
|
419
|
+
case state
|
420
|
+
when 'none'
|
421
|
+
@q_msg << 'config postmortem = false'
|
422
|
+
bps = del_bp bps, ex
|
423
|
+
when 'uncaught'
|
424
|
+
@q_msg << 'config postmortem = true'
|
425
|
+
bps = del_bp bps, ex
|
426
|
+
when 'all'
|
427
|
+
@q_msg << 'config postmortem = false'
|
428
|
+
SESSION.add_catch_breakpoint ex
|
429
|
+
bps[ex] = bps.size
|
430
|
+
end
|
203
431
|
send_response req
|
204
432
|
|
205
433
|
when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
|
206
434
|
@q_msg << req
|
207
435
|
end
|
208
436
|
end
|
437
|
+
rescue Detach
|
438
|
+
@q_msg << 'continue'
|
439
|
+
end
|
440
|
+
|
441
|
+
def del_bp bps, k
|
442
|
+
return bps unless idx = bps[k]
|
443
|
+
|
444
|
+
bps.delete k
|
445
|
+
bps.each_key{|i| bps[i] -= 1 if bps[i] > idx}
|
446
|
+
@q_msg << "del #{idx}"
|
447
|
+
bps
|
209
448
|
end
|
210
449
|
|
211
450
|
def get_source_code path
|
@@ -216,9 +455,32 @@ module DEBUGGER__
|
|
216
455
|
src
|
217
456
|
end
|
218
457
|
|
458
|
+
def activate_bp bps
|
459
|
+
bps.each_key{|k|
|
460
|
+
if k.match /^\d+:(\d+):(.*)/
|
461
|
+
line = $1
|
462
|
+
path = $2
|
463
|
+
SESSION.add_line_breakpoint(path, line.to_i + 1)
|
464
|
+
else
|
465
|
+
SESSION.add_catch_breakpoint 'Exception'
|
466
|
+
end
|
467
|
+
}
|
468
|
+
end
|
469
|
+
|
470
|
+
def deactivate_bp
|
471
|
+
@q_msg << 'del'
|
472
|
+
@q_ans << 'y'
|
473
|
+
end
|
474
|
+
|
475
|
+
def cleanup_reader
|
476
|
+
Process.kill :KILL, @chrome_pid if @chrome_pid
|
477
|
+
end
|
478
|
+
|
219
479
|
## Called by the SESSION thread
|
220
480
|
|
221
481
|
def readline prompt
|
482
|
+
return 'c' unless @q_msg
|
483
|
+
|
222
484
|
@q_msg.pop || 'kill!'
|
223
485
|
end
|
224
486
|
|
@@ -226,6 +488,10 @@ module DEBUGGER__
|
|
226
488
|
send_response req, **result
|
227
489
|
end
|
228
490
|
|
491
|
+
def respond_fail req, **result
|
492
|
+
send_fail_response req, **result
|
493
|
+
end
|
494
|
+
|
229
495
|
def fire_event event, **result
|
230
496
|
if result.empty?
|
231
497
|
send_event event
|
@@ -245,26 +511,48 @@ module DEBUGGER__
|
|
245
511
|
end
|
246
512
|
|
247
513
|
class Session
|
514
|
+
def fail_response req, **result
|
515
|
+
@ui.respond_fail req, result
|
516
|
+
return :retry
|
517
|
+
end
|
518
|
+
|
519
|
+
INVALID_PARAMS = -32602
|
520
|
+
|
248
521
|
def process_protocol_request req
|
249
522
|
case req['method']
|
250
523
|
when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource'
|
251
524
|
@tc << [:cdp, :backtrace, req]
|
252
525
|
when 'Debugger.evaluateOnCallFrame'
|
253
|
-
|
254
|
-
|
526
|
+
frame_id = req.dig('params', 'callFrameId')
|
527
|
+
if fid = @frame_map[frame_id]
|
528
|
+
expr = req.dig('params', 'expression')
|
529
|
+
@tc << [:cdp, :evaluate, req, fid, expr]
|
530
|
+
else
|
531
|
+
fail_response req,
|
532
|
+
code: INVALID_PARAMS,
|
533
|
+
message: "'callFrameId' is an invalid"
|
534
|
+
end
|
255
535
|
when 'Runtime.getProperties'
|
256
536
|
oid = req.dig('params', 'objectId')
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
537
|
+
if ref = @obj_map[oid]
|
538
|
+
case ref[0]
|
539
|
+
when 'local'
|
540
|
+
frame_id = ref[1]
|
541
|
+
fid = @frame_map[frame_id]
|
542
|
+
@tc << [:cdp, :scope, req, fid]
|
543
|
+
when 'properties'
|
544
|
+
@tc << [:cdp, :properties, req, oid]
|
545
|
+
when 'script', 'global'
|
546
|
+
# TODO: Support script and global types
|
547
|
+
@ui.respond req
|
548
|
+
return :retry
|
549
|
+
else
|
550
|
+
raise "Unknown type: #{ref.inspect}"
|
551
|
+
end
|
552
|
+
else
|
553
|
+
fail_response req,
|
554
|
+
code: INVALID_PARAMS,
|
555
|
+
message: "'objectId' is an invalid"
|
268
556
|
end
|
269
557
|
end
|
270
558
|
end
|
@@ -274,7 +562,9 @@ module DEBUGGER__
|
|
274
562
|
|
275
563
|
case type
|
276
564
|
when :backtrace
|
277
|
-
result[:callFrames].each do |frame|
|
565
|
+
result[:callFrames].each.with_index do |frame, i|
|
566
|
+
frame_id = frame[:callFrameId]
|
567
|
+
@frame_map[frame_id] = i
|
278
568
|
s_id = frame.dig(:location, :scriptId)
|
279
569
|
if File.exist?(s_id) && !@script_paths.include?(s_id)
|
280
570
|
src = File.read(s_id)
|
@@ -289,13 +579,60 @@ module DEBUGGER__
|
|
289
579
|
hash: src.hash
|
290
580
|
@script_paths << s_id
|
291
581
|
end
|
582
|
+
|
583
|
+
frame[:scopeChain].each {|s|
|
584
|
+
oid = s.dig(:object, :objectId)
|
585
|
+
@obj_map[oid] = [s[:type], frame_id]
|
586
|
+
}
|
587
|
+
end
|
588
|
+
|
589
|
+
if oid = result.dig(:data, :objectId)
|
590
|
+
@obj_map[oid] = ['properties']
|
292
591
|
end
|
293
|
-
result[:reason] = 'other'
|
294
592
|
@ui.fire_event 'Debugger.paused', **result
|
295
593
|
when :evaluate
|
594
|
+
message = result.delete :message
|
595
|
+
if message
|
596
|
+
fail_response req,
|
597
|
+
code: INVALID_PARAMS,
|
598
|
+
message: message
|
599
|
+
else
|
600
|
+
rs = result.dig(:response, :result)
|
601
|
+
[rs].each{|obj|
|
602
|
+
if oid = obj[:objectId]
|
603
|
+
@obj_map[oid] = ['properties']
|
604
|
+
end
|
605
|
+
}
|
606
|
+
@ui.respond req, **result[:response]
|
607
|
+
|
608
|
+
out = result[:output]
|
609
|
+
if out && !out.empty?
|
610
|
+
@ui.fire_event 'Runtime.consoleAPICalled',
|
611
|
+
type: 'log',
|
612
|
+
args: [
|
613
|
+
type: out.class,
|
614
|
+
value: out
|
615
|
+
],
|
616
|
+
executionContextId: 1, # Change this number if something goes wrong.
|
617
|
+
timestamp: Time.now.to_f
|
618
|
+
end
|
619
|
+
end
|
620
|
+
when :scope
|
621
|
+
result.each{|obj|
|
622
|
+
if oid = obj.dig(:value, :objectId)
|
623
|
+
@obj_map[oid] = ['properties']
|
624
|
+
end
|
625
|
+
}
|
296
626
|
@ui.respond req, result: result
|
297
627
|
when :properties
|
298
|
-
|
628
|
+
result.each_value{|v|
|
629
|
+
v.each{|obj|
|
630
|
+
if oid = obj.dig(:value, :objectId)
|
631
|
+
@obj_map[oid] = ['properties']
|
632
|
+
end
|
633
|
+
}
|
634
|
+
}
|
635
|
+
@ui.respond req, **result
|
299
636
|
end
|
300
637
|
end
|
301
638
|
end
|
@@ -307,43 +644,49 @@ module DEBUGGER__
|
|
307
644
|
|
308
645
|
case type
|
309
646
|
when :backtrace
|
310
|
-
|
647
|
+
exception = nil
|
648
|
+
result = {
|
649
|
+
reason: 'other',
|
311
650
|
callFrames: @target_frames.map.with_index{|frame, i|
|
651
|
+
exception = frame.raised_exception if frame == current_frame && frame.has_raised_exception
|
652
|
+
|
312
653
|
path = frame.realpath || frame.path
|
313
654
|
if path.match /<internal:(.*)>/
|
314
|
-
|
315
|
-
else
|
316
|
-
abs = path
|
655
|
+
path = $1
|
317
656
|
end
|
318
657
|
|
319
|
-
|
658
|
+
{
|
320
659
|
callFrameId: SecureRandom.hex(16),
|
321
660
|
functionName: frame.name,
|
661
|
+
functionLocation: {
|
662
|
+
scriptId: path,
|
663
|
+
lineNumber: 0
|
664
|
+
},
|
322
665
|
location: {
|
323
|
-
scriptId:
|
666
|
+
scriptId: path,
|
324
667
|
lineNumber: frame.location.lineno - 1 # The line number is 0-based.
|
325
668
|
},
|
326
|
-
url: "http://debuggee#{
|
669
|
+
url: "http://debuggee#{path}",
|
327
670
|
scopeChain: [
|
328
671
|
{
|
329
672
|
type: 'local',
|
330
673
|
object: {
|
331
674
|
type: 'object',
|
332
|
-
objectId:
|
675
|
+
objectId: rand.to_s
|
333
676
|
}
|
334
677
|
},
|
335
678
|
{
|
336
679
|
type: 'script',
|
337
680
|
object: {
|
338
681
|
type: 'object',
|
339
|
-
objectId:
|
682
|
+
objectId: rand.to_s
|
340
683
|
}
|
341
684
|
},
|
342
685
|
{
|
343
686
|
type: 'global',
|
344
687
|
object: {
|
345
688
|
type: 'object',
|
346
|
-
objectId:
|
689
|
+
objectId: rand.to_s
|
347
690
|
}
|
348
691
|
}
|
349
692
|
],
|
@@ -353,15 +696,83 @@ module DEBUGGER__
|
|
353
696
|
}
|
354
697
|
}
|
355
698
|
}
|
699
|
+
|
700
|
+
if exception
|
701
|
+
result[:data] = evaluate_result exception
|
702
|
+
result[:reason] = 'exception'
|
703
|
+
end
|
704
|
+
event! :cdp_result, :backtrace, req, result
|
356
705
|
when :evaluate
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
706
|
+
res = {}
|
707
|
+
fid, expr = args
|
708
|
+
frame = @target_frames[fid]
|
709
|
+
message = nil
|
710
|
+
|
711
|
+
if frame && (b = frame.binding)
|
712
|
+
b = b.dup
|
713
|
+
special_local_variables current_frame do |name, var|
|
714
|
+
b.local_variable_set(name, var) if /\%/ !~name
|
715
|
+
end
|
716
|
+
|
717
|
+
result = nil
|
718
|
+
|
719
|
+
case req.dig('params', 'objectGroup')
|
720
|
+
when 'popover'
|
721
|
+
case expr
|
722
|
+
# Chrome doesn't read instance variables
|
723
|
+
when /\A\$\S/
|
724
|
+
global_variables.each{|gvar|
|
725
|
+
if gvar.to_s == expr
|
726
|
+
result = eval(gvar.to_s)
|
727
|
+
break false
|
728
|
+
end
|
729
|
+
} and (message = "Error: Not defined global variable: #{expr.inspect}")
|
730
|
+
when /(\A[A-Z][a-zA-Z]*)/
|
731
|
+
unless result = search_const(b, $1)
|
732
|
+
message = "Error: Not defined constant: #{expr.inspect}"
|
733
|
+
end
|
734
|
+
else
|
735
|
+
begin
|
736
|
+
# try to check local variables
|
737
|
+
b.local_variable_defined?(expr) or raise NameError
|
738
|
+
result = b.local_variable_get(expr)
|
739
|
+
rescue NameError
|
740
|
+
# try to check method
|
741
|
+
if b.receiver.respond_to? expr, include_all: true
|
742
|
+
result = b.receiver.method(expr)
|
743
|
+
else
|
744
|
+
message = "Error: Can not evaluate: #{expr.inspect}"
|
745
|
+
end
|
746
|
+
end
|
747
|
+
end
|
748
|
+
else
|
749
|
+
begin
|
750
|
+
orig_stdout = $stdout
|
751
|
+
$stdout = StringIO.new
|
752
|
+
result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
|
753
|
+
rescue Exception => e
|
754
|
+
result = e
|
755
|
+
b = result.backtrace.map{|e| " #{e}\n"}
|
756
|
+
line = b.first.match('.*:(\d+):in .*')[1].to_i
|
757
|
+
res[:exceptionDetails] = {
|
758
|
+
exceptionId: 1,
|
759
|
+
text: 'Uncaught',
|
760
|
+
lineNumber: line - 1,
|
761
|
+
columnNumber: 0,
|
762
|
+
exception: evaluate_result(result),
|
763
|
+
}
|
764
|
+
ensure
|
765
|
+
output = $stdout.string
|
766
|
+
$stdout = orig_stdout
|
767
|
+
end
|
768
|
+
end
|
769
|
+
else
|
770
|
+
result = Exception.new("Error: Can not evaluate on this frame")
|
362
771
|
end
|
363
|
-
|
364
|
-
|
772
|
+
|
773
|
+
res[:result] = evaluate_result(result)
|
774
|
+
event! :cdp_result, :evaluate, req, message: message, response: res, output: output
|
775
|
+
when :scope
|
365
776
|
fid = args.shift
|
366
777
|
frame = @target_frames[fid]
|
367
778
|
if b = frame.binding
|
@@ -369,8 +780,9 @@ module DEBUGGER__
|
|
369
780
|
v = b.local_variable_get(name)
|
370
781
|
variable(name, v)
|
371
782
|
}
|
372
|
-
|
373
|
-
|
783
|
+
special_local_variables frame do |name, val|
|
784
|
+
vars.unshift variable(name, val)
|
785
|
+
end
|
374
786
|
vars.unshift variable('%self', b.receiver)
|
375
787
|
elsif lvars = frame.local_variables
|
376
788
|
vars = lvars.map{|var, val|
|
@@ -378,46 +790,131 @@ module DEBUGGER__
|
|
378
790
|
}
|
379
791
|
else
|
380
792
|
vars = [variable('%self', frame.self)]
|
381
|
-
|
382
|
-
|
793
|
+
special_local_variables frame do |name, val|
|
794
|
+
vars.unshift variable(name, val)
|
795
|
+
end
|
796
|
+
end
|
797
|
+
event! :cdp_result, :scope, req, vars
|
798
|
+
when :properties
|
799
|
+
oid = args.shift
|
800
|
+
result = []
|
801
|
+
prop = []
|
802
|
+
|
803
|
+
if obj = @obj_map[oid]
|
804
|
+
case obj
|
805
|
+
when Array
|
806
|
+
result = obj.map.with_index{|o, i|
|
807
|
+
variable i.to_s, o
|
808
|
+
}
|
809
|
+
when Hash
|
810
|
+
result = obj.map{|k, v|
|
811
|
+
variable(k, v)
|
812
|
+
}
|
813
|
+
when Struct
|
814
|
+
result = obj.members.map{|m|
|
815
|
+
variable(m, obj[m])
|
816
|
+
}
|
817
|
+
when String
|
818
|
+
prop = [
|
819
|
+
property('#length', obj.length),
|
820
|
+
property('#encoding', obj.encoding)
|
821
|
+
]
|
822
|
+
when Class, Module
|
823
|
+
result = obj.instance_variables.map{|iv|
|
824
|
+
variable(iv, obj.instance_variable_get(iv))
|
825
|
+
}
|
826
|
+
prop = [property('%ancestors', obj.ancestors[1..])]
|
827
|
+
when Range
|
828
|
+
prop = [
|
829
|
+
property('#begin', obj.begin),
|
830
|
+
property('#end', obj.end),
|
831
|
+
]
|
832
|
+
end
|
833
|
+
|
834
|
+
result += obj.instance_variables.map{|iv|
|
835
|
+
variable(iv, obj.instance_variable_get(iv))
|
836
|
+
}
|
837
|
+
prop += [property('#class', obj.class)]
|
383
838
|
end
|
384
|
-
event! :cdp_result, :properties, req,
|
839
|
+
event! :cdp_result, :properties, req, result: result, internalProperties: prop
|
385
840
|
end
|
386
841
|
end
|
387
842
|
|
843
|
+
def search_const b, expr
|
844
|
+
cs = expr.split('::')
|
845
|
+
[Object, *b.eval('Module.nesting')].reverse_each{|mod|
|
846
|
+
if cs.all?{|c|
|
847
|
+
if mod.const_defined?(c)
|
848
|
+
mod = mod.const_get(c)
|
849
|
+
else
|
850
|
+
false
|
851
|
+
end
|
852
|
+
}
|
853
|
+
# if-body
|
854
|
+
return mod
|
855
|
+
end
|
856
|
+
}
|
857
|
+
false
|
858
|
+
end
|
859
|
+
|
388
860
|
def evaluate_result r
|
389
861
|
v = variable nil, r
|
390
862
|
v[:value]
|
391
863
|
end
|
392
864
|
|
393
|
-
def
|
394
|
-
|
865
|
+
def property name, obj
|
866
|
+
v = variable name, obj
|
867
|
+
v.delete :configurable
|
868
|
+
v.delete :enumerable
|
869
|
+
v
|
870
|
+
end
|
871
|
+
|
872
|
+
def variable_ name, obj, type, description: obj.inspect, subtype: nil
|
873
|
+
oid = rand.to_s
|
874
|
+
@obj_map[oid] = obj
|
875
|
+
prop = {
|
395
876
|
name: name,
|
396
877
|
value: {
|
397
878
|
type: type,
|
398
|
-
|
879
|
+
description: description,
|
880
|
+
value: obj,
|
881
|
+
objectId: oid
|
399
882
|
},
|
400
|
-
configurable: true,
|
401
|
-
enumerable: true
|
883
|
+
configurable: true, # TODO: Change these parts because
|
884
|
+
enumerable: true # they are not necessarily `true`.
|
402
885
|
}
|
886
|
+
|
887
|
+
if type == 'object'
|
888
|
+
v = prop[:value]
|
889
|
+
v.delete :value
|
890
|
+
v[:subtype] = subtype if subtype
|
891
|
+
v[:className] = obj.class
|
892
|
+
end
|
893
|
+
prop
|
403
894
|
end
|
404
895
|
|
405
896
|
def variable name, obj
|
406
897
|
case obj
|
407
|
-
when Array
|
408
|
-
variable_ name, obj, 'object'
|
898
|
+
when Array
|
899
|
+
variable_ name, obj, 'object', description: "Array(#{obj.size})", subtype: 'array'
|
900
|
+
when Hash
|
901
|
+
variable_ name, obj, 'object', description: "Hash(#{obj.size})", subtype: 'map'
|
409
902
|
when String
|
410
|
-
variable_ name, obj, 'string',
|
411
|
-
when Class, Module, Struct
|
412
|
-
variable_ name, obj, '
|
903
|
+
variable_ name, obj, 'string', description: obj
|
904
|
+
when Class, Module, Struct, Range, Time, Method
|
905
|
+
variable_ name, obj, 'object'
|
413
906
|
when TrueClass, FalseClass
|
414
907
|
variable_ name, obj, 'boolean'
|
415
908
|
when Symbol
|
416
909
|
variable_ name, obj, 'symbol'
|
417
|
-
when Float
|
418
|
-
variable_ name, obj, 'number'
|
419
|
-
when Integer
|
910
|
+
when Integer, Float
|
420
911
|
variable_ name, obj, 'number'
|
912
|
+
when Exception
|
913
|
+
bt = nil
|
914
|
+
if log = obj.backtrace
|
915
|
+
bt = log.map{|e| " #{e}\n"}.join
|
916
|
+
end
|
917
|
+
variable_ name, obj, 'object', description: "#{obj.inspect}\n#{bt}", subtype: 'error'
|
421
918
|
else
|
422
919
|
variable_ name, obj, 'undefined'
|
423
920
|
end
|