debug 1.3.1 → 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 +43 -12
- data/README.md +38 -11
- data/bin/gentest +12 -4
- data/ext/debug/debug.c +5 -2
- data/lib/debug/breakpoint.rb +61 -11
- data/lib/debug/client.rb +57 -16
- data/lib/debug/color.rb +30 -20
- 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 +47 -77
- data/lib/debug/server_cdp.rb +605 -94
- data/lib/debug/server_dap.rb +256 -57
- data/lib/debug/session.rb +146 -54
- 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 +16 -8
- metadata +3 -2
data/lib/debug/server_cdp.rb
CHANGED
|
@@ -4,22 +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
|
|
95
|
+
def initialize s
|
|
96
|
+
@sock = s
|
|
97
|
+
end
|
|
98
|
+
|
|
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
|
|
13
171
|
def initialize s
|
|
14
172
|
@sock = s
|
|
15
173
|
end
|
|
16
174
|
|
|
17
175
|
def handshake
|
|
18
|
-
CONFIG.set_config no_color: true
|
|
19
|
-
|
|
20
176
|
req = @sock.readpartial 4096
|
|
21
177
|
$stderr.puts '[>]' + req if SHOW_PROTOCOL
|
|
22
|
-
|
|
178
|
+
|
|
23
179
|
if req.match /^Sec-WebSocket-Key: (.*)\r\n/
|
|
24
180
|
accept = Base64.strict_encode64 Digest::SHA1.digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
25
181
|
@sock.print "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
|
|
@@ -34,7 +190,7 @@ module DEBUGGER__
|
|
|
34
190
|
fin = 0b10000000
|
|
35
191
|
opcode = 0b00000001
|
|
36
192
|
frame << fin + opcode
|
|
37
|
-
|
|
193
|
+
|
|
38
194
|
mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
|
|
39
195
|
bytesize = msg.bytesize
|
|
40
196
|
if bytesize < 126
|
|
@@ -46,7 +202,7 @@ module DEBUGGER__
|
|
|
46
202
|
payload_len = 0b01111111
|
|
47
203
|
ex_payload_len = [bytesize].pack('Q>').bytes
|
|
48
204
|
end
|
|
49
|
-
|
|
205
|
+
|
|
50
206
|
frame << mask + payload_len
|
|
51
207
|
frame.push *ex_payload_len if ex_payload_len
|
|
52
208
|
frame.push *msg.bytes
|
|
@@ -57,7 +213,9 @@ module DEBUGGER__
|
|
|
57
213
|
first_group = @sock.getbyte
|
|
58
214
|
fin = first_group & 0b10000000 != 128
|
|
59
215
|
raise 'Unsupported' if fin
|
|
216
|
+
|
|
60
217
|
opcode = first_group & 0b00001111
|
|
218
|
+
raise Detach if opcode == 8
|
|
61
219
|
raise "Unsupported: #{opcode}" unless opcode == 1
|
|
62
220
|
|
|
63
221
|
second_group = @sock.getbyte
|
|
@@ -82,32 +240,40 @@ module DEBUGGER__
|
|
|
82
240
|
|
|
83
241
|
def send_response req, **res
|
|
84
242
|
if res.empty?
|
|
85
|
-
@
|
|
243
|
+
@ws_server.send id: req['id'], result: {}
|
|
86
244
|
else
|
|
87
|
-
@
|
|
245
|
+
@ws_server.send id: req['id'], result: res
|
|
88
246
|
end
|
|
89
247
|
end
|
|
90
248
|
|
|
249
|
+
def send_fail_response req, **res
|
|
250
|
+
@ws_server.send id: req['id'], error: res
|
|
251
|
+
end
|
|
252
|
+
|
|
91
253
|
def send_event method, **params
|
|
92
254
|
if params.empty?
|
|
93
|
-
@
|
|
255
|
+
@ws_server.send method: method, params: {}
|
|
94
256
|
else
|
|
95
|
-
@
|
|
257
|
+
@ws_server.send method: method, params: params
|
|
96
258
|
end
|
|
97
259
|
end
|
|
98
260
|
|
|
261
|
+
INVALID_REQUEST = -32600
|
|
262
|
+
|
|
99
263
|
def process
|
|
264
|
+
bps = {}
|
|
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
|
-
bps = []
|
|
104
269
|
|
|
105
270
|
case req['method']
|
|
106
271
|
|
|
107
272
|
## boot/configuration
|
|
108
273
|
when 'Page.getResourceTree'
|
|
109
|
-
|
|
110
|
-
src = File.read(
|
|
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,11 +286,11 @@ 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
|
-
endLine: src.count(
|
|
293
|
+
endLine: src.count("\n"),
|
|
128
294
|
endColumn: 0,
|
|
129
295
|
executionContextId: 1,
|
|
130
296
|
hash: src.hash
|
|
@@ -136,12 +302,12 @@ module DEBUGGER__
|
|
|
136
302
|
}
|
|
137
303
|
when 'Debugger.getScriptSource'
|
|
138
304
|
s_id = req.dig('params', 'scriptId')
|
|
139
|
-
src =
|
|
305
|
+
src = get_source_code s_id
|
|
140
306
|
send_response req, scriptSource: src
|
|
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,25 +317,60 @@ 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'
|
|
171
369
|
s_id = req.dig('params', 'start', 'scriptId')
|
|
172
|
-
line = req.dig('params', 'lineNumber')
|
|
370
|
+
line = req.dig('params', 'start', 'lineNumber')
|
|
371
|
+
src = get_source_code s_id
|
|
372
|
+
end_line = src.count("\n")
|
|
373
|
+
line = end_line if line > end_line
|
|
173
374
|
send_response req,
|
|
174
375
|
locations: [
|
|
175
376
|
{ scriptId: s_id,
|
|
@@ -178,33 +379,108 @@ module DEBUGGER__
|
|
|
178
379
|
]
|
|
179
380
|
when 'Debugger.setBreakpointByUrl'
|
|
180
381
|
line = req.dig('params', 'lineNumber')
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if
|
|
184
|
-
|
|
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}
|
|
185
398
|
else
|
|
186
|
-
|
|
399
|
+
b_id = "1:#{line}:#{url}"
|
|
187
400
|
end
|
|
188
401
|
send_response req,
|
|
189
|
-
breakpointId:
|
|
190
|
-
locations:
|
|
191
|
-
scriptId: path,
|
|
192
|
-
lineNumber: line
|
|
193
|
-
]
|
|
402
|
+
breakpointId: b_id,
|
|
403
|
+
locations: locations
|
|
194
404
|
when 'Debugger.removeBreakpoint'
|
|
195
405
|
b_id = req.dig('params', 'breakpointId')
|
|
196
|
-
|
|
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
|
|
197
431
|
send_response req
|
|
198
432
|
|
|
199
433
|
when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
|
|
200
434
|
@q_msg << req
|
|
201
435
|
end
|
|
202
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
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def get_source_code path
|
|
451
|
+
return @src_map[path] if @src_map[path]
|
|
452
|
+
|
|
453
|
+
src = File.read(path)
|
|
454
|
+
@src_map[path] = src
|
|
455
|
+
src
|
|
456
|
+
end
|
|
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
|
|
203
477
|
end
|
|
204
478
|
|
|
205
479
|
## Called by the SESSION thread
|
|
206
480
|
|
|
207
481
|
def readline prompt
|
|
482
|
+
return 'c' unless @q_msg
|
|
483
|
+
|
|
208
484
|
@q_msg.pop || 'kill!'
|
|
209
485
|
end
|
|
210
486
|
|
|
@@ -212,6 +488,10 @@ module DEBUGGER__
|
|
|
212
488
|
send_response req, **result
|
|
213
489
|
end
|
|
214
490
|
|
|
491
|
+
def respond_fail req, **result
|
|
492
|
+
send_fail_response req, **result
|
|
493
|
+
end
|
|
494
|
+
|
|
215
495
|
def fire_event event, **result
|
|
216
496
|
if result.empty?
|
|
217
497
|
send_event event
|
|
@@ -231,26 +511,48 @@ module DEBUGGER__
|
|
|
231
511
|
end
|
|
232
512
|
|
|
233
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
|
+
|
|
234
521
|
def process_protocol_request req
|
|
235
522
|
case req['method']
|
|
236
523
|
when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource'
|
|
237
524
|
@tc << [:cdp, :backtrace, req]
|
|
238
525
|
when 'Debugger.evaluateOnCallFrame'
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
241
535
|
when 'Runtime.getProperties'
|
|
242
536
|
oid = req.dig('params', 'objectId')
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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"
|
|
254
556
|
end
|
|
255
557
|
end
|
|
256
558
|
end
|
|
@@ -260,7 +562,9 @@ module DEBUGGER__
|
|
|
260
562
|
|
|
261
563
|
case type
|
|
262
564
|
when :backtrace
|
|
263
|
-
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
|
|
264
568
|
s_id = frame.dig(:location, :scriptId)
|
|
265
569
|
if File.exist?(s_id) && !@script_paths.include?(s_id)
|
|
266
570
|
src = File.read(s_id)
|
|
@@ -269,19 +573,66 @@ module DEBUGGER__
|
|
|
269
573
|
url: frame[:url],
|
|
270
574
|
startLine: 0,
|
|
271
575
|
startColumn: 0,
|
|
272
|
-
endLine: src.count(
|
|
576
|
+
endLine: src.count("\n"),
|
|
273
577
|
endColumn: 0,
|
|
274
578
|
executionContextId: @script_paths.size + 1,
|
|
275
579
|
hash: src.hash
|
|
276
580
|
@script_paths << s_id
|
|
277
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']
|
|
278
591
|
end
|
|
279
|
-
result[:reason] = 'other'
|
|
280
592
|
@ui.fire_event 'Debugger.paused', **result
|
|
281
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
|
+
}
|
|
282
626
|
@ui.respond req, result: result
|
|
283
627
|
when :properties
|
|
284
|
-
|
|
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
|
|
285
636
|
end
|
|
286
637
|
end
|
|
287
638
|
end
|
|
@@ -293,43 +644,49 @@ module DEBUGGER__
|
|
|
293
644
|
|
|
294
645
|
case type
|
|
295
646
|
when :backtrace
|
|
296
|
-
|
|
647
|
+
exception = nil
|
|
648
|
+
result = {
|
|
649
|
+
reason: 'other',
|
|
297
650
|
callFrames: @target_frames.map.with_index{|frame, i|
|
|
651
|
+
exception = frame.raised_exception if frame == current_frame && frame.has_raised_exception
|
|
652
|
+
|
|
298
653
|
path = frame.realpath || frame.path
|
|
299
654
|
if path.match /<internal:(.*)>/
|
|
300
|
-
|
|
301
|
-
else
|
|
302
|
-
abs = path
|
|
655
|
+
path = $1
|
|
303
656
|
end
|
|
304
657
|
|
|
305
|
-
|
|
658
|
+
{
|
|
306
659
|
callFrameId: SecureRandom.hex(16),
|
|
307
660
|
functionName: frame.name,
|
|
661
|
+
functionLocation: {
|
|
662
|
+
scriptId: path,
|
|
663
|
+
lineNumber: 0
|
|
664
|
+
},
|
|
308
665
|
location: {
|
|
309
|
-
scriptId:
|
|
666
|
+
scriptId: path,
|
|
310
667
|
lineNumber: frame.location.lineno - 1 # The line number is 0-based.
|
|
311
668
|
},
|
|
312
|
-
url: "http://debuggee#{
|
|
669
|
+
url: "http://debuggee#{path}",
|
|
313
670
|
scopeChain: [
|
|
314
671
|
{
|
|
315
672
|
type: 'local',
|
|
316
673
|
object: {
|
|
317
674
|
type: 'object',
|
|
318
|
-
objectId:
|
|
675
|
+
objectId: rand.to_s
|
|
319
676
|
}
|
|
320
677
|
},
|
|
321
678
|
{
|
|
322
679
|
type: 'script',
|
|
323
680
|
object: {
|
|
324
681
|
type: 'object',
|
|
325
|
-
objectId:
|
|
682
|
+
objectId: rand.to_s
|
|
326
683
|
}
|
|
327
684
|
},
|
|
328
685
|
{
|
|
329
686
|
type: 'global',
|
|
330
687
|
object: {
|
|
331
688
|
type: 'object',
|
|
332
|
-
objectId:
|
|
689
|
+
objectId: rand.to_s
|
|
333
690
|
}
|
|
334
691
|
}
|
|
335
692
|
],
|
|
@@ -339,15 +696,83 @@ module DEBUGGER__
|
|
|
339
696
|
}
|
|
340
697
|
}
|
|
341
698
|
}
|
|
699
|
+
|
|
700
|
+
if exception
|
|
701
|
+
result[:data] = evaluate_result exception
|
|
702
|
+
result[:reason] = 'exception'
|
|
703
|
+
end
|
|
704
|
+
event! :cdp_result, :backtrace, req, result
|
|
342
705
|
when :evaluate
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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")
|
|
348
771
|
end
|
|
349
|
-
|
|
350
|
-
|
|
772
|
+
|
|
773
|
+
res[:result] = evaluate_result(result)
|
|
774
|
+
event! :cdp_result, :evaluate, req, message: message, response: res, output: output
|
|
775
|
+
when :scope
|
|
351
776
|
fid = args.shift
|
|
352
777
|
frame = @target_frames[fid]
|
|
353
778
|
if b = frame.binding
|
|
@@ -355,8 +780,9 @@ module DEBUGGER__
|
|
|
355
780
|
v = b.local_variable_get(name)
|
|
356
781
|
variable(name, v)
|
|
357
782
|
}
|
|
358
|
-
|
|
359
|
-
|
|
783
|
+
special_local_variables frame do |name, val|
|
|
784
|
+
vars.unshift variable(name, val)
|
|
785
|
+
end
|
|
360
786
|
vars.unshift variable('%self', b.receiver)
|
|
361
787
|
elsif lvars = frame.local_variables
|
|
362
788
|
vars = lvars.map{|var, val|
|
|
@@ -364,46 +790,131 @@ module DEBUGGER__
|
|
|
364
790
|
}
|
|
365
791
|
else
|
|
366
792
|
vars = [variable('%self', frame.self)]
|
|
367
|
-
|
|
368
|
-
|
|
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)]
|
|
369
838
|
end
|
|
370
|
-
event! :cdp_result, :properties, req,
|
|
839
|
+
event! :cdp_result, :properties, req, result: result, internalProperties: prop
|
|
371
840
|
end
|
|
372
841
|
end
|
|
373
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
|
+
|
|
374
860
|
def evaluate_result r
|
|
375
861
|
v = variable nil, r
|
|
376
862
|
v[:value]
|
|
377
863
|
end
|
|
378
864
|
|
|
379
|
-
def
|
|
380
|
-
|
|
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 = {
|
|
381
876
|
name: name,
|
|
382
877
|
value: {
|
|
383
878
|
type: type,
|
|
384
|
-
|
|
879
|
+
description: description,
|
|
880
|
+
value: obj,
|
|
881
|
+
objectId: oid
|
|
385
882
|
},
|
|
386
|
-
configurable: true,
|
|
387
|
-
enumerable: true
|
|
883
|
+
configurable: true, # TODO: Change these parts because
|
|
884
|
+
enumerable: true # they are not necessarily `true`.
|
|
388
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
|
|
389
894
|
end
|
|
390
895
|
|
|
391
896
|
def variable name, obj
|
|
392
897
|
case obj
|
|
393
|
-
when Array
|
|
394
|
-
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'
|
|
395
902
|
when String
|
|
396
|
-
variable_ name, obj, 'string',
|
|
397
|
-
when Class, Module, Struct
|
|
398
|
-
variable_ name, obj, '
|
|
903
|
+
variable_ name, obj, 'string', description: obj
|
|
904
|
+
when Class, Module, Struct, Range, Time, Method
|
|
905
|
+
variable_ name, obj, 'object'
|
|
399
906
|
when TrueClass, FalseClass
|
|
400
907
|
variable_ name, obj, 'boolean'
|
|
401
908
|
when Symbol
|
|
402
909
|
variable_ name, obj, 'symbol'
|
|
403
|
-
when Float
|
|
404
|
-
variable_ name, obj, 'number'
|
|
405
|
-
when Integer
|
|
910
|
+
when Integer, Float
|
|
406
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'
|
|
407
918
|
else
|
|
408
919
|
variable_ name, obj, 'undefined'
|
|
409
920
|
end
|