debug 1.4.0 → 1.9.2
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/CONTRIBUTING.md +210 -6
- data/Gemfile +2 -0
- data/LICENSE.txt +0 -0
- data/README.md +161 -85
- data/Rakefile +33 -10
- data/TODO.md +8 -8
- data/debug.gemspec +9 -7
- data/exe/rdbg +23 -4
- data/ext/debug/debug.c +111 -21
- data/ext/debug/extconf.rb +23 -0
- data/ext/debug/iseq_collector.c +2 -0
- data/lib/debug/abbrev_command.rb +77 -0
- data/lib/debug/breakpoint.rb +102 -74
- data/lib/debug/client.rb +46 -12
- data/lib/debug/color.rb +0 -0
- data/lib/debug/config.rb +129 -36
- data/lib/debug/console.rb +46 -40
- data/lib/debug/dap_custom/traceInspector.rb +336 -0
- data/lib/debug/frame_info.rb +40 -25
- data/lib/debug/irb_integration.rb +37 -0
- data/lib/debug/local.rb +17 -11
- data/lib/debug/open.rb +0 -0
- data/lib/debug/open_nonstop.rb +0 -0
- data/lib/debug/prelude.rb +3 -2
- data/lib/debug/server.rb +126 -56
- data/lib/debug/server_cdp.rb +673 -248
- data/lib/debug/server_dap.rb +497 -261
- data/lib/debug/session.rb +899 -441
- data/lib/debug/source_repository.rb +122 -49
- data/lib/debug/start.rb +1 -1
- data/lib/debug/thread_client.rb +460 -155
- data/lib/debug/tracer.rb +10 -16
- data/lib/debug/version.rb +1 -1
- data/lib/debug.rb +7 -2
- data/misc/README.md.erb +106 -56
- metadata +14 -24
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
- data/.github/ISSUE_TEMPLATE/custom.md +0 -10
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
- data/.github/pull_request_template.md +0 -9
- data/.github/workflows/ruby.yml +0 -34
- data/.gitignore +0 -12
- data/bin/console +0 -14
- data/bin/gentest +0 -30
- data/bin/setup +0 -8
- data/lib/debug/bp.vim +0 -68
data/lib/debug/server_cdp.rb
CHANGED
@@ -2,18 +2,22 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
require 'digest/sha1'
|
5
|
-
require 'base64'
|
6
5
|
require 'securerandom'
|
7
6
|
require 'stringio'
|
8
7
|
require 'open3'
|
9
8
|
require 'tmpdir'
|
9
|
+
require 'tempfile'
|
10
|
+
require 'timeout'
|
10
11
|
|
11
12
|
module DEBUGGER__
|
12
13
|
module UI_CDP
|
13
14
|
SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
|
14
15
|
|
16
|
+
class UnsupportedError < StandardError; end
|
17
|
+
class NotFoundChromeEndpointError < StandardError; end
|
18
|
+
|
15
19
|
class << self
|
16
|
-
def setup_chrome addr
|
20
|
+
def setup_chrome addr, uuid
|
17
21
|
return if CONFIG[:chrome_path] == ''
|
18
22
|
|
19
23
|
port, path, pid = run_new_chrome
|
@@ -27,83 +31,273 @@ module DEBUGGER__
|
|
27
31
|
ws_client.handshake port, path
|
28
32
|
ws_client.send id: 1, method: 'Target.getTargets'
|
29
33
|
|
30
|
-
|
34
|
+
loop do
|
31
35
|
res = ws_client.extract_data
|
32
|
-
case
|
33
|
-
when
|
36
|
+
case res['id']
|
37
|
+
when 1
|
38
|
+
target_info = res.dig('result', 'targetInfos')
|
34
39
|
page = target_info.find{|t| t['type'] == 'page'}
|
35
40
|
ws_client.send id: 2, method: 'Target.attachToTarget',
|
36
41
|
params: {
|
37
42
|
targetId: page['targetId'],
|
38
43
|
flatten: true
|
39
44
|
}
|
40
|
-
when
|
45
|
+
when 2
|
41
46
|
s_id = res.dig('result', 'sessionId')
|
42
|
-
|
43
|
-
ws_client.send sessionId: s_id, id:
|
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,
|
44
59
|
method: 'Page.navigate',
|
45
60
|
params: {
|
46
|
-
url: "devtools://devtools/bundled/inspector.html?ws=#{addr}"
|
61
|
+
url: "devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&noJavaScriptCompletion=true&ws=#{addr}/#{uuid}",
|
62
|
+
frameId: f_id
|
47
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
|
48
77
|
end
|
49
78
|
end
|
50
79
|
pid
|
51
|
-
rescue Errno::ENOENT
|
80
|
+
rescue Errno::ENOENT, UnsupportedError, NotFoundChromeEndpointError
|
52
81
|
nil
|
53
82
|
end
|
54
83
|
|
55
|
-
|
56
|
-
|
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
|
57
92
|
|
58
93
|
# The process to check OS is based on `selenium` project.
|
59
94
|
case RbConfig::CONFIG['host_os']
|
60
95
|
when /mswin|msys|mingw|cygwin|emc/
|
61
|
-
|
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
|
+
}
|
62
124
|
when /darwin|mac os/
|
63
|
-
'/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
|
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
|
+
}
|
64
142
|
when /linux/
|
65
|
-
'google-chrome'
|
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
|
+
}
|
66
169
|
else
|
67
|
-
raise
|
170
|
+
raise UnsupportedError
|
68
171
|
end
|
172
|
+
|
173
|
+
[port, path, wait_thr.pid]
|
69
174
|
end
|
70
175
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
82
199
|
end
|
83
|
-
|
200
|
+
raise NotFoundChromeEndpointError
|
201
|
+
end
|
202
|
+
end
|
84
203
|
|
85
|
-
|
86
|
-
|
87
|
-
|
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"
|
88
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
|
89
240
|
|
90
|
-
|
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
|
91
281
|
end
|
92
282
|
end
|
93
283
|
|
94
284
|
class WebSocketClient
|
285
|
+
include WebSocketUtils
|
286
|
+
|
95
287
|
def initialize s
|
96
288
|
@sock = s
|
97
289
|
end
|
98
290
|
|
99
291
|
def handshake port, path
|
100
292
|
key = SecureRandom.hex(11)
|
101
|
-
|
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
|
102
296
|
res = @sock.readpartial 4092
|
103
|
-
|
297
|
+
show_protocol :<, res
|
104
298
|
|
105
|
-
if res.match
|
106
|
-
correct_key =
|
299
|
+
if res.match(/^Sec-WebSocket-Accept: (.*)\r\n/)
|
300
|
+
correct_key = Digest::SHA1.base64digest "#{key}==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
107
301
|
raise "The Sec-WebSocket-Accept value: #{$1} is not valid" unless $1 == correct_key
|
108
302
|
else
|
109
303
|
raise "Unknown response: #{res}"
|
@@ -112,34 +306,39 @@ module DEBUGGER__
|
|
112
306
|
|
113
307
|
def send **msg
|
114
308
|
msg = JSON.generate(msg)
|
115
|
-
|
309
|
+
show_protocol :>, msg
|
310
|
+
frame = Frame.new
|
116
311
|
fin = 0b10000000
|
117
312
|
opcode = 0b00000001
|
118
|
-
frame
|
313
|
+
frame.char fin + opcode
|
119
314
|
|
120
315
|
mask = 0b10000000 # A client must mask all frames in a WebSocket Protocol.
|
121
316
|
bytesize = msg.bytesize
|
122
317
|
if bytesize < 126
|
123
318
|
payload_len = bytesize
|
319
|
+
frame.char mask + payload_len
|
124
320
|
elsif bytesize < 2 ** 16
|
125
321
|
payload_len = 0b01111110
|
126
|
-
|
127
|
-
|
322
|
+
frame.char mask + payload_len
|
323
|
+
frame.uint16 bytesize
|
324
|
+
elsif bytesize < 2 ** 64
|
128
325
|
payload_len = 0b01111111
|
129
|
-
|
326
|
+
frame.char mask + payload_len
|
327
|
+
frame.ulonglong bytesize
|
328
|
+
else
|
329
|
+
raise 'Bytesize is too big.'
|
130
330
|
end
|
131
331
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
332
|
+
masking_key = 4.times.map{
|
333
|
+
key = rand(1..255)
|
334
|
+
frame.char key
|
335
|
+
key
|
336
|
+
}
|
137
337
|
msg.bytes.each_with_index do |b, i|
|
138
|
-
|
338
|
+
frame.char(b ^ masking_key[i % 4])
|
139
339
|
end
|
140
340
|
|
141
|
-
frame.
|
142
|
-
@sock.print frame.pack 'c*'
|
341
|
+
@sock.print frame.b
|
143
342
|
end
|
144
343
|
|
145
344
|
def extract_data
|
@@ -158,9 +357,9 @@ module DEBUGGER__
|
|
158
357
|
payload_len = @sock.read(2).unpack('n*')[0]
|
159
358
|
end
|
160
359
|
|
161
|
-
|
162
|
-
|
163
|
-
|
360
|
+
msg = @sock.read payload_len
|
361
|
+
show_protocol :<, msg
|
362
|
+
JSON.parse msg
|
164
363
|
end
|
165
364
|
end
|
166
365
|
|
@@ -168,17 +367,21 @@ module DEBUGGER__
|
|
168
367
|
end
|
169
368
|
|
170
369
|
class WebSocketServer
|
370
|
+
include WebSocketUtils
|
371
|
+
|
171
372
|
def initialize s
|
172
373
|
@sock = s
|
173
374
|
end
|
174
375
|
|
175
376
|
def handshake
|
176
377
|
req = @sock.readpartial 4096
|
177
|
-
|
378
|
+
show_protocol '>', req
|
178
379
|
|
179
|
-
if req.match
|
180
|
-
accept =
|
181
|
-
|
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
|
182
385
|
else
|
183
386
|
"Unknown request: #{req}"
|
184
387
|
end
|
@@ -186,27 +389,31 @@ module DEBUGGER__
|
|
186
389
|
|
187
390
|
def send **msg
|
188
391
|
msg = JSON.generate(msg)
|
189
|
-
|
392
|
+
show_protocol :<, msg
|
393
|
+
frame = Frame.new
|
190
394
|
fin = 0b10000000
|
191
395
|
opcode = 0b00000001
|
192
|
-
frame
|
396
|
+
frame.char fin + opcode
|
193
397
|
|
194
398
|
mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
|
195
399
|
bytesize = msg.bytesize
|
196
400
|
if bytesize < 126
|
197
401
|
payload_len = bytesize
|
402
|
+
frame.char mask + payload_len
|
198
403
|
elsif bytesize < 2 ** 16
|
199
404
|
payload_len = 0b01111110
|
200
|
-
|
201
|
-
|
405
|
+
frame.char mask + payload_len
|
406
|
+
frame.uint16 bytesize
|
407
|
+
elsif bytesize < 2 ** 64
|
202
408
|
payload_len = 0b01111111
|
203
|
-
|
409
|
+
frame.char mask + payload_len
|
410
|
+
frame.ulonglong bytesize
|
411
|
+
else
|
412
|
+
raise 'Bytesize is too big.'
|
204
413
|
end
|
205
414
|
|
206
|
-
frame <<
|
207
|
-
frame.
|
208
|
-
frame.push *msg.bytes
|
209
|
-
@sock.print frame.pack 'c*'
|
415
|
+
frame << msg
|
416
|
+
@sock.print frame.b
|
210
417
|
end
|
211
418
|
|
212
419
|
def extract_data
|
@@ -234,16 +441,14 @@ module DEBUGGER__
|
|
234
441
|
masked = @sock.getbyte
|
235
442
|
unmasked << (masked ^ masking_key[n % 4])
|
236
443
|
end
|
237
|
-
|
444
|
+
msg = unmasked.pack 'c*'
|
445
|
+
show_protocol :>, msg
|
446
|
+
JSON.parse msg
|
238
447
|
end
|
239
448
|
end
|
240
449
|
|
241
450
|
def send_response req, **res
|
242
|
-
|
243
|
-
@ws_server.send id: req['id'], result: {}
|
244
|
-
else
|
245
|
-
@ws_server.send id: req['id'], result: res
|
246
|
-
end
|
451
|
+
@ws_server.send id: req['id'], result: res
|
247
452
|
end
|
248
453
|
|
249
454
|
def send_fail_response req, **res
|
@@ -251,11 +456,7 @@ module DEBUGGER__
|
|
251
456
|
end
|
252
457
|
|
253
458
|
def send_event method, **params
|
254
|
-
|
255
|
-
@ws_server.send method: method, params: {}
|
256
|
-
else
|
257
|
-
@ws_server.send method: method, params: params
|
258
|
-
end
|
459
|
+
@ws_server.send method: method, params: params
|
259
460
|
end
|
260
461
|
|
261
462
|
INVALID_REQUEST = -32600
|
@@ -265,63 +466,46 @@ module DEBUGGER__
|
|
265
466
|
@src_map = {}
|
266
467
|
loop do
|
267
468
|
req = @ws_server.extract_data
|
268
|
-
$stderr.puts '[>]' + req.inspect if SHOW_PROTOCOL
|
269
469
|
|
270
470
|
case req['method']
|
271
471
|
|
272
472
|
## boot/configuration
|
273
|
-
when '
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
id: SecureRandom.hex(16),
|
281
|
-
loaderId: SecureRandom.hex(16),
|
282
|
-
url: 'http://debuggee/',
|
283
|
-
securityOrigin: 'http://debuggee',
|
284
|
-
mimeType: 'text/plain' },
|
285
|
-
resources: [
|
286
|
-
]
|
287
|
-
}
|
288
|
-
send_event 'Debugger.scriptParsed',
|
289
|
-
scriptId: path,
|
290
|
-
url: "http://debuggee#{path}",
|
291
|
-
startLine: 0,
|
292
|
-
startColumn: 0,
|
293
|
-
endLine: src.count("\n"),
|
294
|
-
endColumn: 0,
|
295
|
-
executionContextId: 1,
|
296
|
-
hash: src.hash
|
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
|
297
480
|
send_event 'Runtime.executionContextCreated',
|
298
481
|
context: {
|
299
482
|
id: SecureRandom.hex(16),
|
300
|
-
origin: "http://#{@
|
483
|
+
origin: "http://#{@local_addr.inspect_sockaddr}",
|
301
484
|
name: ''
|
302
485
|
}
|
303
|
-
when '
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
486
|
+
when 'Runtime.getIsolateId'
|
487
|
+
send_response req,
|
488
|
+
id: SecureRandom.hex
|
489
|
+
when 'Runtime.terminateExecution'
|
490
|
+
send_response req
|
491
|
+
exit
|
308
492
|
when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
|
309
493
|
'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
|
310
|
-
'Runtime.releaseObjectGroup', 'Runtime.discardConsoleEntries', 'Log.clear'
|
494
|
+
'Runtime.releaseObjectGroup', 'Runtime.discardConsoleEntries', 'Log.clear', 'Runtime.runIfWaitingForDebugger'
|
311
495
|
send_response req
|
312
496
|
|
313
497
|
## control
|
314
498
|
when 'Debugger.resume'
|
315
|
-
@q_msg << 'c'
|
316
|
-
@q_msg << req
|
317
499
|
send_response req
|
318
500
|
send_event 'Debugger.resumed'
|
501
|
+
@q_msg << 'c'
|
502
|
+
@q_msg << req
|
319
503
|
when 'Debugger.stepOver'
|
320
504
|
begin
|
321
505
|
@session.check_postmortem
|
322
|
-
@q_msg << 'n'
|
323
506
|
send_response req
|
324
507
|
send_event 'Debugger.resumed'
|
508
|
+
@q_msg << 'n'
|
325
509
|
rescue PostmortemError
|
326
510
|
send_fail_response req,
|
327
511
|
code: INVALID_REQUEST,
|
@@ -332,9 +516,9 @@ module DEBUGGER__
|
|
332
516
|
when 'Debugger.stepInto'
|
333
517
|
begin
|
334
518
|
@session.check_postmortem
|
335
|
-
@q_msg << 's'
|
336
519
|
send_response req
|
337
520
|
send_event 'Debugger.resumed'
|
521
|
+
@q_msg << 's'
|
338
522
|
rescue PostmortemError
|
339
523
|
send_fail_response req,
|
340
524
|
code: INVALID_REQUEST,
|
@@ -345,9 +529,9 @@ module DEBUGGER__
|
|
345
529
|
when 'Debugger.stepOut'
|
346
530
|
begin
|
347
531
|
@session.check_postmortem
|
348
|
-
@q_msg << 'fin'
|
349
532
|
send_response req
|
350
533
|
send_event 'Debugger.resumed'
|
534
|
+
@q_msg << 'fin'
|
351
535
|
rescue PostmortemError
|
352
536
|
send_fail_response req,
|
353
537
|
code: INVALID_REQUEST,
|
@@ -363,44 +547,42 @@ module DEBUGGER__
|
|
363
547
|
activate_bp bps
|
364
548
|
end
|
365
549
|
send_response req
|
550
|
+
when 'Debugger.pause'
|
551
|
+
send_response req
|
552
|
+
Process.kill(UI_ServerBase::TRAP_SIGNAL, Process.pid)
|
366
553
|
|
367
554
|
# breakpoint
|
368
555
|
when 'Debugger.getPossibleBreakpoints'
|
369
|
-
|
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
|
374
|
-
send_response req,
|
375
|
-
locations: [
|
376
|
-
{ scriptId: s_id,
|
377
|
-
lineNumber: line,
|
378
|
-
}
|
379
|
-
]
|
556
|
+
@q_msg << req
|
380
557
|
when 'Debugger.setBreakpointByUrl'
|
381
558
|
line = req.dig('params', 'lineNumber')
|
382
|
-
|
383
|
-
|
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
|
559
|
+
if regexp = req.dig('params', 'urlRegex')
|
560
|
+
b_id = "1:#{line}:#{regexp}"
|
396
561
|
bps[b_id] = bps.size
|
397
|
-
|
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
|
398
576
|
else
|
399
|
-
|
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
|
400
585
|
end
|
401
|
-
send_response req,
|
402
|
-
breakpointId: b_id,
|
403
|
-
locations: locations
|
404
586
|
when 'Debugger.removeBreakpoint'
|
405
587
|
b_id = req.dig('params', 'breakpointId')
|
406
588
|
bps = del_bp bps, b_id
|
@@ -438,6 +620,24 @@ module DEBUGGER__
|
|
438
620
|
@q_msg << 'continue'
|
439
621
|
end
|
440
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
|
+
|
441
641
|
def del_bp bps, k
|
442
642
|
return bps unless idx = bps[k]
|
443
643
|
|
@@ -457,7 +657,7 @@ module DEBUGGER__
|
|
457
657
|
|
458
658
|
def activate_bp bps
|
459
659
|
bps.each_key{|k|
|
460
|
-
if k.match
|
660
|
+
if k.match(/^\d+:(\d+):(.*)/)
|
461
661
|
line = $1
|
462
662
|
path = $2
|
463
663
|
SESSION.add_line_breakpoint(path, line.to_i + 1)
|
@@ -473,78 +673,113 @@ module DEBUGGER__
|
|
473
673
|
end
|
474
674
|
|
475
675
|
def cleanup_reader
|
676
|
+
super
|
476
677
|
Process.kill :KILL, @chrome_pid if @chrome_pid
|
678
|
+
rescue Errno::ESRCH # continue if @chrome_pid process is not found
|
477
679
|
end
|
478
680
|
|
479
681
|
## Called by the SESSION thread
|
480
682
|
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
@q_msg.pop || 'kill!'
|
485
|
-
end
|
486
|
-
|
487
|
-
def respond req, **result
|
488
|
-
send_response req, **result
|
489
|
-
end
|
490
|
-
|
491
|
-
def respond_fail req, **result
|
492
|
-
send_fail_response req, **result
|
493
|
-
end
|
494
|
-
|
495
|
-
def fire_event event, **result
|
496
|
-
if result.empty?
|
497
|
-
send_event event
|
498
|
-
else
|
499
|
-
send_event event, **result
|
500
|
-
end
|
501
|
-
end
|
683
|
+
alias respond send_response
|
684
|
+
alias respond_fail send_fail_response
|
685
|
+
alias fire_event send_event
|
502
686
|
|
503
687
|
def sock skip: false
|
504
688
|
yield $stderr
|
505
689
|
end
|
506
690
|
|
507
|
-
def puts result
|
691
|
+
def puts result=''
|
508
692
|
# STDERR.puts "puts: #{result}"
|
509
693
|
# send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
|
510
694
|
end
|
511
695
|
end
|
512
696
|
|
513
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
|
+
|
514
722
|
def fail_response req, **result
|
515
|
-
@ui.respond_fail req, result
|
723
|
+
@ui.respond_fail req, **result
|
516
724
|
return :retry
|
517
725
|
end
|
518
726
|
|
519
727
|
INVALID_PARAMS = -32602
|
728
|
+
INTERNAL_ERROR = -32603
|
520
729
|
|
521
730
|
def process_protocol_request req
|
522
731
|
case req['method']
|
523
|
-
when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.
|
524
|
-
|
732
|
+
when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.enable'
|
733
|
+
request_tc [:cdp, :backtrace, req]
|
525
734
|
when 'Debugger.evaluateOnCallFrame'
|
526
735
|
frame_id = req.dig('params', 'callFrameId')
|
736
|
+
group = req.dig('params', 'objectGroup')
|
527
737
|
if fid = @frame_map[frame_id]
|
528
738
|
expr = req.dig('params', 'expression')
|
529
|
-
|
739
|
+
request_tc [:cdp, :evaluate, req, fid, expr, group]
|
530
740
|
else
|
531
741
|
fail_response req,
|
532
742
|
code: INVALID_PARAMS,
|
533
743
|
message: "'callFrameId' is an invalid"
|
534
744
|
end
|
535
|
-
when 'Runtime.getProperties'
|
536
|
-
oid = req.dig('params', 'objectId')
|
745
|
+
when 'Runtime.getProperties', 'Runtime.getExceptionDetails'
|
746
|
+
oid = req.dig('params', 'objectId') || req.dig('params', 'errorObjectId')
|
537
747
|
if ref = @obj_map[oid]
|
538
748
|
case ref[0]
|
539
749
|
when 'local'
|
540
750
|
frame_id = ref[1]
|
541
751
|
fid = @frame_map[frame_id]
|
542
|
-
|
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
|
543
776
|
when 'properties'
|
544
|
-
|
545
|
-
when '
|
777
|
+
request_tc [:cdp, :properties, req, oid]
|
778
|
+
when 'exception'
|
779
|
+
request_tc [:cdp, :exception, req, oid]
|
780
|
+
when 'script'
|
546
781
|
# TODO: Support script and global types
|
547
|
-
@ui.respond req
|
782
|
+
@ui.respond req, result: []
|
548
783
|
return :retry
|
549
784
|
else
|
550
785
|
raise "Unknown type: #{ref.inspect}"
|
@@ -554,10 +789,54 @@ module DEBUGGER__
|
|
554
789
|
code: INVALID_PARAMS,
|
555
790
|
message: "'objectId' is an invalid"
|
556
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
|
557
836
|
end
|
558
837
|
end
|
559
838
|
|
560
|
-
def
|
839
|
+
def process_protocol_result args
|
561
840
|
type, req, result = args
|
562
841
|
|
563
842
|
case type
|
@@ -565,20 +844,29 @@ module DEBUGGER__
|
|
565
844
|
result[:callFrames].each.with_index do |frame, i|
|
566
845
|
frame_id = frame[:callFrameId]
|
567
846
|
@frame_map[frame_id] = i
|
568
|
-
|
569
|
-
|
570
|
-
|
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
|
571
858
|
@ui.fire_event 'Debugger.scriptParsed',
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
@script_paths << s_id
|
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
|
581
867
|
end
|
868
|
+
frame[:location][:scriptId] = s_id
|
869
|
+
frame[:functionLocation][:scriptId] = s_id
|
582
870
|
|
583
871
|
frame[:scopeChain].each {|s|
|
584
872
|
oid = s.dig(:object, :objectId)
|
@@ -597,6 +885,36 @@ module DEBUGGER__
|
|
597
885
|
code: INVALID_PARAMS,
|
598
886
|
message: message
|
599
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
|
600
918
|
rs = result.dig(:response, :result)
|
601
919
|
[rs].each{|obj|
|
602
920
|
if oid = obj[:objectId]
|
@@ -633,6 +951,8 @@ module DEBUGGER__
|
|
633
951
|
}
|
634
952
|
}
|
635
953
|
@ui.respond req, **result
|
954
|
+
when :exception
|
955
|
+
@ui.respond req, **result
|
636
956
|
end
|
637
957
|
end
|
638
958
|
end
|
@@ -651,22 +971,25 @@ module DEBUGGER__
|
|
651
971
|
exception = frame.raised_exception if frame == current_frame && frame.has_raised_exception
|
652
972
|
|
653
973
|
path = frame.realpath || frame.path
|
654
|
-
|
655
|
-
|
974
|
+
|
975
|
+
if frame.iseq.nil?
|
976
|
+
lineno = 0
|
977
|
+
else
|
978
|
+
lineno = frame.iseq.first_line - 1
|
656
979
|
end
|
657
980
|
|
658
981
|
{
|
659
982
|
callFrameId: SecureRandom.hex(16),
|
660
983
|
functionName: frame.name,
|
661
984
|
functionLocation: {
|
662
|
-
scriptId:
|
663
|
-
lineNumber:
|
985
|
+
# scriptId: N, # filled by SESSION
|
986
|
+
lineNumber: lineno
|
664
987
|
},
|
665
988
|
location: {
|
666
|
-
scriptId:
|
989
|
+
# scriptId: N, # filled by SESSION
|
667
990
|
lineNumber: frame.location.lineno - 1 # The line number is 0-based.
|
668
991
|
},
|
669
|
-
url:
|
992
|
+
url: path,
|
670
993
|
scopeChain: [
|
671
994
|
{
|
672
995
|
type: 'local',
|
@@ -701,77 +1024,68 @@ module DEBUGGER__
|
|
701
1024
|
result[:data] = evaluate_result exception
|
702
1025
|
result[:reason] = 'exception'
|
703
1026
|
end
|
704
|
-
event! :
|
1027
|
+
event! :protocol_result, :backtrace, req, result
|
705
1028
|
when :evaluate
|
706
1029
|
res = {}
|
707
|
-
fid, expr = args
|
1030
|
+
fid, expr, group = args
|
708
1031
|
frame = @target_frames[fid]
|
709
1032
|
message = nil
|
710
1033
|
|
711
|
-
if frame && (b = frame.
|
712
|
-
|
713
|
-
special_local_variables current_frame do |name, var|
|
1034
|
+
if frame && (b = frame.eval_binding)
|
1035
|
+
special_local_variables frame do |name, var|
|
714
1036
|
b.local_variable_set(name, var) if /\%/ !~name
|
715
1037
|
end
|
716
1038
|
|
717
1039
|
result = nil
|
718
1040
|
|
719
|
-
case
|
1041
|
+
case group
|
720
1042
|
when 'popover'
|
721
1043
|
case expr
|
722
1044
|
# Chrome doesn't read instance variables
|
723
1045
|
when /\A\$\S/
|
724
|
-
|
1046
|
+
safe_global_variables.each{|gvar|
|
725
1047
|
if gvar.to_s == expr
|
726
1048
|
result = eval(gvar.to_s)
|
727
1049
|
break false
|
728
1050
|
end
|
729
1051
|
} and (message = "Error: Not defined global variable: #{expr.inspect}")
|
730
|
-
when /(\A[A-Z][
|
1052
|
+
when /(\A((::[A-Z]|[A-Z])\w*)+)/
|
731
1053
|
unless result = search_const(b, $1)
|
732
1054
|
message = "Error: Not defined constant: #{expr.inspect}"
|
733
1055
|
end
|
734
1056
|
else
|
735
1057
|
begin
|
736
|
-
# try to check local variables
|
737
|
-
b.local_variable_defined?(expr) or raise NameError
|
738
1058
|
result = b.local_variable_get(expr)
|
739
1059
|
rescue NameError
|
740
1060
|
# try to check method
|
741
|
-
if b.receiver
|
742
|
-
result = b.receiver
|
1061
|
+
if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true)
|
1062
|
+
result = M_METHOD.bind_call(b.receiver, expr)
|
743
1063
|
else
|
744
1064
|
message = "Error: Can not evaluate: #{expr.inspect}"
|
745
1065
|
end
|
746
1066
|
end
|
747
1067
|
end
|
748
|
-
|
1068
|
+
when 'console', 'watch-group'
|
749
1069
|
begin
|
750
1070
|
orig_stdout = $stdout
|
751
1071
|
$stdout = StringIO.new
|
752
|
-
result =
|
1072
|
+
result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
|
753
1073
|
rescue Exception => e
|
754
1074
|
result = e
|
755
|
-
|
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
|
-
}
|
1075
|
+
res[:exceptionDetails] = exceptionDetails(e, 'Uncaught')
|
764
1076
|
ensure
|
765
1077
|
output = $stdout.string
|
766
1078
|
$stdout = orig_stdout
|
767
1079
|
end
|
1080
|
+
else
|
1081
|
+
message = "Error: unknown objectGroup: #{group}"
|
768
1082
|
end
|
769
1083
|
else
|
770
1084
|
result = Exception.new("Error: Can not evaluate on this frame")
|
771
1085
|
end
|
772
1086
|
|
773
1087
|
res[:result] = evaluate_result(result)
|
774
|
-
event! :
|
1088
|
+
event! :protocol_result, :evaluate, req, message: message, response: res, output: output
|
775
1089
|
when :scope
|
776
1090
|
fid = args.shift
|
777
1091
|
frame = @target_frames[fid]
|
@@ -794,7 +1108,7 @@ module DEBUGGER__
|
|
794
1108
|
vars.unshift variable(name, val)
|
795
1109
|
end
|
796
1110
|
end
|
797
|
-
event! :
|
1111
|
+
event! :protocol_result, :scope, req, vars
|
798
1112
|
when :properties
|
799
1113
|
oid = args.shift
|
800
1114
|
result = []
|
@@ -816,36 +1130,78 @@ module DEBUGGER__
|
|
816
1130
|
}
|
817
1131
|
when String
|
818
1132
|
prop = [
|
819
|
-
|
820
|
-
|
1133
|
+
internalProperty('#length', obj.length),
|
1134
|
+
internalProperty('#encoding', obj.encoding)
|
821
1135
|
]
|
822
1136
|
when Class, Module
|
823
1137
|
result = obj.instance_variables.map{|iv|
|
824
1138
|
variable(iv, obj.instance_variable_get(iv))
|
825
1139
|
}
|
826
|
-
prop = [
|
1140
|
+
prop = [internalProperty('%ancestors', obj.ancestors[1..])]
|
827
1141
|
when Range
|
828
1142
|
prop = [
|
829
|
-
|
830
|
-
|
1143
|
+
internalProperty('#begin', obj.begin),
|
1144
|
+
internalProperty('#end', obj.end),
|
831
1145
|
]
|
832
1146
|
end
|
833
1147
|
|
834
|
-
result += obj.
|
835
|
-
variable(iv,
|
1148
|
+
result += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
|
1149
|
+
variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
|
836
1150
|
}
|
837
|
-
prop += [
|
1151
|
+
prop += [internalProperty('#class', M_CLASS.bind_call(obj))]
|
838
1152
|
end
|
839
|
-
event! :
|
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
|
+
}
|
840
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
|
+
}
|
841
1193
|
end
|
842
1194
|
|
843
1195
|
def search_const b, expr
|
844
|
-
cs = expr.split('::')
|
845
|
-
[Object, *b.eval('Module.nesting')].reverse_each{|mod|
|
1196
|
+
cs = expr.delete_prefix('::').split('::')
|
1197
|
+
[Object, *b.eval('::Module.nesting')].reverse_each{|mod|
|
846
1198
|
if cs.all?{|c|
|
847
1199
|
if mod.const_defined?(c)
|
848
|
-
|
1200
|
+
begin
|
1201
|
+
mod = mod.const_get(c)
|
1202
|
+
rescue Exception
|
1203
|
+
false
|
1204
|
+
end
|
849
1205
|
else
|
850
1206
|
false
|
851
1207
|
end
|
@@ -862,14 +1218,15 @@ module DEBUGGER__
|
|
862
1218
|
v[:value]
|
863
1219
|
end
|
864
1220
|
|
865
|
-
def
|
1221
|
+
def internalProperty name, obj
|
866
1222
|
v = variable name, obj
|
867
1223
|
v.delete :configurable
|
868
1224
|
v.delete :enumerable
|
869
1225
|
v
|
870
1226
|
end
|
871
1227
|
|
872
|
-
def
|
1228
|
+
def propertyDescriptor_ name, obj, type, description: nil, subtype: nil
|
1229
|
+
description = DEBUGGER__.safe_inspect(obj, short: true) if description.nil?
|
873
1230
|
oid = rand.to_s
|
874
1231
|
@obj_map[oid] = obj
|
875
1232
|
prop = {
|
@@ -888,35 +1245,103 @@ module DEBUGGER__
|
|
888
1245
|
v = prop[:value]
|
889
1246
|
v.delete :value
|
890
1247
|
v[:subtype] = subtype if subtype
|
891
|
-
v[:className] = obj.
|
1248
|
+
v[:className] = (klass = M_CLASS.bind_call(obj)).name || klass.to_s
|
892
1249
|
end
|
893
1250
|
prop
|
894
1251
|
end
|
895
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
|
+
|
896
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
|
897
1321
|
case obj
|
898
1322
|
when Array
|
899
|
-
|
1323
|
+
propertyDescriptor_ name, obj, 'object', subtype: 'array'
|
900
1324
|
when Hash
|
901
|
-
|
1325
|
+
propertyDescriptor_ name, obj, 'object', subtype: 'map'
|
902
1326
|
when String
|
903
|
-
|
904
|
-
when Class, Module, Struct, Range, Time, Method
|
905
|
-
variable_ name, obj, 'object'
|
1327
|
+
propertyDescriptor_ name, obj, 'string', description: obj
|
906
1328
|
when TrueClass, FalseClass
|
907
|
-
|
1329
|
+
propertyDescriptor_ name, obj, 'boolean'
|
908
1330
|
when Symbol
|
909
|
-
|
1331
|
+
propertyDescriptor_ name, obj, 'symbol'
|
910
1332
|
when Integer, Float
|
911
|
-
|
1333
|
+
propertyDescriptor_ name, obj, 'number'
|
912
1334
|
when Exception
|
913
|
-
bt =
|
914
|
-
if log = obj.
|
915
|
-
|
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
|
916
1341
|
end
|
917
|
-
|
1342
|
+
propertyDescriptor_ name, obj, 'object', description: "#{obj.inspect}\n#{bt}", subtype: 'error'
|
918
1343
|
else
|
919
|
-
|
1344
|
+
propertyDescriptor_ name, obj, 'object'
|
920
1345
|
end
|
921
1346
|
end
|
922
1347
|
end
|