debug 1.4.0 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +210 -6
  3. data/Gemfile +2 -0
  4. data/LICENSE.txt +0 -0
  5. data/README.md +161 -85
  6. data/Rakefile +33 -10
  7. data/TODO.md +8 -8
  8. data/debug.gemspec +9 -7
  9. data/exe/rdbg +23 -4
  10. data/ext/debug/debug.c +111 -21
  11. data/ext/debug/extconf.rb +23 -0
  12. data/ext/debug/iseq_collector.c +2 -0
  13. data/lib/debug/abbrev_command.rb +77 -0
  14. data/lib/debug/breakpoint.rb +102 -74
  15. data/lib/debug/client.rb +46 -12
  16. data/lib/debug/color.rb +0 -0
  17. data/lib/debug/config.rb +129 -36
  18. data/lib/debug/console.rb +46 -40
  19. data/lib/debug/dap_custom/traceInspector.rb +336 -0
  20. data/lib/debug/frame_info.rb +40 -25
  21. data/lib/debug/irb_integration.rb +37 -0
  22. data/lib/debug/local.rb +17 -11
  23. data/lib/debug/open.rb +0 -0
  24. data/lib/debug/open_nonstop.rb +0 -0
  25. data/lib/debug/prelude.rb +3 -2
  26. data/lib/debug/server.rb +126 -56
  27. data/lib/debug/server_cdp.rb +673 -248
  28. data/lib/debug/server_dap.rb +497 -261
  29. data/lib/debug/session.rb +899 -441
  30. data/lib/debug/source_repository.rb +122 -49
  31. data/lib/debug/start.rb +1 -1
  32. data/lib/debug/thread_client.rb +460 -155
  33. data/lib/debug/tracer.rb +10 -16
  34. data/lib/debug/version.rb +1 -1
  35. data/lib/debug.rb +7 -2
  36. data/misc/README.md.erb +106 -56
  37. metadata +14 -24
  38. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
  39. data/.github/ISSUE_TEMPLATE/custom.md +0 -10
  40. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
  41. data/.github/pull_request_template.md +0 -9
  42. data/.github/workflows/ruby.yml +0 -34
  43. data/.gitignore +0 -12
  44. data/bin/console +0 -14
  45. data/bin/gentest +0 -30
  46. data/bin/setup +0 -8
  47. data/lib/debug/bp.vim +0 -68
@@ -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
- 4.times do
34
+ loop do
31
35
  res = ws_client.extract_data
32
- case
33
- when res['id'] == 1 && target_info = res.dig('result', 'targetInfos')
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 res['id'] == 2
45
+ when 2
41
46
  s_id = res.dig('result', 'sessionId')
42
- sleep 0.1
43
- ws_client.send sessionId: s_id, id: 1,
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
- def get_chrome_path
56
- return CONFIG[:chrome_path] if CONFIG[:chrome_path]
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
- 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
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 "Unsupported OS"
170
+ raise UnsupportedError
68
171
  end
172
+
173
+ [port, path, wait_thr.pid]
69
174
  end
70
175
 
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
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
- stderr.close
200
+ raise NotFoundChromeEndpointError
201
+ end
202
+ end
84
203
 
85
- at_exit{
86
- CONFIG[:skip_path] = [//] # skip all
87
- FileUtils.rm_rf dir
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
- [port, path, wait_thr.pid]
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
- @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"
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
- $stderr.puts '[>]' + res if SHOW_PROTOCOL
297
+ show_protocol :<, res
104
298
 
105
- if res.match /^Sec-WebSocket-Accept: (.*)\r\n/
106
- correct_key = Base64.strict_encode64 Digest::SHA1.digest "#{key}==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
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
- frame = []
309
+ show_protocol :>, msg
310
+ frame = Frame.new
116
311
  fin = 0b10000000
117
312
  opcode = 0b00000001
118
- frame << fin + opcode
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
- ex_payload_len = [bytesize].pack('n*').bytes
127
- else
322
+ frame.char mask + payload_len
323
+ frame.uint16 bytesize
324
+ elsif bytesize < 2 ** 64
128
325
  payload_len = 0b01111111
129
- ex_payload_len = [bytesize].pack('Q>').bytes
326
+ frame.char mask + payload_len
327
+ frame.ulonglong bytesize
328
+ else
329
+ raise 'Bytesize is too big.'
130
330
  end
131
331
 
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 = []
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
- masked << (b ^ masking_key[i % 4])
338
+ frame.char(b ^ masking_key[i % 4])
139
339
  end
140
340
 
141
- frame.push *masked
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
- data = JSON.parse @sock.read payload_len
162
- $stderr.puts '[>]' + data.inspect if SHOW_PROTOCOL
163
- data
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
- $stderr.puts '[>]' + req if SHOW_PROTOCOL
378
+ show_protocol '>', req
178
379
 
179
- if req.match /^Sec-WebSocket-Key: (.*)\r\n/
180
- accept = Base64.strict_encode64 Digest::SHA1.digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
181
- @sock.print "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
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
- frame = []
392
+ show_protocol :<, msg
393
+ frame = Frame.new
190
394
  fin = 0b10000000
191
395
  opcode = 0b00000001
192
- frame << fin + opcode
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
- ex_payload_len = [bytesize].pack('n*').bytes
201
- else
405
+ frame.char mask + payload_len
406
+ frame.uint16 bytesize
407
+ elsif bytesize < 2 ** 64
202
408
  payload_len = 0b01111111
203
- ex_payload_len = [bytesize].pack('Q>').bytes
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 << mask + payload_len
207
- frame.push *ex_payload_len if ex_payload_len
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
- JSON.parse unmasked.pack 'c*'
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
- if res.empty?
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
- if params.empty?
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 'Page.getResourceTree'
274
- path = File.absolute_path($0)
275
- src = File.read(path)
276
- @src_map[path] = src
277
- send_response req,
278
- frameTree: {
279
- frame: {
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://#{@addr}",
483
+ origin: "http://#{@local_addr.inspect_sockaddr}",
301
484
  name: ''
302
485
  }
303
- when 'Debugger.getScriptSource'
304
- s_id = req.dig('params', 'scriptId')
305
- src = get_source_code s_id
306
- send_response req, scriptSource: src
307
- @q_msg << req
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
- s_id = req.dig('params', 'start', 'scriptId')
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
- 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
559
+ if regexp = req.dig('params', 'urlRegex')
560
+ b_id = "1:#{line}:#{regexp}"
396
561
  bps[b_id] = bps.size
397
- locations << {scriptId: path, lineNumber: line}
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
- b_id = "1:#{line}:#{url}"
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 /^\d+:(\d+):(.*)/
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
- def readline prompt
482
- return 'c' unless @q_msg
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.getScriptSource'
524
- @tc << [:cdp, :backtrace, req]
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
- @tc << [:cdp, :evaluate, req, fid, expr]
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
- @tc << [:cdp, :scope, req, fid]
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
- @tc << [:cdp, :properties, req, oid]
545
- when 'script', 'global'
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 cdp_event args
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
- s_id = frame.dig(:location, :scriptId)
569
- if File.exist?(s_id) && !@script_paths.include?(s_id)
570
- src = File.read(s_id)
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
- scriptId: s_id,
573
- url: frame[:url],
574
- startLine: 0,
575
- startColumn: 0,
576
- endLine: src.count("\n"),
577
- endColumn: 0,
578
- executionContextId: @script_paths.size + 1,
579
- hash: src.hash
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
- if path.match /<internal:(.*)>/
655
- path = $1
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: path,
663
- lineNumber: 0
985
+ # scriptId: N, # filled by SESSION
986
+ lineNumber: lineno
664
987
  },
665
988
  location: {
666
- scriptId: path,
989
+ # scriptId: N, # filled by SESSION
667
990
  lineNumber: frame.location.lineno - 1 # The line number is 0-based.
668
991
  },
669
- url: "http://debuggee#{path}",
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! :cdp_result, :backtrace, req, result
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.binding)
712
- b = b.dup
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 req.dig('params', 'objectGroup')
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
- global_variables.each{|gvar|
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][a-zA-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.respond_to? expr, include_all: true
742
- result = b.receiver.method(expr)
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
- else
1068
+ when 'console', 'watch-group'
749
1069
  begin
750
1070
  orig_stdout = $stdout
751
1071
  $stdout = StringIO.new
752
- result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
1072
+ result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
753
1073
  rescue Exception => e
754
1074
  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
- }
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! :cdp_result, :evaluate, req, message: message, response: res, output: output
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! :cdp_result, :scope, req, vars
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
- property('#length', obj.length),
820
- property('#encoding', obj.encoding)
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 = [property('%ancestors', obj.ancestors[1..])]
1140
+ prop = [internalProperty('%ancestors', obj.ancestors[1..])]
827
1141
  when Range
828
1142
  prop = [
829
- property('#begin', obj.begin),
830
- property('#end', obj.end),
1143
+ internalProperty('#begin', obj.begin),
1144
+ internalProperty('#end', obj.end),
831
1145
  ]
832
1146
  end
833
1147
 
834
- result += obj.instance_variables.map{|iv|
835
- variable(iv, obj.instance_variable_get(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 += [property('#class', obj.class)]
1151
+ prop += [internalProperty('#class', M_CLASS.bind_call(obj))]
838
1152
  end
839
- event! :cdp_result, :properties, req, result: result, internalProperties: prop
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
- mod = mod.const_get(c)
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 property name, obj
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 variable_ name, obj, type, description: obj.inspect, subtype: nil
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.class
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
- variable_ name, obj, 'object', description: "Array(#{obj.size})", subtype: 'array'
1323
+ propertyDescriptor_ name, obj, 'object', subtype: 'array'
900
1324
  when Hash
901
- variable_ name, obj, 'object', description: "Hash(#{obj.size})", subtype: 'map'
1325
+ propertyDescriptor_ name, obj, 'object', subtype: 'map'
902
1326
  when String
903
- variable_ name, obj, 'string', description: obj
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
- variable_ name, obj, 'boolean'
1329
+ propertyDescriptor_ name, obj, 'boolean'
908
1330
  when Symbol
909
- variable_ name, obj, 'symbol'
1331
+ propertyDescriptor_ name, obj, 'symbol'
910
1332
  when Integer, Float
911
- variable_ name, obj, 'number'
1333
+ propertyDescriptor_ name, obj, 'number'
912
1334
  when Exception
913
- bt = nil
914
- if log = obj.backtrace
915
- bt = log.map{|e| " #{e}\n"}.join
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
- variable_ name, obj, 'object', description: "#{obj.inspect}\n#{bt}", subtype: 'error'
1342
+ propertyDescriptor_ name, obj, 'object', description: "#{obj.inspect}\n#{bt}", subtype: 'error'
918
1343
  else
919
- variable_ name, obj, 'undefined'
1344
+ propertyDescriptor_ name, obj, 'object'
920
1345
  end
921
1346
  end
922
1347
  end