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.
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