debug 1.3.4 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|