debug 1.3.4 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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