debug 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,20 +4,178 @@ require 'json'
4
4
  require 'digest/sha1'
5
5
  require 'base64'
6
6
  require 'securerandom'
7
+ require 'stringio'
8
+ require 'open3'
9
+ require 'tmpdir'
7
10
 
8
11
  module DEBUGGER__
9
12
  module UI_CDP
10
13
  SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
11
14
 
12
- class WebSocket
15
+ class << self
16
+ def setup_chrome addr
17
+ return if CONFIG[:chrome_path] == ''
18
+
19
+ port, path, pid = run_new_chrome
20
+ begin
21
+ s = Socket.tcp '127.0.0.1', port
22
+ rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL
23
+ return
24
+ end
25
+
26
+ ws_client = WebSocketClient.new(s)
27
+ ws_client.handshake port, path
28
+ ws_client.send id: 1, method: 'Target.getTargets'
29
+
30
+ 4.times do
31
+ res = ws_client.extract_data
32
+ case
33
+ when res['id'] == 1 && target_info = res.dig('result', 'targetInfos')
34
+ page = target_info.find{|t| t['type'] == 'page'}
35
+ ws_client.send id: 2, method: 'Target.attachToTarget',
36
+ params: {
37
+ targetId: page['targetId'],
38
+ flatten: true
39
+ }
40
+ when res['id'] == 2
41
+ s_id = res.dig('result', 'sessionId')
42
+ sleep 0.1
43
+ ws_client.send sessionId: s_id, id: 1,
44
+ method: 'Page.navigate',
45
+ params: {
46
+ url: "devtools://devtools/bundled/inspector.html?ws=#{addr}"
47
+ }
48
+ end
49
+ end
50
+ pid
51
+ rescue Errno::ENOENT
52
+ nil
53
+ end
54
+
55
+ def get_chrome_path
56
+ return CONFIG[:chrome_path] if CONFIG[:chrome_path]
57
+
58
+ # The process to check OS is based on `selenium` project.
59
+ case RbConfig::CONFIG['host_os']
60
+ when /mswin|msys|mingw|cygwin|emc/
61
+ 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
62
+ when /darwin|mac os/
63
+ '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
64
+ when /linux/
65
+ 'google-chrome'
66
+ else
67
+ raise "Unsupported OS"
68
+ end
69
+ end
70
+
71
+ def run_new_chrome
72
+ dir = Dir.mktmpdir
73
+ # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting
74
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{get_chrome_path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
75
+ stdin.close
76
+ stdout.close
77
+
78
+ data = stderr.readpartial 4096
79
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
80
+ port = $1
81
+ path = $2
82
+ end
83
+ stderr.close
84
+
85
+ at_exit{
86
+ CONFIG[:skip_path] = [//] # skip all
87
+ FileUtils.rm_rf dir
88
+ }
89
+
90
+ [port, path, wait_thr.pid]
91
+ end
92
+ end
93
+
94
+ class WebSocketClient
13
95
  def initialize s
14
96
  @sock = s
15
97
  end
16
98
 
17
- def handshake
99
+ def handshake port, path
100
+ key = SecureRandom.hex(11)
101
+ @sock.print "GET #{path} HTTP/1.1\r\nHost: 127.0.0.1:#{port}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: #{key}==\r\n\r\n"
102
+ res = @sock.readpartial 4092
103
+ $stderr.puts '[>]' + res if SHOW_PROTOCOL
104
+
105
+ if res.match /^Sec-WebSocket-Accept: (.*)\r\n/
106
+ correct_key = Base64.strict_encode64 Digest::SHA1.digest "#{key}==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
107
+ raise "The Sec-WebSocket-Accept value: #{$1} is not valid" unless $1 == correct_key
108
+ else
109
+ raise "Unknown response: #{res}"
110
+ end
111
+ end
112
+
113
+ def send **msg
114
+ msg = JSON.generate(msg)
115
+ frame = []
116
+ fin = 0b10000000
117
+ opcode = 0b00000001
118
+ frame << fin + opcode
119
+
120
+ mask = 0b10000000 # A client must mask all frames in a WebSocket Protocol.
121
+ bytesize = msg.bytesize
122
+ if bytesize < 126
123
+ payload_len = bytesize
124
+ elsif bytesize < 2 ** 16
125
+ payload_len = 0b01111110
126
+ ex_payload_len = [bytesize].pack('n*').bytes
127
+ else
128
+ payload_len = 0b01111111
129
+ ex_payload_len = [bytesize].pack('Q>').bytes
130
+ end
131
+
132
+ frame << mask + payload_len
133
+ frame.push *ex_payload_len if ex_payload_len
134
+
135
+ frame.push *masking_key = 4.times.map{rand(1..255)}
136
+ masked = []
137
+ msg.bytes.each_with_index do |b, i|
138
+ masked << (b ^ masking_key[i % 4])
139
+ end
140
+
141
+ frame.push *masked
142
+ @sock.print frame.pack 'c*'
143
+ end
144
+
145
+ def extract_data
146
+ first_group = @sock.getbyte
147
+ fin = first_group & 0b10000000 != 128
148
+ raise 'Unsupported' if fin
149
+ opcode = first_group & 0b00001111
150
+ raise "Unsupported: #{opcode}" unless opcode == 1
151
+
152
+ second_group = @sock.getbyte
153
+ mask = second_group & 0b10000000 == 128
154
+ raise 'The server must not mask any frames' if mask
155
+ payload_len = second_group & 0b01111111
156
+ # TODO: Support other payload_lengths
157
+ if payload_len == 126
158
+ payload_len = @sock.read(2).unpack('n*')[0]
159
+ end
160
+
161
+ data = JSON.parse @sock.read payload_len
162
+ $stderr.puts '[>]' + data.inspect if SHOW_PROTOCOL
163
+ data
164
+ end
165
+ end
166
+
167
+ class Detach < StandardError
168
+ end
169
+
170
+ class WebSocketServer
171
+ def initialize s
172
+ @sock = s
173
+ end
174
+
175
+ def handshake
18
176
  req = @sock.readpartial 4096
19
177
  $stderr.puts '[>]' + req if SHOW_PROTOCOL
20
-
178
+
21
179
  if req.match /^Sec-WebSocket-Key: (.*)\r\n/
22
180
  accept = Base64.strict_encode64 Digest::SHA1.digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
23
181
  @sock.print "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
@@ -32,7 +190,7 @@ module DEBUGGER__
32
190
  fin = 0b10000000
33
191
  opcode = 0b00000001
34
192
  frame << fin + opcode
35
-
193
+
36
194
  mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
37
195
  bytesize = msg.bytesize
38
196
  if bytesize < 126
@@ -44,7 +202,7 @@ module DEBUGGER__
44
202
  payload_len = 0b01111111
45
203
  ex_payload_len = [bytesize].pack('Q>').bytes
46
204
  end
47
-
205
+
48
206
  frame << mask + payload_len
49
207
  frame.push *ex_payload_len if ex_payload_len
50
208
  frame.push *msg.bytes
@@ -55,7 +213,9 @@ module DEBUGGER__
55
213
  first_group = @sock.getbyte
56
214
  fin = first_group & 0b10000000 != 128
57
215
  raise 'Unsupported' if fin
216
+
58
217
  opcode = first_group & 0b00001111
218
+ raise Detach if opcode == 8
59
219
  raise "Unsupported: #{opcode}" unless opcode == 1
60
220
 
61
221
  second_group = @sock.getbyte
@@ -80,34 +240,40 @@ module DEBUGGER__
80
240
 
81
241
  def send_response req, **res
82
242
  if res.empty?
83
- @web_sock.send id: req['id'], result: {}
243
+ @ws_server.send id: req['id'], result: {}
84
244
  else
85
- @web_sock.send id: req['id'], result: res
245
+ @ws_server.send id: req['id'], result: res
86
246
  end
87
247
  end
88
248
 
249
+ def send_fail_response req, **res
250
+ @ws_server.send id: req['id'], error: res
251
+ end
252
+
89
253
  def send_event method, **params
90
254
  if params.empty?
91
- @web_sock.send method: method, params: {}
255
+ @ws_server.send method: method, params: {}
92
256
  else
93
- @web_sock.send method: method, params: params
257
+ @ws_server.send method: method, params: params
94
258
  end
95
259
  end
96
260
 
261
+ INVALID_REQUEST = -32600
262
+
97
263
  def process
98
- bps = []
264
+ bps = {}
99
265
  @src_map = {}
100
266
  loop do
101
- req = @web_sock.extract_data
267
+ req = @ws_server.extract_data
102
268
  $stderr.puts '[>]' + req.inspect if SHOW_PROTOCOL
103
269
 
104
270
  case req['method']
105
271
 
106
272
  ## boot/configuration
107
273
  when 'Page.getResourceTree'
108
- abs = File.absolute_path($0)
109
- src = File.read(abs)
110
- @src_map[abs] = src
274
+ path = File.absolute_path($0)
275
+ src = File.read(path)
276
+ @src_map[path] = src
111
277
  send_response req,
112
278
  frameTree: {
113
279
  frame: {
@@ -120,8 +286,8 @@ module DEBUGGER__
120
286
  ]
121
287
  }
122
288
  send_event 'Debugger.scriptParsed',
123
- scriptId: abs,
124
- url: "http://debuggee#{abs}",
289
+ scriptId: path,
290
+ url: "http://debuggee#{path}",
125
291
  startLine: 0,
126
292
  startColumn: 0,
127
293
  endLine: src.count("\n"),
@@ -141,7 +307,7 @@ module DEBUGGER__
141
307
  @q_msg << req
142
308
  when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
143
309
  'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
144
- 'Debugger.setBreakpointsActive', 'Runtime.releaseObjectGroup'
310
+ 'Runtime.releaseObjectGroup', 'Runtime.discardConsoleEntries', 'Log.clear'
145
311
  send_response req
146
312
 
147
313
  ## control
@@ -151,20 +317,52 @@ module DEBUGGER__
151
317
  send_response req
152
318
  send_event 'Debugger.resumed'
153
319
  when 'Debugger.stepOver'
154
- @q_msg << 'n'
155
- @q_msg << req
156
- send_response req
157
- send_event 'Debugger.resumed'
320
+ begin
321
+ @session.check_postmortem
322
+ @q_msg << 'n'
323
+ send_response req
324
+ send_event 'Debugger.resumed'
325
+ rescue PostmortemError
326
+ send_fail_response req,
327
+ code: INVALID_REQUEST,
328
+ message: "'stepOver' is not supported while postmortem mode"
329
+ ensure
330
+ @q_msg << req
331
+ end
158
332
  when 'Debugger.stepInto'
159
- @q_msg << 's'
160
- @q_msg << req
161
- send_response req
162
- send_event 'Debugger.resumed'
333
+ begin
334
+ @session.check_postmortem
335
+ @q_msg << 's'
336
+ send_response req
337
+ send_event 'Debugger.resumed'
338
+ rescue PostmortemError
339
+ send_fail_response req,
340
+ code: INVALID_REQUEST,
341
+ message: "'stepInto' is not supported while postmortem mode"
342
+ ensure
343
+ @q_msg << req
344
+ end
163
345
  when 'Debugger.stepOut'
164
- @q_msg << 'fin'
165
- @q_msg << req
346
+ begin
347
+ @session.check_postmortem
348
+ @q_msg << 'fin'
349
+ send_response req
350
+ send_event 'Debugger.resumed'
351
+ rescue PostmortemError
352
+ send_fail_response req,
353
+ code: INVALID_REQUEST,
354
+ message: "'stepOut' is not supported while postmortem mode"
355
+ ensure
356
+ @q_msg << req
357
+ end
358
+ when 'Debugger.setSkipAllPauses'
359
+ skip = req.dig('params', 'skip')
360
+ if skip
361
+ deactivate_bp
362
+ else
363
+ activate_bp bps
364
+ end
166
365
  send_response req
167
- send_event 'Debugger.resumed'
168
366
 
169
367
  # breakpoint
170
368
  when 'Debugger.getPossibleBreakpoints'
@@ -181,31 +379,72 @@ module DEBUGGER__
181
379
  ]
182
380
  when 'Debugger.setBreakpointByUrl'
183
381
  line = req.dig('params', 'lineNumber')
184
- path = req.dig('params', 'url').match('http://debuggee(.*)')[1]
185
- cond = req.dig('params', 'condition')
186
- src = get_source_code path
187
- end_line = src.count("\n")
188
- line = end_line if line > end_line
189
- if cond != ''
190
- bps << SESSION.add_line_breakpoint(path, line + 1, cond: cond)
382
+ url = req.dig('params', 'url')
383
+ locations = []
384
+ if url.match /http:\/\/debuggee(.*)/
385
+ path = $1
386
+ cond = req.dig('params', 'condition')
387
+ src = get_source_code path
388
+ end_line = src.count("\n")
389
+ line = end_line if line > end_line
390
+ b_id = "1:#{line}:#{path}"
391
+ if cond != ''
392
+ SESSION.add_line_breakpoint(path, line + 1, cond: cond)
393
+ else
394
+ SESSION.add_line_breakpoint(path, line + 1)
395
+ end
396
+ bps[b_id] = bps.size
397
+ locations << {scriptId: path, lineNumber: line}
191
398
  else
192
- bps << SESSION.add_line_breakpoint(path, line + 1)
399
+ b_id = "1:#{line}:#{url}"
193
400
  end
194
401
  send_response req,
195
- breakpointId: (bps.size - 1).to_s,
196
- locations: [
197
- scriptId: path,
198
- lineNumber: line
199
- ]
402
+ breakpointId: b_id,
403
+ locations: locations
200
404
  when 'Debugger.removeBreakpoint'
201
405
  b_id = req.dig('params', 'breakpointId')
202
- @q_msg << "del #{b_id}"
406
+ bps = del_bp bps, b_id
407
+ send_response req
408
+ when 'Debugger.setBreakpointsActive'
409
+ active = req.dig('params', 'active')
410
+ if active
411
+ activate_bp bps
412
+ else
413
+ deactivate_bp # TODO: Change this part because catch breakpoints should not be deactivated.
414
+ end
415
+ send_response req
416
+ when 'Debugger.setPauseOnExceptions'
417
+ state = req.dig('params', 'state')
418
+ ex = 'Exception'
419
+ case state
420
+ when 'none'
421
+ @q_msg << 'config postmortem = false'
422
+ bps = del_bp bps, ex
423
+ when 'uncaught'
424
+ @q_msg << 'config postmortem = true'
425
+ bps = del_bp bps, ex
426
+ when 'all'
427
+ @q_msg << 'config postmortem = false'
428
+ SESSION.add_catch_breakpoint ex
429
+ bps[ex] = bps.size
430
+ end
203
431
  send_response req
204
432
 
205
433
  when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
206
434
  @q_msg << req
207
435
  end
208
436
  end
437
+ rescue Detach
438
+ @q_msg << 'continue'
439
+ end
440
+
441
+ def del_bp bps, k
442
+ return bps unless idx = bps[k]
443
+
444
+ bps.delete k
445
+ bps.each_key{|i| bps[i] -= 1 if bps[i] > idx}
446
+ @q_msg << "del #{idx}"
447
+ bps
209
448
  end
210
449
 
211
450
  def get_source_code path
@@ -216,9 +455,32 @@ module DEBUGGER__
216
455
  src
217
456
  end
218
457
 
458
+ def activate_bp bps
459
+ bps.each_key{|k|
460
+ if k.match /^\d+:(\d+):(.*)/
461
+ line = $1
462
+ path = $2
463
+ SESSION.add_line_breakpoint(path, line.to_i + 1)
464
+ else
465
+ SESSION.add_catch_breakpoint 'Exception'
466
+ end
467
+ }
468
+ end
469
+
470
+ def deactivate_bp
471
+ @q_msg << 'del'
472
+ @q_ans << 'y'
473
+ end
474
+
475
+ def cleanup_reader
476
+ Process.kill :KILL, @chrome_pid if @chrome_pid
477
+ end
478
+
219
479
  ## Called by the SESSION thread
220
480
 
221
481
  def readline prompt
482
+ return 'c' unless @q_msg
483
+
222
484
  @q_msg.pop || 'kill!'
223
485
  end
224
486
 
@@ -226,6 +488,10 @@ module DEBUGGER__
226
488
  send_response req, **result
227
489
  end
228
490
 
491
+ def respond_fail req, **result
492
+ send_fail_response req, **result
493
+ end
494
+
229
495
  def fire_event event, **result
230
496
  if result.empty?
231
497
  send_event event
@@ -245,26 +511,48 @@ module DEBUGGER__
245
511
  end
246
512
 
247
513
  class Session
514
+ def fail_response req, **result
515
+ @ui.respond_fail req, result
516
+ return :retry
517
+ end
518
+
519
+ INVALID_PARAMS = -32602
520
+
248
521
  def process_protocol_request req
249
522
  case req['method']
250
523
  when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource'
251
524
  @tc << [:cdp, :backtrace, req]
252
525
  when 'Debugger.evaluateOnCallFrame'
253
- expr = req.dig('params', 'expression')
254
- @tc << [:cdp, :evaluate, req, expr]
526
+ frame_id = req.dig('params', 'callFrameId')
527
+ if fid = @frame_map[frame_id]
528
+ expr = req.dig('params', 'expression')
529
+ @tc << [:cdp, :evaluate, req, fid, expr]
530
+ else
531
+ fail_response req,
532
+ code: INVALID_PARAMS,
533
+ message: "'callFrameId' is an invalid"
534
+ end
255
535
  when 'Runtime.getProperties'
256
536
  oid = req.dig('params', 'objectId')
257
- case oid
258
- when /(\d?):local/
259
- @tc << [:cdp, :properties, req, $1.to_i]
260
- when /\d?:script/
261
- # TODO: Support a script type
262
- @ui.respond req
263
- return :retry
264
- when /\d?:global/
265
- # TODO: Support a global type
266
- @ui.respond req
267
- return :retry
537
+ if ref = @obj_map[oid]
538
+ case ref[0]
539
+ when 'local'
540
+ frame_id = ref[1]
541
+ fid = @frame_map[frame_id]
542
+ @tc << [:cdp, :scope, req, fid]
543
+ when 'properties'
544
+ @tc << [:cdp, :properties, req, oid]
545
+ when 'script', 'global'
546
+ # TODO: Support script and global types
547
+ @ui.respond req
548
+ return :retry
549
+ else
550
+ raise "Unknown type: #{ref.inspect}"
551
+ end
552
+ else
553
+ fail_response req,
554
+ code: INVALID_PARAMS,
555
+ message: "'objectId' is an invalid"
268
556
  end
269
557
  end
270
558
  end
@@ -274,7 +562,9 @@ module DEBUGGER__
274
562
 
275
563
  case type
276
564
  when :backtrace
277
- result[:callFrames].each do |frame|
565
+ result[:callFrames].each.with_index do |frame, i|
566
+ frame_id = frame[:callFrameId]
567
+ @frame_map[frame_id] = i
278
568
  s_id = frame.dig(:location, :scriptId)
279
569
  if File.exist?(s_id) && !@script_paths.include?(s_id)
280
570
  src = File.read(s_id)
@@ -289,13 +579,60 @@ module DEBUGGER__
289
579
  hash: src.hash
290
580
  @script_paths << s_id
291
581
  end
582
+
583
+ frame[:scopeChain].each {|s|
584
+ oid = s.dig(:object, :objectId)
585
+ @obj_map[oid] = [s[:type], frame_id]
586
+ }
587
+ end
588
+
589
+ if oid = result.dig(:data, :objectId)
590
+ @obj_map[oid] = ['properties']
292
591
  end
293
- result[:reason] = 'other'
294
592
  @ui.fire_event 'Debugger.paused', **result
295
593
  when :evaluate
594
+ message = result.delete :message
595
+ if message
596
+ fail_response req,
597
+ code: INVALID_PARAMS,
598
+ message: message
599
+ else
600
+ rs = result.dig(:response, :result)
601
+ [rs].each{|obj|
602
+ if oid = obj[:objectId]
603
+ @obj_map[oid] = ['properties']
604
+ end
605
+ }
606
+ @ui.respond req, **result[:response]
607
+
608
+ out = result[:output]
609
+ if out && !out.empty?
610
+ @ui.fire_event 'Runtime.consoleAPICalled',
611
+ type: 'log',
612
+ args: [
613
+ type: out.class,
614
+ value: out
615
+ ],
616
+ executionContextId: 1, # Change this number if something goes wrong.
617
+ timestamp: Time.now.to_f
618
+ end
619
+ end
620
+ when :scope
621
+ result.each{|obj|
622
+ if oid = obj.dig(:value, :objectId)
623
+ @obj_map[oid] = ['properties']
624
+ end
625
+ }
296
626
  @ui.respond req, result: result
297
627
  when :properties
298
- @ui.respond req, result: result
628
+ result.each_value{|v|
629
+ v.each{|obj|
630
+ if oid = obj.dig(:value, :objectId)
631
+ @obj_map[oid] = ['properties']
632
+ end
633
+ }
634
+ }
635
+ @ui.respond req, **result
299
636
  end
300
637
  end
301
638
  end
@@ -307,43 +644,49 @@ module DEBUGGER__
307
644
 
308
645
  case type
309
646
  when :backtrace
310
- event! :cdp_result, :backtrace, req, {
647
+ exception = nil
648
+ result = {
649
+ reason: 'other',
311
650
  callFrames: @target_frames.map.with_index{|frame, i|
651
+ exception = frame.raised_exception if frame == current_frame && frame.has_raised_exception
652
+
312
653
  path = frame.realpath || frame.path
313
654
  if path.match /<internal:(.*)>/
314
- abs = $1
315
- else
316
- abs = path
655
+ path = $1
317
656
  end
318
657
 
319
- local_scope = {
658
+ {
320
659
  callFrameId: SecureRandom.hex(16),
321
660
  functionName: frame.name,
661
+ functionLocation: {
662
+ scriptId: path,
663
+ lineNumber: 0
664
+ },
322
665
  location: {
323
- scriptId: abs,
666
+ scriptId: path,
324
667
  lineNumber: frame.location.lineno - 1 # The line number is 0-based.
325
668
  },
326
- url: "http://debuggee#{abs}",
669
+ url: "http://debuggee#{path}",
327
670
  scopeChain: [
328
671
  {
329
672
  type: 'local',
330
673
  object: {
331
674
  type: 'object',
332
- objectId: "#{i}:local"
675
+ objectId: rand.to_s
333
676
  }
334
677
  },
335
678
  {
336
679
  type: 'script',
337
680
  object: {
338
681
  type: 'object',
339
- objectId: "#{i}:script"
682
+ objectId: rand.to_s
340
683
  }
341
684
  },
342
685
  {
343
686
  type: 'global',
344
687
  object: {
345
688
  type: 'object',
346
- objectId: "#{i}:global"
689
+ objectId: rand.to_s
347
690
  }
348
691
  }
349
692
  ],
@@ -353,15 +696,83 @@ module DEBUGGER__
353
696
  }
354
697
  }
355
698
  }
699
+
700
+ if exception
701
+ result[:data] = evaluate_result exception
702
+ result[:reason] = 'exception'
703
+ end
704
+ event! :cdp_result, :backtrace, req, result
356
705
  when :evaluate
357
- expr = args.shift
358
- begin
359
- result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
360
- rescue Exception => e
361
- result = e
706
+ res = {}
707
+ fid, expr = args
708
+ frame = @target_frames[fid]
709
+ message = nil
710
+
711
+ if frame && (b = frame.binding)
712
+ b = b.dup
713
+ special_local_variables current_frame do |name, var|
714
+ b.local_variable_set(name, var) if /\%/ !~name
715
+ end
716
+
717
+ result = nil
718
+
719
+ case req.dig('params', 'objectGroup')
720
+ when 'popover'
721
+ case expr
722
+ # Chrome doesn't read instance variables
723
+ when /\A\$\S/
724
+ global_variables.each{|gvar|
725
+ if gvar.to_s == expr
726
+ result = eval(gvar.to_s)
727
+ break false
728
+ end
729
+ } and (message = "Error: Not defined global variable: #{expr.inspect}")
730
+ when /(\A[A-Z][a-zA-Z]*)/
731
+ unless result = search_const(b, $1)
732
+ message = "Error: Not defined constant: #{expr.inspect}"
733
+ end
734
+ else
735
+ begin
736
+ # try to check local variables
737
+ b.local_variable_defined?(expr) or raise NameError
738
+ result = b.local_variable_get(expr)
739
+ rescue NameError
740
+ # try to check method
741
+ if b.receiver.respond_to? expr, include_all: true
742
+ result = b.receiver.method(expr)
743
+ else
744
+ message = "Error: Can not evaluate: #{expr.inspect}"
745
+ end
746
+ end
747
+ end
748
+ else
749
+ begin
750
+ orig_stdout = $stdout
751
+ $stdout = StringIO.new
752
+ result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
753
+ rescue Exception => e
754
+ result = e
755
+ b = result.backtrace.map{|e| " #{e}\n"}
756
+ line = b.first.match('.*:(\d+):in .*')[1].to_i
757
+ res[:exceptionDetails] = {
758
+ exceptionId: 1,
759
+ text: 'Uncaught',
760
+ lineNumber: line - 1,
761
+ columnNumber: 0,
762
+ exception: evaluate_result(result),
763
+ }
764
+ ensure
765
+ output = $stdout.string
766
+ $stdout = orig_stdout
767
+ end
768
+ end
769
+ else
770
+ result = Exception.new("Error: Can not evaluate on this frame")
362
771
  end
363
- event! :cdp_result, :evaluate, req, evaluate_result(result)
364
- when :properties
772
+
773
+ res[:result] = evaluate_result(result)
774
+ event! :cdp_result, :evaluate, req, message: message, response: res, output: output
775
+ when :scope
365
776
  fid = args.shift
366
777
  frame = @target_frames[fid]
367
778
  if b = frame.binding
@@ -369,8 +780,9 @@ module DEBUGGER__
369
780
  v = b.local_variable_get(name)
370
781
  variable(name, v)
371
782
  }
372
- vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
373
- vars.unshift variable('%return', frame.return_value) if frame.has_return_value
783
+ special_local_variables frame do |name, val|
784
+ vars.unshift variable(name, val)
785
+ end
374
786
  vars.unshift variable('%self', b.receiver)
375
787
  elsif lvars = frame.local_variables
376
788
  vars = lvars.map{|var, val|
@@ -378,46 +790,131 @@ module DEBUGGER__
378
790
  }
379
791
  else
380
792
  vars = [variable('%self', frame.self)]
381
- vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
382
- vars.push variable('%return', frame.return_value) if frame.has_return_value
793
+ special_local_variables frame do |name, val|
794
+ vars.unshift variable(name, val)
795
+ end
796
+ end
797
+ event! :cdp_result, :scope, req, vars
798
+ when :properties
799
+ oid = args.shift
800
+ result = []
801
+ prop = []
802
+
803
+ if obj = @obj_map[oid]
804
+ case obj
805
+ when Array
806
+ result = obj.map.with_index{|o, i|
807
+ variable i.to_s, o
808
+ }
809
+ when Hash
810
+ result = obj.map{|k, v|
811
+ variable(k, v)
812
+ }
813
+ when Struct
814
+ result = obj.members.map{|m|
815
+ variable(m, obj[m])
816
+ }
817
+ when String
818
+ prop = [
819
+ property('#length', obj.length),
820
+ property('#encoding', obj.encoding)
821
+ ]
822
+ when Class, Module
823
+ result = obj.instance_variables.map{|iv|
824
+ variable(iv, obj.instance_variable_get(iv))
825
+ }
826
+ prop = [property('%ancestors', obj.ancestors[1..])]
827
+ when Range
828
+ prop = [
829
+ property('#begin', obj.begin),
830
+ property('#end', obj.end),
831
+ ]
832
+ end
833
+
834
+ result += obj.instance_variables.map{|iv|
835
+ variable(iv, obj.instance_variable_get(iv))
836
+ }
837
+ prop += [property('#class', obj.class)]
383
838
  end
384
- event! :cdp_result, :properties, req, vars
839
+ event! :cdp_result, :properties, req, result: result, internalProperties: prop
385
840
  end
386
841
  end
387
842
 
843
+ def search_const b, expr
844
+ cs = expr.split('::')
845
+ [Object, *b.eval('Module.nesting')].reverse_each{|mod|
846
+ if cs.all?{|c|
847
+ if mod.const_defined?(c)
848
+ mod = mod.const_get(c)
849
+ else
850
+ false
851
+ end
852
+ }
853
+ # if-body
854
+ return mod
855
+ end
856
+ }
857
+ false
858
+ end
859
+
388
860
  def evaluate_result r
389
861
  v = variable nil, r
390
862
  v[:value]
391
863
  end
392
864
 
393
- def variable_ name, obj, type, use_short: true
394
- {
865
+ def property name, obj
866
+ v = variable name, obj
867
+ v.delete :configurable
868
+ v.delete :enumerable
869
+ v
870
+ end
871
+
872
+ def variable_ name, obj, type, description: obj.inspect, subtype: nil
873
+ oid = rand.to_s
874
+ @obj_map[oid] = obj
875
+ prop = {
395
876
  name: name,
396
877
  value: {
397
878
  type: type,
398
- value: DEBUGGER__.short_inspect(obj, use_short)
879
+ description: description,
880
+ value: obj,
881
+ objectId: oid
399
882
  },
400
- configurable: true,
401
- enumerable: true
883
+ configurable: true, # TODO: Change these parts because
884
+ enumerable: true # they are not necessarily `true`.
402
885
  }
886
+
887
+ if type == 'object'
888
+ v = prop[:value]
889
+ v.delete :value
890
+ v[:subtype] = subtype if subtype
891
+ v[:className] = obj.class
892
+ end
893
+ prop
403
894
  end
404
895
 
405
896
  def variable name, obj
406
897
  case obj
407
- when Array, Hash, Range, NilClass, Time
408
- variable_ name, obj, 'object'
898
+ when Array
899
+ variable_ name, obj, 'object', description: "Array(#{obj.size})", subtype: 'array'
900
+ when Hash
901
+ variable_ name, obj, 'object', description: "Hash(#{obj.size})", subtype: 'map'
409
902
  when String
410
- variable_ name, obj, 'string', use_short: false
411
- when Class, Module, Struct
412
- variable_ name, obj, 'function'
903
+ variable_ name, obj, 'string', description: obj
904
+ when Class, Module, Struct, Range, Time, Method
905
+ variable_ name, obj, 'object'
413
906
  when TrueClass, FalseClass
414
907
  variable_ name, obj, 'boolean'
415
908
  when Symbol
416
909
  variable_ name, obj, 'symbol'
417
- when Float
418
- variable_ name, obj, 'number'
419
- when Integer
910
+ when Integer, Float
420
911
  variable_ name, obj, 'number'
912
+ when Exception
913
+ bt = nil
914
+ if log = obj.backtrace
915
+ bt = log.map{|e| " #{e}\n"}.join
916
+ end
917
+ variable_ name, obj, 'object', description: "#{obj.inspect}\n#{bt}", subtype: 'error'
421
918
  else
422
919
  variable_ name, obj, 'undefined'
423
920
  end