ed-precompiled_debug 1.11.0-arm64-darwin
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 +7 -0
- data/CONTRIBUTING.md +573 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +996 -0
- data/Rakefile +57 -0
- data/TODO.md +23 -0
- data/debug.gemspec +33 -0
- data/exe/rdbg +53 -0
- data/ext/debug/debug.c +228 -0
- data/ext/debug/extconf.rb +27 -0
- data/ext/debug/iseq_collector.c +93 -0
- data/lib/debug/3.0/debug.bundle +0 -0
- data/lib/debug/3.1/debug.bundle +0 -0
- data/lib/debug/3.2/debug.bundle +0 -0
- data/lib/debug/3.3/debug.bundle +0 -0
- data/lib/debug/3.4/debug.bundle +0 -0
- data/lib/debug/abbrev_command.rb +77 -0
- data/lib/debug/breakpoint.rb +556 -0
- data/lib/debug/client.rb +263 -0
- data/lib/debug/color.rb +123 -0
- data/lib/debug/config.rb +592 -0
- data/lib/debug/console.rb +224 -0
- data/lib/debug/dap_custom/traceInspector.rb +336 -0
- data/lib/debug/frame_info.rb +191 -0
- data/lib/debug/irb_integration.rb +37 -0
- data/lib/debug/local.rb +115 -0
- data/lib/debug/open.rb +13 -0
- data/lib/debug/open_nonstop.rb +15 -0
- data/lib/debug/prelude.rb +50 -0
- data/lib/debug/server.rb +534 -0
- data/lib/debug/server_cdp.rb +1348 -0
- data/lib/debug/server_dap.rb +1108 -0
- data/lib/debug/session.rb +2667 -0
- data/lib/debug/source_repository.rb +150 -0
- data/lib/debug/start.rb +5 -0
- data/lib/debug/thread_client.rb +1457 -0
- data/lib/debug/tracer.rb +241 -0
- data/lib/debug/version.rb +5 -0
- data/lib/debug.rb +9 -0
- data/misc/README.md.erb +660 -0
- metadata +117 -0
@@ -0,0 +1,1348 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'stringio'
|
7
|
+
require 'open3'
|
8
|
+
require 'tmpdir'
|
9
|
+
require 'tempfile'
|
10
|
+
require 'timeout'
|
11
|
+
|
12
|
+
module DEBUGGER__
|
13
|
+
module UI_CDP
|
14
|
+
SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
|
15
|
+
|
16
|
+
class UnsupportedError < StandardError; end
|
17
|
+
class NotFoundChromeEndpointError < StandardError; end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def setup_chrome addr, uuid
|
21
|
+
return if CONFIG[:chrome_path] == ''
|
22
|
+
|
23
|
+
port, path, pid = run_new_chrome
|
24
|
+
begin
|
25
|
+
s = Socket.tcp '127.0.0.1', port
|
26
|
+
rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
ws_client = WebSocketClient.new(s)
|
31
|
+
ws_client.handshake port, path
|
32
|
+
ws_client.send id: 1, method: 'Target.getTargets'
|
33
|
+
|
34
|
+
loop do
|
35
|
+
res = ws_client.extract_data
|
36
|
+
case res['id']
|
37
|
+
when 1
|
38
|
+
target_info = res.dig('result', 'targetInfos')
|
39
|
+
page = target_info.find{|t| t['type'] == 'page'}
|
40
|
+
ws_client.send id: 2, method: 'Target.attachToTarget',
|
41
|
+
params: {
|
42
|
+
targetId: page['targetId'],
|
43
|
+
flatten: true
|
44
|
+
}
|
45
|
+
when 2
|
46
|
+
s_id = res.dig('result', 'sessionId')
|
47
|
+
# TODO: change id
|
48
|
+
ws_client.send sessionId: s_id, id: 100, method: 'Network.enable'
|
49
|
+
ws_client.send sessionId: s_id, id: 3,
|
50
|
+
method: 'Page.enable'
|
51
|
+
when 3
|
52
|
+
s_id = res['sessionId']
|
53
|
+
ws_client.send sessionId: s_id, id: 4,
|
54
|
+
method: 'Page.getFrameTree'
|
55
|
+
when 4
|
56
|
+
s_id = res['sessionId']
|
57
|
+
f_id = res.dig('result', 'frameTree', 'frame', 'id')
|
58
|
+
ws_client.send sessionId: s_id, id: 5,
|
59
|
+
method: 'Page.navigate',
|
60
|
+
params: {
|
61
|
+
url: "devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&noJavaScriptCompletion=true&ws=#{addr}/#{uuid}",
|
62
|
+
frameId: f_id
|
63
|
+
}
|
64
|
+
when 101
|
65
|
+
break
|
66
|
+
else
|
67
|
+
if res['method'] == 'Network.webSocketWillSendHandshakeRequest'
|
68
|
+
s_id = res['sessionId']
|
69
|
+
# Display the console by entering ESC key
|
70
|
+
ws_client.send sessionId: s_id, id: 101, # TODO: change id
|
71
|
+
method:"Input.dispatchKeyEvent",
|
72
|
+
params: {
|
73
|
+
type:"keyDown",
|
74
|
+
windowsVirtualKeyCode:27 # ESC key
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
pid
|
80
|
+
rescue Errno::ENOENT, UnsupportedError, NotFoundChromeEndpointError
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
TIMEOUT_SEC = 5
|
85
|
+
|
86
|
+
def run_new_chrome
|
87
|
+
path = CONFIG[:chrome_path]
|
88
|
+
|
89
|
+
data = nil
|
90
|
+
port = nil
|
91
|
+
wait_thr = nil
|
92
|
+
|
93
|
+
# The process to check OS is based on `selenium` project.
|
94
|
+
case RbConfig::CONFIG['host_os']
|
95
|
+
when /mswin|msys|mingw|cygwin|emc/
|
96
|
+
if path.nil?
|
97
|
+
candidates = ['C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe']
|
98
|
+
path = get_chrome_path candidates
|
99
|
+
end
|
100
|
+
# The path is based on https://github.com/sindresorhus/open/blob/v8.4.0/index.js#L128.
|
101
|
+
stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
|
102
|
+
tf = Tempfile.create(['debug-', '.txt'])
|
103
|
+
|
104
|
+
stdin.puts("Start-process '#{path}' -Argumentlist '--remote-debugging-port=0', '--no-first-run', '--no-default-browser-check', '--user-data-dir=C:\\temp' -Wait -RedirectStandardError #{tf.path}")
|
105
|
+
stdin.close
|
106
|
+
stdout.close
|
107
|
+
stderr.close
|
108
|
+
port, path = get_devtools_endpoint(tf.path)
|
109
|
+
|
110
|
+
at_exit{
|
111
|
+
DEBUGGER__.skip_all
|
112
|
+
|
113
|
+
stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
|
114
|
+
stdin.puts("Stop-process -Name chrome")
|
115
|
+
stdin.close
|
116
|
+
stdout.close
|
117
|
+
stderr.close
|
118
|
+
tf.close
|
119
|
+
begin
|
120
|
+
File.unlink(tf)
|
121
|
+
rescue Errno::EACCES
|
122
|
+
end
|
123
|
+
}
|
124
|
+
when /darwin|mac os/
|
125
|
+
path = path || '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
|
126
|
+
dir = Dir.mktmpdir
|
127
|
+
# The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
|
128
|
+
stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
|
129
|
+
stdin.close
|
130
|
+
stdout.close
|
131
|
+
data = stderr.readpartial 4096
|
132
|
+
stderr.close
|
133
|
+
if data.match(/DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/)
|
134
|
+
port = $1
|
135
|
+
path = $2
|
136
|
+
end
|
137
|
+
|
138
|
+
at_exit{
|
139
|
+
DEBUGGER__.skip_all
|
140
|
+
FileUtils.rm_rf dir
|
141
|
+
}
|
142
|
+
when /linux/
|
143
|
+
path = path || 'google-chrome'
|
144
|
+
dir = Dir.mktmpdir
|
145
|
+
# The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
|
146
|
+
stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
|
147
|
+
stdin.close
|
148
|
+
stdout.close
|
149
|
+
data = ''
|
150
|
+
begin
|
151
|
+
Timeout.timeout(TIMEOUT_SEC) do
|
152
|
+
until data.match?(/DevTools listening on ws:\/\/127.0.0.1:\d+.*/)
|
153
|
+
data = stderr.readpartial 4096
|
154
|
+
end
|
155
|
+
end
|
156
|
+
rescue Exception
|
157
|
+
raise NotFoundChromeEndpointError
|
158
|
+
end
|
159
|
+
stderr.close
|
160
|
+
if data.match(/DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/)
|
161
|
+
port = $1
|
162
|
+
path = $2
|
163
|
+
end
|
164
|
+
|
165
|
+
at_exit{
|
166
|
+
DEBUGGER__.skip_all
|
167
|
+
FileUtils.rm_rf dir
|
168
|
+
}
|
169
|
+
else
|
170
|
+
raise UnsupportedError
|
171
|
+
end
|
172
|
+
|
173
|
+
[port, path, wait_thr.pid]
|
174
|
+
end
|
175
|
+
|
176
|
+
def get_chrome_path candidates
|
177
|
+
candidates.each{|c|
|
178
|
+
if File.exist? c
|
179
|
+
return c
|
180
|
+
end
|
181
|
+
}
|
182
|
+
raise UnsupportedError
|
183
|
+
end
|
184
|
+
|
185
|
+
ITERATIONS = 50
|
186
|
+
|
187
|
+
def get_devtools_endpoint tf
|
188
|
+
i = 1
|
189
|
+
while i < ITERATIONS
|
190
|
+
i += 1
|
191
|
+
if File.exist?(tf) && data = File.read(tf)
|
192
|
+
if data.match(/DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/)
|
193
|
+
port = $1
|
194
|
+
path = $2
|
195
|
+
return [port, path]
|
196
|
+
end
|
197
|
+
end
|
198
|
+
sleep 0.1
|
199
|
+
end
|
200
|
+
raise NotFoundChromeEndpointError
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def send_chrome_response req
|
205
|
+
@repl = false
|
206
|
+
case req
|
207
|
+
when /^GET\s\/json\/version\sHTTP\/1.1/
|
208
|
+
body = {
|
209
|
+
Browser: "ruby/v#{RUBY_VERSION}",
|
210
|
+
'Protocol-Version': "1.1"
|
211
|
+
}
|
212
|
+
send_http_res body
|
213
|
+
raise UI_ServerBase::RetryConnection
|
214
|
+
|
215
|
+
when /^GET\s\/json\sHTTP\/1.1/
|
216
|
+
@uuid = @uuid || SecureRandom.uuid
|
217
|
+
addr = @local_addr.inspect_sockaddr
|
218
|
+
body = [{
|
219
|
+
description: "ruby instance",
|
220
|
+
devtoolsFrontendUrl: "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=#{addr}/#{@uuid}",
|
221
|
+
id: @uuid,
|
222
|
+
title: $0,
|
223
|
+
type: "node",
|
224
|
+
url: "file://#{File.absolute_path($0)}",
|
225
|
+
webSocketDebuggerUrl: "ws://#{addr}/#{@uuid}"
|
226
|
+
}]
|
227
|
+
send_http_res body
|
228
|
+
raise UI_ServerBase::RetryConnection
|
229
|
+
|
230
|
+
when /^GET\s\/(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\sHTTP\/1.1/
|
231
|
+
raise 'Incorrect uuid' unless $1 == @uuid
|
232
|
+
|
233
|
+
@need_pause_at_first = false
|
234
|
+
CONFIG.set_config no_color: true
|
235
|
+
|
236
|
+
@ws_server = WebSocketServer.new(@sock)
|
237
|
+
@ws_server.handshake
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def send_http_res body
|
242
|
+
json = JSON.generate body
|
243
|
+
header = "HTTP/1.0 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\nCache-Control: no-cache\r\nContent-Length: #{json.bytesize}\r\n\r\n"
|
244
|
+
@sock.puts "#{header}#{json}"
|
245
|
+
end
|
246
|
+
|
247
|
+
module WebSocketUtils
|
248
|
+
class Frame
|
249
|
+
attr_reader :b
|
250
|
+
|
251
|
+
def initialize
|
252
|
+
@b = ''.b
|
253
|
+
end
|
254
|
+
|
255
|
+
def << obj
|
256
|
+
case obj
|
257
|
+
when String
|
258
|
+
@b << obj.b
|
259
|
+
when Enumerable
|
260
|
+
obj.each{|e| self << e}
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def char bytes
|
265
|
+
@b << bytes
|
266
|
+
end
|
267
|
+
|
268
|
+
def ulonglong bytes
|
269
|
+
@b << [bytes].pack('Q>')
|
270
|
+
end
|
271
|
+
|
272
|
+
def uint16 bytes
|
273
|
+
@b << [bytes].pack('n*')
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def show_protocol dir, msg
|
278
|
+
if DEBUGGER__::UI_CDP::SHOW_PROTOCOL
|
279
|
+
$stderr.puts "\#[#{dir}] #{msg}"
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
class WebSocketClient
|
285
|
+
include WebSocketUtils
|
286
|
+
|
287
|
+
def initialize s
|
288
|
+
@sock = s
|
289
|
+
end
|
290
|
+
|
291
|
+
def handshake port, path
|
292
|
+
key = SecureRandom.hex(11)
|
293
|
+
req = "GET #{path} HTTP/1.1\r\nHost: 127.0.0.1:#{port}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: #{key}==\r\n\r\n"
|
294
|
+
show_protocol :>, req
|
295
|
+
@sock.print req
|
296
|
+
res = @sock.readpartial 4092
|
297
|
+
show_protocol :<, res
|
298
|
+
|
299
|
+
if res.match(/^Sec-WebSocket-Accept: (.*)\r\n/)
|
300
|
+
correct_key = Digest::SHA1.base64digest "#{key}==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
301
|
+
raise "The Sec-WebSocket-Accept value: #{$1} is not valid" unless $1 == correct_key
|
302
|
+
else
|
303
|
+
raise "Unknown response: #{res}"
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def send **msg
|
308
|
+
msg = JSON.generate(msg)
|
309
|
+
show_protocol :>, msg
|
310
|
+
frame = Frame.new
|
311
|
+
fin = 0b10000000
|
312
|
+
opcode = 0b00000001
|
313
|
+
frame.char fin + opcode
|
314
|
+
|
315
|
+
mask = 0b10000000 # A client must mask all frames in a WebSocket Protocol.
|
316
|
+
bytesize = msg.bytesize
|
317
|
+
if bytesize < 126
|
318
|
+
payload_len = bytesize
|
319
|
+
frame.char mask + payload_len
|
320
|
+
elsif bytesize < 2 ** 16
|
321
|
+
payload_len = 0b01111110
|
322
|
+
frame.char mask + payload_len
|
323
|
+
frame.uint16 bytesize
|
324
|
+
elsif bytesize < 2 ** 64
|
325
|
+
payload_len = 0b01111111
|
326
|
+
frame.char mask + payload_len
|
327
|
+
frame.ulonglong bytesize
|
328
|
+
else
|
329
|
+
raise 'Bytesize is too big.'
|
330
|
+
end
|
331
|
+
|
332
|
+
masking_key = 4.times.map{
|
333
|
+
key = rand(1..255)
|
334
|
+
frame.char key
|
335
|
+
key
|
336
|
+
}
|
337
|
+
msg.bytes.each_with_index do |b, i|
|
338
|
+
frame.char(b ^ masking_key[i % 4])
|
339
|
+
end
|
340
|
+
|
341
|
+
@sock.print frame.b
|
342
|
+
end
|
343
|
+
|
344
|
+
def extract_data
|
345
|
+
first_group = @sock.getbyte
|
346
|
+
fin = first_group & 0b10000000 != 128
|
347
|
+
raise 'Unsupported' if fin
|
348
|
+
opcode = first_group & 0b00001111
|
349
|
+
raise "Unsupported: #{opcode}" unless opcode == 1
|
350
|
+
|
351
|
+
second_group = @sock.getbyte
|
352
|
+
mask = second_group & 0b10000000 == 128
|
353
|
+
raise 'The server must not mask any frames' if mask
|
354
|
+
payload_len = second_group & 0b01111111
|
355
|
+
# TODO: Support other payload_lengths
|
356
|
+
if payload_len == 126
|
357
|
+
payload_len = @sock.read(2).unpack('n*')[0]
|
358
|
+
end
|
359
|
+
|
360
|
+
msg = @sock.read payload_len
|
361
|
+
show_protocol :<, msg
|
362
|
+
JSON.parse msg
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
class Detach < StandardError
|
367
|
+
end
|
368
|
+
|
369
|
+
class WebSocketServer
|
370
|
+
include WebSocketUtils
|
371
|
+
|
372
|
+
def initialize s
|
373
|
+
@sock = s
|
374
|
+
end
|
375
|
+
|
376
|
+
def handshake
|
377
|
+
req = @sock.readpartial 4096
|
378
|
+
show_protocol '>', req
|
379
|
+
|
380
|
+
if req.match(/^Sec-WebSocket-Key: (.*)\r\n/)
|
381
|
+
accept = Digest::SHA1.base64digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
382
|
+
res = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
|
383
|
+
@sock.print res
|
384
|
+
show_protocol :<, res
|
385
|
+
else
|
386
|
+
"Unknown request: #{req}"
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def send **msg
|
391
|
+
msg = JSON.generate(msg)
|
392
|
+
show_protocol :<, msg
|
393
|
+
frame = Frame.new
|
394
|
+
fin = 0b10000000
|
395
|
+
opcode = 0b00000001
|
396
|
+
frame.char fin + opcode
|
397
|
+
|
398
|
+
mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
|
399
|
+
bytesize = msg.bytesize
|
400
|
+
if bytesize < 126
|
401
|
+
payload_len = bytesize
|
402
|
+
frame.char mask + payload_len
|
403
|
+
elsif bytesize < 2 ** 16
|
404
|
+
payload_len = 0b01111110
|
405
|
+
frame.char mask + payload_len
|
406
|
+
frame.uint16 bytesize
|
407
|
+
elsif bytesize < 2 ** 64
|
408
|
+
payload_len = 0b01111111
|
409
|
+
frame.char mask + payload_len
|
410
|
+
frame.ulonglong bytesize
|
411
|
+
else
|
412
|
+
raise 'Bytesize is too big.'
|
413
|
+
end
|
414
|
+
|
415
|
+
frame << msg
|
416
|
+
@sock.print frame.b
|
417
|
+
end
|
418
|
+
|
419
|
+
def extract_data
|
420
|
+
first_group = @sock.getbyte
|
421
|
+
fin = first_group & 0b10000000 != 128
|
422
|
+
raise 'Unsupported' if fin
|
423
|
+
|
424
|
+
opcode = first_group & 0b00001111
|
425
|
+
raise Detach if opcode == 8
|
426
|
+
raise "Unsupported: #{opcode}" unless opcode == 1
|
427
|
+
|
428
|
+
second_group = @sock.getbyte
|
429
|
+
mask = second_group & 0b10000000 == 128
|
430
|
+
raise 'The client must mask all frames' unless mask
|
431
|
+
payload_len = second_group & 0b01111111
|
432
|
+
# TODO: Support other payload_lengths
|
433
|
+
if payload_len == 126
|
434
|
+
payload_len = @sock.gets(2).unpack('n*')[0]
|
435
|
+
end
|
436
|
+
|
437
|
+
masking_key = []
|
438
|
+
4.times { masking_key << @sock.getbyte }
|
439
|
+
unmasked = []
|
440
|
+
payload_len.times do |n|
|
441
|
+
masked = @sock.getbyte
|
442
|
+
unmasked << (masked ^ masking_key[n % 4])
|
443
|
+
end
|
444
|
+
msg = unmasked.pack 'c*'
|
445
|
+
show_protocol :>, msg
|
446
|
+
JSON.parse msg
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
def send_response req, **res
|
451
|
+
@ws_server.send id: req['id'], result: res
|
452
|
+
end
|
453
|
+
|
454
|
+
def send_fail_response req, **res
|
455
|
+
@ws_server.send id: req['id'], error: res
|
456
|
+
end
|
457
|
+
|
458
|
+
def send_event method, **params
|
459
|
+
@ws_server.send method: method, params: params
|
460
|
+
end
|
461
|
+
|
462
|
+
INVALID_REQUEST = -32600
|
463
|
+
|
464
|
+
def process
|
465
|
+
bps = {}
|
466
|
+
@src_map = {}
|
467
|
+
loop do
|
468
|
+
req = @ws_server.extract_data
|
469
|
+
|
470
|
+
case req['method']
|
471
|
+
|
472
|
+
## boot/configuration
|
473
|
+
when 'Debugger.getScriptSource'
|
474
|
+
@q_msg << req
|
475
|
+
when 'Debugger.enable'
|
476
|
+
send_response req, debuggerId: rand.to_s
|
477
|
+
@q_msg << req
|
478
|
+
when 'Runtime.enable'
|
479
|
+
send_response req
|
480
|
+
send_event 'Runtime.executionContextCreated',
|
481
|
+
context: {
|
482
|
+
id: SecureRandom.hex(16),
|
483
|
+
origin: "http://#{@local_addr.inspect_sockaddr}",
|
484
|
+
name: ''
|
485
|
+
}
|
486
|
+
when 'Runtime.getIsolateId'
|
487
|
+
send_response req,
|
488
|
+
id: SecureRandom.hex
|
489
|
+
when 'Runtime.terminateExecution'
|
490
|
+
send_response req
|
491
|
+
exit
|
492
|
+
when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
|
493
|
+
'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
|
494
|
+
'Runtime.releaseObjectGroup', 'Runtime.discardConsoleEntries', 'Log.clear', 'Runtime.runIfWaitingForDebugger'
|
495
|
+
send_response req
|
496
|
+
|
497
|
+
## control
|
498
|
+
when 'Debugger.resume'
|
499
|
+
send_response req
|
500
|
+
send_event 'Debugger.resumed'
|
501
|
+
@q_msg << 'c'
|
502
|
+
@q_msg << req
|
503
|
+
when 'Debugger.stepOver'
|
504
|
+
begin
|
505
|
+
@session.check_postmortem
|
506
|
+
send_response req
|
507
|
+
send_event 'Debugger.resumed'
|
508
|
+
@q_msg << 'n'
|
509
|
+
rescue PostmortemError
|
510
|
+
send_fail_response req,
|
511
|
+
code: INVALID_REQUEST,
|
512
|
+
message: "'stepOver' is not supported while postmortem mode"
|
513
|
+
ensure
|
514
|
+
@q_msg << req
|
515
|
+
end
|
516
|
+
when 'Debugger.stepInto'
|
517
|
+
begin
|
518
|
+
@session.check_postmortem
|
519
|
+
send_response req
|
520
|
+
send_event 'Debugger.resumed'
|
521
|
+
@q_msg << 's'
|
522
|
+
rescue PostmortemError
|
523
|
+
send_fail_response req,
|
524
|
+
code: INVALID_REQUEST,
|
525
|
+
message: "'stepInto' is not supported while postmortem mode"
|
526
|
+
ensure
|
527
|
+
@q_msg << req
|
528
|
+
end
|
529
|
+
when 'Debugger.stepOut'
|
530
|
+
begin
|
531
|
+
@session.check_postmortem
|
532
|
+
send_response req
|
533
|
+
send_event 'Debugger.resumed'
|
534
|
+
@q_msg << 'fin'
|
535
|
+
rescue PostmortemError
|
536
|
+
send_fail_response req,
|
537
|
+
code: INVALID_REQUEST,
|
538
|
+
message: "'stepOut' is not supported while postmortem mode"
|
539
|
+
ensure
|
540
|
+
@q_msg << req
|
541
|
+
end
|
542
|
+
when 'Debugger.setSkipAllPauses'
|
543
|
+
skip = req.dig('params', 'skip')
|
544
|
+
if skip
|
545
|
+
deactivate_bp
|
546
|
+
else
|
547
|
+
activate_bp bps
|
548
|
+
end
|
549
|
+
send_response req
|
550
|
+
when 'Debugger.pause'
|
551
|
+
send_response req
|
552
|
+
Process.kill(UI_ServerBase::TRAP_SIGNAL, Process.pid)
|
553
|
+
|
554
|
+
# breakpoint
|
555
|
+
when 'Debugger.getPossibleBreakpoints'
|
556
|
+
@q_msg << req
|
557
|
+
when 'Debugger.setBreakpointByUrl'
|
558
|
+
line = req.dig('params', 'lineNumber')
|
559
|
+
if regexp = req.dig('params', 'urlRegex')
|
560
|
+
b_id = "1:#{line}:#{regexp}"
|
561
|
+
bps[b_id] = bps.size
|
562
|
+
path = regexp.match(/(.*)\|/)[1].gsub("\\", "")
|
563
|
+
add_line_breakpoint(req, b_id, path)
|
564
|
+
elsif url = req.dig('params', 'url')
|
565
|
+
b_id = "#{line}:#{url}"
|
566
|
+
# When breakpoints are set in Script snippet, non-existent path such as "snippet:///Script%20snippet%20%231" sent.
|
567
|
+
# That's why we need to check it here.
|
568
|
+
if File.exist? url
|
569
|
+
bps[b_id] = bps.size
|
570
|
+
add_line_breakpoint(req, b_id, url)
|
571
|
+
else
|
572
|
+
send_response req,
|
573
|
+
breakpointId: b_id,
|
574
|
+
locations: []
|
575
|
+
end
|
576
|
+
else
|
577
|
+
if hash = req.dig('params', 'scriptHash')
|
578
|
+
b_id = "#{line}:#{hash}"
|
579
|
+
send_response req,
|
580
|
+
breakpointId: b_id,
|
581
|
+
locations: []
|
582
|
+
else
|
583
|
+
raise 'Unsupported'
|
584
|
+
end
|
585
|
+
end
|
586
|
+
when 'Debugger.removeBreakpoint'
|
587
|
+
b_id = req.dig('params', 'breakpointId')
|
588
|
+
bps = del_bp bps, b_id
|
589
|
+
send_response req
|
590
|
+
when 'Debugger.setBreakpointsActive'
|
591
|
+
active = req.dig('params', 'active')
|
592
|
+
if active
|
593
|
+
activate_bp bps
|
594
|
+
else
|
595
|
+
deactivate_bp # TODO: Change this part because catch breakpoints should not be deactivated.
|
596
|
+
end
|
597
|
+
send_response req
|
598
|
+
when 'Debugger.setPauseOnExceptions'
|
599
|
+
state = req.dig('params', 'state')
|
600
|
+
ex = 'Exception'
|
601
|
+
case state
|
602
|
+
when 'none'
|
603
|
+
@q_msg << 'config postmortem = false'
|
604
|
+
bps = del_bp bps, ex
|
605
|
+
when 'uncaught'
|
606
|
+
@q_msg << 'config postmortem = true'
|
607
|
+
bps = del_bp bps, ex
|
608
|
+
when 'all'
|
609
|
+
@q_msg << 'config postmortem = false'
|
610
|
+
SESSION.add_catch_breakpoint ex
|
611
|
+
bps[ex] = bps.size
|
612
|
+
end
|
613
|
+
send_response req
|
614
|
+
|
615
|
+
when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
|
616
|
+
@q_msg << req
|
617
|
+
end
|
618
|
+
end
|
619
|
+
rescue Detach
|
620
|
+
@q_msg << 'continue'
|
621
|
+
end
|
622
|
+
|
623
|
+
def add_line_breakpoint req, b_id, path
|
624
|
+
cond = req.dig('params', 'condition')
|
625
|
+
line = req.dig('params', 'lineNumber')
|
626
|
+
src = get_source_code path
|
627
|
+
end_line = src.lines.count
|
628
|
+
line = end_line if line > end_line
|
629
|
+
if cond != ''
|
630
|
+
SESSION.add_line_breakpoint(path, line + 1, cond: cond)
|
631
|
+
else
|
632
|
+
SESSION.add_line_breakpoint(path, line + 1)
|
633
|
+
end
|
634
|
+
# Because we need to return scriptId, responses are returned in SESSION thread.
|
635
|
+
req['params']['scriptId'] = path
|
636
|
+
req['params']['lineNumber'] = line
|
637
|
+
req['params']['breakpointId'] = b_id
|
638
|
+
@q_msg << req
|
639
|
+
end
|
640
|
+
|
641
|
+
def del_bp bps, k
|
642
|
+
return bps unless idx = bps[k]
|
643
|
+
|
644
|
+
bps.delete k
|
645
|
+
bps.each_key{|i| bps[i] -= 1 if bps[i] > idx}
|
646
|
+
@q_msg << "del #{idx}"
|
647
|
+
bps
|
648
|
+
end
|
649
|
+
|
650
|
+
def get_source_code path
|
651
|
+
return @src_map[path] if @src_map[path]
|
652
|
+
|
653
|
+
src = File.read(path)
|
654
|
+
@src_map[path] = src
|
655
|
+
src
|
656
|
+
end
|
657
|
+
|
658
|
+
def activate_bp bps
|
659
|
+
bps.each_key{|k|
|
660
|
+
if k.match(/^\d+:(\d+):(.*)/)
|
661
|
+
line = $1
|
662
|
+
path = $2
|
663
|
+
SESSION.add_line_breakpoint(path, line.to_i + 1)
|
664
|
+
else
|
665
|
+
SESSION.add_catch_breakpoint 'Exception'
|
666
|
+
end
|
667
|
+
}
|
668
|
+
end
|
669
|
+
|
670
|
+
def deactivate_bp
|
671
|
+
@q_msg << 'del'
|
672
|
+
@q_ans << 'y'
|
673
|
+
end
|
674
|
+
|
675
|
+
def cleanup_reader
|
676
|
+
super
|
677
|
+
Process.kill :KILL, @chrome_pid if @chrome_pid
|
678
|
+
rescue Errno::ESRCH # continue if @chrome_pid process is not found
|
679
|
+
end
|
680
|
+
|
681
|
+
## Called by the SESSION thread
|
682
|
+
|
683
|
+
alias respond send_response
|
684
|
+
alias respond_fail send_fail_response
|
685
|
+
alias fire_event send_event
|
686
|
+
|
687
|
+
def sock skip: false
|
688
|
+
yield $stderr
|
689
|
+
end
|
690
|
+
|
691
|
+
def puts result = ""
|
692
|
+
# STDERR.puts "puts: #{result}"
|
693
|
+
# send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
|
694
|
+
end
|
695
|
+
end
|
696
|
+
|
697
|
+
class Session
|
698
|
+
include GlobalVariablesHelper
|
699
|
+
|
700
|
+
# FIXME: unify this method with ThreadClient#propertyDescriptor.
|
701
|
+
def get_type obj
|
702
|
+
case obj
|
703
|
+
when Array
|
704
|
+
['object', 'array']
|
705
|
+
when Hash
|
706
|
+
['object', 'map']
|
707
|
+
when String
|
708
|
+
['string']
|
709
|
+
when TrueClass, FalseClass
|
710
|
+
['boolean']
|
711
|
+
when Symbol
|
712
|
+
['symbol']
|
713
|
+
when Integer, Float
|
714
|
+
['number']
|
715
|
+
when Exception
|
716
|
+
['object', 'error']
|
717
|
+
else
|
718
|
+
['object']
|
719
|
+
end
|
720
|
+
end
|
721
|
+
|
722
|
+
def fail_response req, **result
|
723
|
+
@ui.respond_fail req, **result
|
724
|
+
return :retry
|
725
|
+
end
|
726
|
+
|
727
|
+
INVALID_PARAMS = -32602
|
728
|
+
INTERNAL_ERROR = -32603
|
729
|
+
|
730
|
+
def process_protocol_request req
|
731
|
+
case req['method']
|
732
|
+
when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.enable'
|
733
|
+
request_tc [:cdp, :backtrace, req]
|
734
|
+
when 'Debugger.evaluateOnCallFrame'
|
735
|
+
frame_id = req.dig('params', 'callFrameId')
|
736
|
+
group = req.dig('params', 'objectGroup')
|
737
|
+
if fid = @frame_map[frame_id]
|
738
|
+
expr = req.dig('params', 'expression')
|
739
|
+
request_tc [:cdp, :evaluate, req, fid, expr, group]
|
740
|
+
else
|
741
|
+
fail_response req,
|
742
|
+
code: INVALID_PARAMS,
|
743
|
+
message: "'callFrameId' is an invalid"
|
744
|
+
end
|
745
|
+
when 'Runtime.getProperties', 'Runtime.getExceptionDetails'
|
746
|
+
oid = req.dig('params', 'objectId') || req.dig('params', 'errorObjectId')
|
747
|
+
if ref = @obj_map[oid]
|
748
|
+
case ref[0]
|
749
|
+
when 'local'
|
750
|
+
frame_id = ref[1]
|
751
|
+
fid = @frame_map[frame_id]
|
752
|
+
request_tc [:cdp, :scope, req, fid]
|
753
|
+
when 'global'
|
754
|
+
vars = safe_global_variables.sort.map do |name|
|
755
|
+
begin
|
756
|
+
gv = eval(name.to_s)
|
757
|
+
rescue Errno::ENOENT
|
758
|
+
gv = nil
|
759
|
+
end
|
760
|
+
prop = {
|
761
|
+
name: name,
|
762
|
+
value: {
|
763
|
+
description: gv.inspect
|
764
|
+
},
|
765
|
+
configurable: true,
|
766
|
+
enumerable: true
|
767
|
+
}
|
768
|
+
type, subtype = get_type(gv)
|
769
|
+
prop[:value][:type] = type
|
770
|
+
prop[:value][:subtype] = subtype if subtype
|
771
|
+
prop
|
772
|
+
end
|
773
|
+
|
774
|
+
@ui.respond req, result: vars
|
775
|
+
return :retry
|
776
|
+
when 'properties'
|
777
|
+
request_tc [:cdp, :properties, req, oid]
|
778
|
+
when 'exception'
|
779
|
+
request_tc [:cdp, :exception, req, oid]
|
780
|
+
when 'script'
|
781
|
+
# TODO: Support script and global types
|
782
|
+
@ui.respond req, result: []
|
783
|
+
return :retry
|
784
|
+
else
|
785
|
+
raise "Unknown type: #{ref.inspect}"
|
786
|
+
end
|
787
|
+
else
|
788
|
+
fail_response req,
|
789
|
+
code: INVALID_PARAMS,
|
790
|
+
message: "'objectId' is an invalid"
|
791
|
+
end
|
792
|
+
when 'Debugger.getScriptSource'
|
793
|
+
s_id = req.dig('params', 'scriptId')
|
794
|
+
if src = @src_map[s_id]
|
795
|
+
@ui.respond req, scriptSource: src
|
796
|
+
else
|
797
|
+
fail_response req,
|
798
|
+
code: INVALID_PARAMS,
|
799
|
+
message: "'scriptId' is an invalid"
|
800
|
+
end
|
801
|
+
return :retry
|
802
|
+
when 'Debugger.getPossibleBreakpoints'
|
803
|
+
s_id = req.dig('params', 'start', 'scriptId')
|
804
|
+
if src = @src_map[s_id]
|
805
|
+
lineno = req.dig('params', 'start', 'lineNumber')
|
806
|
+
end_line = src.lines.count
|
807
|
+
lineno = end_line if lineno > end_line
|
808
|
+
@ui.respond req,
|
809
|
+
locations: [{
|
810
|
+
scriptId: s_id,
|
811
|
+
lineNumber: lineno
|
812
|
+
}]
|
813
|
+
else
|
814
|
+
fail_response req,
|
815
|
+
code: INVALID_PARAMS,
|
816
|
+
message: "'scriptId' is an invalid"
|
817
|
+
end
|
818
|
+
return :retry
|
819
|
+
when 'Debugger.setBreakpointByUrl'
|
820
|
+
path = req.dig('params', 'scriptId')
|
821
|
+
if s_id = @scr_id_map[path]
|
822
|
+
lineno = req.dig('params', 'lineNumber')
|
823
|
+
b_id = req.dig('params', 'breakpointId')
|
824
|
+
@ui.respond req,
|
825
|
+
breakpointId: b_id,
|
826
|
+
locations: [{
|
827
|
+
scriptId: s_id,
|
828
|
+
lineNumber: lineno
|
829
|
+
}]
|
830
|
+
else
|
831
|
+
fail_response req,
|
832
|
+
code: INTERNAL_ERROR,
|
833
|
+
message: 'The target script is not found...'
|
834
|
+
end
|
835
|
+
return :retry
|
836
|
+
end
|
837
|
+
end
|
838
|
+
|
839
|
+
def process_protocol_result args
|
840
|
+
type, req, result = args
|
841
|
+
|
842
|
+
case type
|
843
|
+
when :backtrace
|
844
|
+
result[:callFrames].each.with_index do |frame, i|
|
845
|
+
frame_id = frame[:callFrameId]
|
846
|
+
@frame_map[frame_id] = i
|
847
|
+
path = frame[:url]
|
848
|
+
unless s_id = @scr_id_map[path]
|
849
|
+
s_id = (@scr_id_map.size + 1).to_s
|
850
|
+
@scr_id_map[path] = s_id
|
851
|
+
lineno = 0
|
852
|
+
src = ''
|
853
|
+
if path && File.exist?(path)
|
854
|
+
src = File.read(path)
|
855
|
+
@src_map[s_id] = src
|
856
|
+
lineno = src.lines.count
|
857
|
+
end
|
858
|
+
@ui.fire_event 'Debugger.scriptParsed',
|
859
|
+
scriptId: s_id,
|
860
|
+
url: path,
|
861
|
+
startLine: 0,
|
862
|
+
startColumn: 0,
|
863
|
+
endLine: lineno,
|
864
|
+
endColumn: 0,
|
865
|
+
executionContextId: 1,
|
866
|
+
hash: src.hash.inspect
|
867
|
+
end
|
868
|
+
frame[:location][:scriptId] = s_id
|
869
|
+
frame[:functionLocation][:scriptId] = s_id
|
870
|
+
|
871
|
+
frame[:scopeChain].each {|s|
|
872
|
+
oid = s.dig(:object, :objectId)
|
873
|
+
@obj_map[oid] = [s[:type], frame_id]
|
874
|
+
}
|
875
|
+
end
|
876
|
+
|
877
|
+
if oid = result.dig(:data, :objectId)
|
878
|
+
@obj_map[oid] = ['properties']
|
879
|
+
end
|
880
|
+
@ui.fire_event 'Debugger.paused', **result
|
881
|
+
when :evaluate
|
882
|
+
message = result.delete :message
|
883
|
+
if message
|
884
|
+
fail_response req,
|
885
|
+
code: INVALID_PARAMS,
|
886
|
+
message: message
|
887
|
+
else
|
888
|
+
src = req.dig('params', 'expression')
|
889
|
+
s_id = (@src_map.size + 1).to_s
|
890
|
+
@src_map[s_id] = src
|
891
|
+
lineno = src.lines.count
|
892
|
+
@ui.fire_event 'Debugger.scriptParsed',
|
893
|
+
scriptId: s_id,
|
894
|
+
url: '',
|
895
|
+
startLine: 0,
|
896
|
+
startColumn: 0,
|
897
|
+
endLine: lineno,
|
898
|
+
endColumn: 0,
|
899
|
+
executionContextId: 1,
|
900
|
+
hash: src.hash.inspect
|
901
|
+
if exc = result.dig(:response, :exceptionDetails)
|
902
|
+
exc[:stackTrace][:callFrames].each{|frame|
|
903
|
+
if frame[:url].empty?
|
904
|
+
frame[:scriptId] = s_id
|
905
|
+
else
|
906
|
+
path = frame[:url]
|
907
|
+
unless s_id = @scr_id_map[path]
|
908
|
+
s_id = (@scr_id_map.size + 1).to_s
|
909
|
+
@scr_id_map[path] = s_id
|
910
|
+
end
|
911
|
+
frame[:scriptId] = s_id
|
912
|
+
end
|
913
|
+
}
|
914
|
+
if oid = exc[:exception][:objectId]
|
915
|
+
@obj_map[oid] = ['exception']
|
916
|
+
end
|
917
|
+
end
|
918
|
+
rs = result.dig(:response, :result)
|
919
|
+
[rs].each{|obj|
|
920
|
+
if oid = obj[:objectId]
|
921
|
+
@obj_map[oid] = ['properties']
|
922
|
+
end
|
923
|
+
}
|
924
|
+
@ui.respond req, **result[:response]
|
925
|
+
|
926
|
+
out = result[:output]
|
927
|
+
if out && !out.empty?
|
928
|
+
@ui.fire_event 'Runtime.consoleAPICalled',
|
929
|
+
type: 'log',
|
930
|
+
args: [
|
931
|
+
type: out.class,
|
932
|
+
value: out
|
933
|
+
],
|
934
|
+
executionContextId: 1, # Change this number if something goes wrong.
|
935
|
+
timestamp: Time.now.to_f
|
936
|
+
end
|
937
|
+
end
|
938
|
+
when :scope
|
939
|
+
result.each{|obj|
|
940
|
+
if oid = obj.dig(:value, :objectId)
|
941
|
+
@obj_map[oid] = ['properties']
|
942
|
+
end
|
943
|
+
}
|
944
|
+
@ui.respond req, result: result
|
945
|
+
when :properties
|
946
|
+
result.each_value{|v|
|
947
|
+
v.each{|obj|
|
948
|
+
if oid = obj.dig(:value, :objectId)
|
949
|
+
@obj_map[oid] = ['properties']
|
950
|
+
end
|
951
|
+
}
|
952
|
+
}
|
953
|
+
@ui.respond req, **result
|
954
|
+
when :exception
|
955
|
+
@ui.respond req, **result
|
956
|
+
end
|
957
|
+
end
|
958
|
+
end
|
959
|
+
|
960
|
+
class ThreadClient
|
961
|
+
def process_cdp args
|
962
|
+
type = args.shift
|
963
|
+
req = args.shift
|
964
|
+
|
965
|
+
case type
|
966
|
+
when :backtrace
|
967
|
+
exception = nil
|
968
|
+
result = {
|
969
|
+
reason: 'other',
|
970
|
+
callFrames: @target_frames.map.with_index{|frame, i|
|
971
|
+
exception = frame.raised_exception if frame == current_frame && frame.has_raised_exception
|
972
|
+
|
973
|
+
path = frame.realpath || frame.path
|
974
|
+
|
975
|
+
if frame.iseq.nil?
|
976
|
+
lineno = 0
|
977
|
+
else
|
978
|
+
lineno = frame.iseq.first_line - 1
|
979
|
+
end
|
980
|
+
|
981
|
+
{
|
982
|
+
callFrameId: SecureRandom.hex(16),
|
983
|
+
functionName: frame.name,
|
984
|
+
functionLocation: {
|
985
|
+
# scriptId: N, # filled by SESSION
|
986
|
+
lineNumber: lineno
|
987
|
+
},
|
988
|
+
location: {
|
989
|
+
# scriptId: N, # filled by SESSION
|
990
|
+
lineNumber: frame.location.lineno - 1 # The line number is 0-based.
|
991
|
+
},
|
992
|
+
url: path,
|
993
|
+
scopeChain: [
|
994
|
+
{
|
995
|
+
type: 'local',
|
996
|
+
object: {
|
997
|
+
type: 'object',
|
998
|
+
objectId: rand.to_s
|
999
|
+
}
|
1000
|
+
},
|
1001
|
+
{
|
1002
|
+
type: 'script',
|
1003
|
+
object: {
|
1004
|
+
type: 'object',
|
1005
|
+
objectId: rand.to_s
|
1006
|
+
}
|
1007
|
+
},
|
1008
|
+
{
|
1009
|
+
type: 'global',
|
1010
|
+
object: {
|
1011
|
+
type: 'object',
|
1012
|
+
objectId: rand.to_s
|
1013
|
+
}
|
1014
|
+
}
|
1015
|
+
],
|
1016
|
+
this: {
|
1017
|
+
type: 'object'
|
1018
|
+
}
|
1019
|
+
}
|
1020
|
+
}
|
1021
|
+
}
|
1022
|
+
|
1023
|
+
if exception
|
1024
|
+
result[:data] = evaluate_result exception
|
1025
|
+
result[:reason] = 'exception'
|
1026
|
+
end
|
1027
|
+
event! :protocol_result, :backtrace, req, result
|
1028
|
+
when :evaluate
|
1029
|
+
res = {}
|
1030
|
+
fid, expr, group = args
|
1031
|
+
frame = @target_frames[fid]
|
1032
|
+
message = nil
|
1033
|
+
|
1034
|
+
if frame && (b = frame.eval_binding)
|
1035
|
+
special_local_variables frame do |name, var|
|
1036
|
+
b.local_variable_set(name, var) if /\%/ !~name
|
1037
|
+
end
|
1038
|
+
|
1039
|
+
result = nil
|
1040
|
+
|
1041
|
+
case group
|
1042
|
+
when 'popover'
|
1043
|
+
case expr
|
1044
|
+
# Chrome doesn't read instance variables
|
1045
|
+
when /\A\$\S/
|
1046
|
+
safe_global_variables.each{|gvar|
|
1047
|
+
if gvar.to_s == expr
|
1048
|
+
result = eval(gvar.to_s)
|
1049
|
+
break false
|
1050
|
+
end
|
1051
|
+
} and (message = "Error: Not defined global variable: #{expr.inspect}")
|
1052
|
+
when /(\A((::[A-Z]|[A-Z])\w*)+)/
|
1053
|
+
unless result = search_const(b, $1)
|
1054
|
+
message = "Error: Not defined constant: #{expr.inspect}"
|
1055
|
+
end
|
1056
|
+
else
|
1057
|
+
begin
|
1058
|
+
result = b.local_variable_get(expr)
|
1059
|
+
rescue NameError
|
1060
|
+
# try to check method
|
1061
|
+
if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true)
|
1062
|
+
result = M_METHOD.bind_call(b.receiver, expr)
|
1063
|
+
else
|
1064
|
+
message = "Error: Can not evaluate: #{expr.inspect}"
|
1065
|
+
end
|
1066
|
+
end
|
1067
|
+
end
|
1068
|
+
when 'console', 'watch-group'
|
1069
|
+
begin
|
1070
|
+
orig_stdout = $stdout
|
1071
|
+
$stdout = StringIO.new
|
1072
|
+
result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
|
1073
|
+
rescue Exception => e
|
1074
|
+
result = e
|
1075
|
+
res[:exceptionDetails] = exceptionDetails(e, 'Uncaught')
|
1076
|
+
ensure
|
1077
|
+
output = $stdout.string
|
1078
|
+
$stdout = orig_stdout
|
1079
|
+
end
|
1080
|
+
else
|
1081
|
+
message = "Error: unknown objectGroup: #{group}"
|
1082
|
+
end
|
1083
|
+
else
|
1084
|
+
result = Exception.new("Error: Can not evaluate on this frame")
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
res[:result] = evaluate_result(result)
|
1088
|
+
event! :protocol_result, :evaluate, req, message: message, response: res, output: output
|
1089
|
+
when :scope
|
1090
|
+
fid = args.shift
|
1091
|
+
frame = @target_frames[fid]
|
1092
|
+
if b = frame.binding
|
1093
|
+
vars = b.local_variables.map{|name|
|
1094
|
+
v = b.local_variable_get(name)
|
1095
|
+
variable(name, v)
|
1096
|
+
}
|
1097
|
+
special_local_variables frame do |name, val|
|
1098
|
+
vars.unshift variable(name, val)
|
1099
|
+
end
|
1100
|
+
vars.unshift variable('%self', b.receiver)
|
1101
|
+
elsif lvars = frame.local_variables
|
1102
|
+
vars = lvars.map{|var, val|
|
1103
|
+
variable(var, val)
|
1104
|
+
}
|
1105
|
+
else
|
1106
|
+
vars = [variable('%self', frame.self)]
|
1107
|
+
special_local_variables frame do |name, val|
|
1108
|
+
vars.unshift variable(name, val)
|
1109
|
+
end
|
1110
|
+
end
|
1111
|
+
event! :protocol_result, :scope, req, vars
|
1112
|
+
when :properties
|
1113
|
+
oid = args.shift
|
1114
|
+
result = []
|
1115
|
+
prop = []
|
1116
|
+
|
1117
|
+
if obj = @obj_map[oid]
|
1118
|
+
case obj
|
1119
|
+
when Array
|
1120
|
+
result = obj.map.with_index{|o, i|
|
1121
|
+
variable i.to_s, o
|
1122
|
+
}
|
1123
|
+
when Hash
|
1124
|
+
result = obj.map{|k, v|
|
1125
|
+
variable(k, v)
|
1126
|
+
}
|
1127
|
+
when Struct
|
1128
|
+
result = obj.members.map{|m|
|
1129
|
+
variable(m, obj[m])
|
1130
|
+
}
|
1131
|
+
when String
|
1132
|
+
prop = [
|
1133
|
+
internalProperty('#length', obj.length),
|
1134
|
+
internalProperty('#encoding', obj.encoding)
|
1135
|
+
]
|
1136
|
+
when Class, Module
|
1137
|
+
result = obj.instance_variables.map{|iv|
|
1138
|
+
variable(iv, obj.instance_variable_get(iv))
|
1139
|
+
}
|
1140
|
+
prop = [internalProperty('%ancestors', obj.ancestors[1..])]
|
1141
|
+
when Range
|
1142
|
+
prop = [
|
1143
|
+
internalProperty('#begin', obj.begin),
|
1144
|
+
internalProperty('#end', obj.end),
|
1145
|
+
]
|
1146
|
+
end
|
1147
|
+
|
1148
|
+
result += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
|
1149
|
+
variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
|
1150
|
+
}
|
1151
|
+
prop += [internalProperty('#class', M_CLASS.bind_call(obj))]
|
1152
|
+
end
|
1153
|
+
event! :protocol_result, :properties, req, result: result, internalProperties: prop
|
1154
|
+
when :exception
|
1155
|
+
oid = args.shift
|
1156
|
+
exc = nil
|
1157
|
+
if obj = @obj_map[oid]
|
1158
|
+
exc = exceptionDetails obj, obj.to_s
|
1159
|
+
end
|
1160
|
+
event! :protocol_result, :exception, req, exceptionDetails: exc
|
1161
|
+
end
|
1162
|
+
end
|
1163
|
+
|
1164
|
+
def exceptionDetails exc, text
|
1165
|
+
frames = [
|
1166
|
+
{
|
1167
|
+
columnNumber: 0,
|
1168
|
+
functionName: 'eval',
|
1169
|
+
lineNumber: 0,
|
1170
|
+
url: ''
|
1171
|
+
}
|
1172
|
+
]
|
1173
|
+
exc.backtrace_locations&.each do |loc|
|
1174
|
+
break if loc.path == __FILE__
|
1175
|
+
path = loc.absolute_path || loc.path
|
1176
|
+
frames << {
|
1177
|
+
columnNumber: 0,
|
1178
|
+
functionName: loc.base_label,
|
1179
|
+
lineNumber: loc.lineno - 1,
|
1180
|
+
url: path
|
1181
|
+
}
|
1182
|
+
end
|
1183
|
+
{
|
1184
|
+
exceptionId: 1,
|
1185
|
+
text: text,
|
1186
|
+
lineNumber: 0,
|
1187
|
+
columnNumber: 0,
|
1188
|
+
exception: evaluate_result(exc),
|
1189
|
+
stackTrace: {
|
1190
|
+
callFrames: frames
|
1191
|
+
}
|
1192
|
+
}
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
def search_const b, expr
|
1196
|
+
cs = expr.delete_prefix('::').split('::')
|
1197
|
+
[Object, *b.eval('::Module.nesting')].reverse_each{|mod|
|
1198
|
+
if cs.all?{|c|
|
1199
|
+
if mod.const_defined?(c)
|
1200
|
+
begin
|
1201
|
+
mod = mod.const_get(c)
|
1202
|
+
rescue Exception
|
1203
|
+
false
|
1204
|
+
end
|
1205
|
+
else
|
1206
|
+
false
|
1207
|
+
end
|
1208
|
+
}
|
1209
|
+
# if-body
|
1210
|
+
return mod
|
1211
|
+
end
|
1212
|
+
}
|
1213
|
+
false
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
def evaluate_result r
|
1217
|
+
v = variable nil, r
|
1218
|
+
v[:value]
|
1219
|
+
end
|
1220
|
+
|
1221
|
+
def internalProperty name, obj
|
1222
|
+
v = variable name, obj
|
1223
|
+
v.delete :configurable
|
1224
|
+
v.delete :enumerable
|
1225
|
+
v
|
1226
|
+
end
|
1227
|
+
|
1228
|
+
def propertyDescriptor_ name, obj, type, description: nil, subtype: nil
|
1229
|
+
description = DEBUGGER__.safe_inspect(obj, short: true) if description.nil?
|
1230
|
+
oid = rand.to_s
|
1231
|
+
@obj_map[oid] = obj
|
1232
|
+
prop = {
|
1233
|
+
name: name,
|
1234
|
+
value: {
|
1235
|
+
type: type,
|
1236
|
+
description: description,
|
1237
|
+
value: obj,
|
1238
|
+
objectId: oid
|
1239
|
+
},
|
1240
|
+
configurable: true, # TODO: Change these parts because
|
1241
|
+
enumerable: true # they are not necessarily `true`.
|
1242
|
+
}
|
1243
|
+
|
1244
|
+
if type == 'object'
|
1245
|
+
v = prop[:value]
|
1246
|
+
v.delete :value
|
1247
|
+
v[:subtype] = subtype if subtype
|
1248
|
+
v[:className] = (klass = M_CLASS.bind_call(obj)).name || klass.to_s
|
1249
|
+
end
|
1250
|
+
prop
|
1251
|
+
end
|
1252
|
+
|
1253
|
+
def preview_ value, hash, overflow
|
1254
|
+
# The reason for not using "map" method is to prevent the object overriding it from causing bugs.
|
1255
|
+
# https://github.com/ruby/debug/issues/781
|
1256
|
+
props = []
|
1257
|
+
hash.each{|k, v|
|
1258
|
+
pd = propertyDescriptor k, v
|
1259
|
+
props << {
|
1260
|
+
name: pd[:name],
|
1261
|
+
type: pd[:value][:type],
|
1262
|
+
value: pd[:value][:description]
|
1263
|
+
}
|
1264
|
+
}
|
1265
|
+
{
|
1266
|
+
type: value[:type],
|
1267
|
+
subtype: value[:subtype],
|
1268
|
+
description: value[:description],
|
1269
|
+
overflow: overflow,
|
1270
|
+
properties: props
|
1271
|
+
}
|
1272
|
+
end
|
1273
|
+
|
1274
|
+
def variable name, obj
|
1275
|
+
pd = propertyDescriptor name, obj
|
1276
|
+
case obj
|
1277
|
+
when Array
|
1278
|
+
pd[:value][:preview] = preview name, obj
|
1279
|
+
obj.each_with_index{|item, idx|
|
1280
|
+
if valuePreview = preview(idx.to_s, item)
|
1281
|
+
pd[:value][:preview][:properties][idx][:valuePreview] = valuePreview
|
1282
|
+
end
|
1283
|
+
}
|
1284
|
+
when Hash
|
1285
|
+
pd[:value][:preview] = preview name, obj
|
1286
|
+
obj.each_with_index{|item, idx|
|
1287
|
+
key, val = item
|
1288
|
+
if valuePreview = preview(key, val)
|
1289
|
+
pd[:value][:preview][:properties][idx][:valuePreview] = valuePreview
|
1290
|
+
end
|
1291
|
+
}
|
1292
|
+
end
|
1293
|
+
pd
|
1294
|
+
end
|
1295
|
+
|
1296
|
+
def preview name, obj
|
1297
|
+
case obj
|
1298
|
+
when Array
|
1299
|
+
pd = propertyDescriptor name, obj
|
1300
|
+
overflow = false
|
1301
|
+
if obj.size > 100
|
1302
|
+
obj = obj[0..99]
|
1303
|
+
overflow = true
|
1304
|
+
end
|
1305
|
+
hash = obj.each_with_index.to_h{|o, i| [i.to_s, o]}
|
1306
|
+
preview_ pd[:value], hash, overflow
|
1307
|
+
when Hash
|
1308
|
+
pd = propertyDescriptor name, obj
|
1309
|
+
overflow = false
|
1310
|
+
if obj.size > 100
|
1311
|
+
obj = obj.to_a[0..99].to_h
|
1312
|
+
overflow = true
|
1313
|
+
end
|
1314
|
+
preview_ pd[:value], obj, overflow
|
1315
|
+
else
|
1316
|
+
nil
|
1317
|
+
end
|
1318
|
+
end
|
1319
|
+
|
1320
|
+
def propertyDescriptor name, obj
|
1321
|
+
case obj
|
1322
|
+
when Array
|
1323
|
+
propertyDescriptor_ name, obj, 'object', subtype: 'array'
|
1324
|
+
when Hash
|
1325
|
+
propertyDescriptor_ name, obj, 'object', subtype: 'map'
|
1326
|
+
when String
|
1327
|
+
propertyDescriptor_ name, obj, 'string', description: obj
|
1328
|
+
when TrueClass, FalseClass
|
1329
|
+
propertyDescriptor_ name, obj, 'boolean'
|
1330
|
+
when Symbol
|
1331
|
+
propertyDescriptor_ name, obj, 'symbol'
|
1332
|
+
when Integer, Float
|
1333
|
+
propertyDescriptor_ name, obj, 'number'
|
1334
|
+
when Exception
|
1335
|
+
bt = ''
|
1336
|
+
if log = obj.backtrace_locations
|
1337
|
+
log.each do |loc|
|
1338
|
+
break if loc.path == __FILE__
|
1339
|
+
bt += " #{loc}\n"
|
1340
|
+
end
|
1341
|
+
end
|
1342
|
+
propertyDescriptor_ name, obj, 'object', description: "#{obj.inspect}\n#{bt}", subtype: 'error'
|
1343
|
+
else
|
1344
|
+
propertyDescriptor_ name, obj, 'object'
|
1345
|
+
end
|
1346
|
+
end
|
1347
|
+
end
|
1348
|
+
end
|