debug 1.6.1 → 1.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,13 +7,18 @@ require 'securerandom'
7
7
  require 'stringio'
8
8
  require 'open3'
9
9
  require 'tmpdir'
10
+ require 'tempfile'
11
+ require 'timeout'
10
12
 
11
13
  module DEBUGGER__
12
14
  module UI_CDP
13
15
  SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
14
16
 
17
+ class UnsupportedError < StandardError; end
18
+ class NotFoundChromeEndpointError < StandardError; end
19
+
15
20
  class << self
16
- def setup_chrome addr
21
+ def setup_chrome addr, uuid
17
22
  return if CONFIG[:chrome_path] == ''
18
23
 
19
24
  port, path, pid = run_new_chrome
@@ -29,79 +34,217 @@ module DEBUGGER__
29
34
 
30
35
  loop do
31
36
  res = ws_client.extract_data
32
- case
33
- when res['id'] == 1 && target_info = res.dig('result', 'targetInfos')
37
+ case res['id']
38
+ when 1
39
+ target_info = res.dig('result', 'targetInfos')
34
40
  page = target_info.find{|t| t['type'] == 'page'}
35
41
  ws_client.send id: 2, method: 'Target.attachToTarget',
36
42
  params: {
37
43
  targetId: page['targetId'],
38
44
  flatten: true
39
45
  }
40
- when res['id'] == 2
46
+ when 2
41
47
  s_id = res.dig('result', 'sessionId')
48
+ # TODO: change id
49
+ ws_client.send sessionId: s_id, id: 100, method: 'Network.enable'
42
50
  ws_client.send sessionId: s_id, id: 3,
43
51
  method: 'Page.enable'
44
- when res['id'] == 3
52
+ when 3
45
53
  s_id = res['sessionId']
46
54
  ws_client.send sessionId: s_id, id: 4,
47
55
  method: 'Page.getFrameTree'
48
- when res['id'] == 4
56
+ when 4
49
57
  s_id = res['sessionId']
50
58
  f_id = res.dig('result', 'frameTree', 'frame', 'id')
51
59
  ws_client.send sessionId: s_id, id: 5,
52
60
  method: 'Page.navigate',
53
61
  params: {
54
- url: "devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&ws=#{addr}/#{SecureRandom.uuid}",
62
+ url: "devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&noJavaScriptCompletion=true&ws=#{addr}/#{uuid}",
55
63
  frameId: f_id
56
64
  }
57
- when res['method'] == 'Page.loadEventFired'
65
+ when 101
58
66
  break
67
+ else
68
+ if res['method'] == 'Network.webSocketWillSendHandshakeRequest'
69
+ s_id = res['sessionId']
70
+ # Display the console by entering ESC key
71
+ ws_client.send sessionId: s_id, id: 101, # TODO: change id
72
+ method:"Input.dispatchKeyEvent",
73
+ params: {
74
+ type:"keyDown",
75
+ windowsVirtualKeyCode:27 # ESC key
76
+ }
77
+ end
59
78
  end
60
79
  end
61
80
  pid
62
- rescue Errno::ENOENT
81
+ rescue Errno::ENOENT, UnsupportedError, NotFoundChromeEndpointError
63
82
  nil
64
83
  end
65
84
 
66
- def get_chrome_path
67
- return CONFIG[:chrome_path] if CONFIG[:chrome_path]
85
+ TIMEOUT_SEC = 5
86
+
87
+ def run_new_chrome
88
+ path = CONFIG[:chrome_path]
89
+
90
+ data = nil
91
+ port = nil
92
+ wait_thr = nil
68
93
 
69
94
  # The process to check OS is based on `selenium` project.
70
95
  case RbConfig::CONFIG['host_os']
71
96
  when /mswin|msys|mingw|cygwin|emc/
72
- 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
97
+ if path.nil?
98
+ candidates = ['C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe']
99
+ path = get_chrome_path candidates
100
+ end
101
+ # The path is based on https://github.com/sindresorhus/open/blob/v8.4.0/index.js#L128.
102
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
103
+ tf = Tempfile.create(['debug-', '.txt'])
104
+
105
+ 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}")
106
+ stdin.close
107
+ stdout.close
108
+ stderr.close
109
+ port, path = get_devtools_endpoint(tf.path)
110
+
111
+ at_exit{
112
+ DEBUGGER__.skip_all
113
+
114
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
115
+ stdin.puts("Stop-process -Name chrome")
116
+ stdin.close
117
+ stdout.close
118
+ stderr.close
119
+ tf.close
120
+ begin
121
+ File.unlink(tf)
122
+ rescue Errno::EACCES
123
+ end
124
+ }
73
125
  when /darwin|mac os/
74
- '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
126
+ path = path || '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
127
+ dir = Dir.mktmpdir
128
+ # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
129
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
130
+ stdin.close
131
+ stdout.close
132
+ data = stderr.readpartial 4096
133
+ stderr.close
134
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
135
+ port = $1
136
+ path = $2
137
+ end
138
+
139
+ at_exit{
140
+ DEBUGGER__.skip_all
141
+ FileUtils.rm_rf dir
142
+ }
75
143
  when /linux/
76
- 'google-chrome'
144
+ path = path || 'google-chrome'
145
+ dir = Dir.mktmpdir
146
+ # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
147
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
148
+ stdin.close
149
+ stdout.close
150
+ data = ''
151
+ begin
152
+ Timeout.timeout(TIMEOUT_SEC) do
153
+ until data.match?(/DevTools listening on ws:\/\/127.0.0.1:\d+.*/)
154
+ data = stderr.readpartial 4096
155
+ end
156
+ end
157
+ rescue Exception
158
+ raise NotFoundChromeEndpointError
159
+ end
160
+ stderr.close
161
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
162
+ port = $1
163
+ path = $2
164
+ end
165
+
166
+ at_exit{
167
+ DEBUGGER__.skip_all
168
+ FileUtils.rm_rf dir
169
+ }
77
170
  else
78
- raise "Unsupported OS"
171
+ raise UnsupportedError
79
172
  end
173
+
174
+ [port, path, wait_thr.pid]
80
175
  end
81
176
 
82
- def run_new_chrome
83
- dir = Dir.mktmpdir
84
- # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting
85
- 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}")
86
- stdin.close
87
- stdout.close
88
-
89
- data = stderr.readpartial 4096
90
- if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
91
- port = $1
92
- path = $2
177
+ def get_chrome_path candidates
178
+ candidates.each{|c|
179
+ if File.exist? c
180
+ return c
181
+ end
182
+ }
183
+ raise UnsupportedError
184
+ end
185
+
186
+ ITERATIONS = 50
187
+
188
+ def get_devtools_endpoint tf
189
+ i = 1
190
+ while i < ITERATIONS
191
+ i += 1
192
+ if File.exist?(tf) && data = File.read(tf)
193
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
194
+ port = $1
195
+ path = $2
196
+ return [port, path]
197
+ end
198
+ end
199
+ sleep 0.1
93
200
  end
94
- stderr.close
201
+ raise NotFoundChromeEndpointError
202
+ end
203
+ end
95
204
 
96
- at_exit{
97
- CONFIG[:skip_path] = [//] # skip all
98
- FileUtils.rm_rf dir
205
+ def send_chrome_response req
206
+ @repl = false
207
+ case req
208
+ when /^GET\s\/json\/version\sHTTP\/1.1/
209
+ body = {
210
+ Browser: "ruby/v#{RUBY_VERSION}",
211
+ 'Protocol-Version': "1.1"
99
212
  }
100
-
101
- [port, path, wait_thr.pid]
213
+ send_http_res body
214
+ raise UI_ServerBase::RetryConnection
215
+
216
+ when /^GET\s\/json\sHTTP\/1.1/
217
+ @uuid = @uuid || SecureRandom.uuid
218
+ addr = @local_addr.inspect_sockaddr
219
+ body = [{
220
+ description: "ruby instance",
221
+ devtoolsFrontendUrl: "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=#{addr}/#{@uuid}",
222
+ id: @uuid,
223
+ title: $0,
224
+ type: "node",
225
+ url: "file://#{File.absolute_path($0)}",
226
+ webSocketDebuggerUrl: "ws://#{addr}/#{@uuid}"
227
+ }]
228
+ send_http_res body
229
+ raise UI_ServerBase::RetryConnection
230
+
231
+ when /^GET\s\/(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\sHTTP\/1.1/
232
+ raise 'Incorrect uuid' unless $1 == @uuid
233
+
234
+ @need_pause_at_first = false
235
+ CONFIG.set_config no_color: true
236
+
237
+ @ws_server = WebSocketServer.new(@sock)
238
+ @ws_server.handshake
102
239
  end
103
240
  end
104
241
 
242
+ def send_http_res body
243
+ json = JSON.generate body
244
+ 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"
245
+ @sock.puts "#{header}#{json}"
246
+ end
247
+
105
248
  module WebSocketUtils
106
249
  class Frame
107
250
  attr_reader :b
@@ -306,11 +449,7 @@ module DEBUGGER__
306
449
  end
307
450
 
308
451
  def send_response req, **res
309
- if res.empty?
310
- @ws_server.send id: req['id'], result: {}
311
- else
312
- @ws_server.send id: req['id'], result: res
313
- end
452
+ @ws_server.send id: req['id'], result: res
314
453
  end
315
454
 
316
455
  def send_fail_response req, **res
@@ -318,11 +457,7 @@ module DEBUGGER__
318
457
  end
319
458
 
320
459
  def send_event method, **params
321
- if params.empty?
322
- @ws_server.send method: method, params: {}
323
- else
324
- @ws_server.send method: method, params: params
325
- end
460
+ @ws_server.send method: method, params: params
326
461
  end
327
462
 
328
463
  INVALID_REQUEST = -32600
@@ -339,7 +474,7 @@ module DEBUGGER__
339
474
  when 'Debugger.getScriptSource'
340
475
  @q_msg << req
341
476
  when 'Debugger.enable'
342
- send_response req
477
+ send_response req, debuggerId: rand.to_s
343
478
  @q_msg << req
344
479
  when 'Runtime.enable'
345
480
  send_response req
@@ -413,6 +548,9 @@ module DEBUGGER__
413
548
  activate_bp bps
414
549
  end
415
550
  send_response req
551
+ when 'Debugger.pause'
552
+ send_response req
553
+ Process.kill(UI_ServerBase::TRAP_SIGNAL, Process.pid)
416
554
 
417
555
  # breakpoint
418
556
  when 'Debugger.getPossibleBreakpoints'
@@ -420,35 +558,31 @@ module DEBUGGER__
420
558
  when 'Debugger.setBreakpointByUrl'
421
559
  line = req.dig('params', 'lineNumber')
422
560
  if regexp = req.dig('params', 'urlRegex')
423
- path = regexp.match(/(.*)\|/)[1].gsub("\\", "")
424
- cond = req.dig('params', 'condition')
425
- src = get_source_code path
426
- end_line = src.lines.count
427
- line = end_line if line > end_line
428
561
  b_id = "1:#{line}:#{regexp}"
429
- if cond != ''
430
- SESSION.add_line_breakpoint(path, line + 1, cond: cond)
431
- else
432
- SESSION.add_line_breakpoint(path, line + 1)
433
- end
434
562
  bps[b_id] = bps.size
435
- # Because we need to return scriptId, responses are returned in SESSION thread.
436
- req['params']['scriptId'] = path
437
- req['params']['lineNumber'] = line
438
- req['params']['breakpointId'] = b_id
439
- @q_msg << req
563
+ path = regexp.match(/(.*)\|/)[1].gsub("\\", "")
564
+ add_line_breakpoint(req, b_id, path)
440
565
  elsif url = req.dig('params', 'url')
441
566
  b_id = "#{line}:#{url}"
442
- send_response req,
443
- breakpointId: b_id,
444
- locations: []
445
- elsif hash = req.dig('params', 'scriptHash')
446
- b_id = "#{line}:#{hash}"
447
- send_response req,
448
- breakpointId: b_id,
449
- locations: []
567
+ # When breakpoints are set in Script snippet, non-existent path such as "snippet:///Script%20snippet%20%231" sent.
568
+ # That's why we need to check it here.
569
+ if File.exist? url
570
+ bps[b_id] = bps.size
571
+ add_line_breakpoint(req, b_id, url)
572
+ else
573
+ send_response req,
574
+ breakpointId: b_id,
575
+ locations: []
576
+ end
450
577
  else
451
- raise 'Unsupported'
578
+ if hash = req.dig('params', 'scriptHash')
579
+ b_id = "#{line}:#{hash}"
580
+ send_response req,
581
+ breakpointId: b_id,
582
+ locations: []
583
+ else
584
+ raise 'Unsupported'
585
+ end
452
586
  end
453
587
  when 'Debugger.removeBreakpoint'
454
588
  b_id = req.dig('params', 'breakpointId')
@@ -487,6 +621,24 @@ module DEBUGGER__
487
621
  @q_msg << 'continue'
488
622
  end
489
623
 
624
+ def add_line_breakpoint req, b_id, path
625
+ cond = req.dig('params', 'condition')
626
+ line = req.dig('params', 'lineNumber')
627
+ src = get_source_code path
628
+ end_line = src.lines.count
629
+ line = end_line if line > end_line
630
+ if cond != ''
631
+ SESSION.add_line_breakpoint(path, line + 1, cond: cond)
632
+ else
633
+ SESSION.add_line_breakpoint(path, line + 1)
634
+ end
635
+ # Because we need to return scriptId, responses are returned in SESSION thread.
636
+ req['params']['scriptId'] = path
637
+ req['params']['lineNumber'] = line
638
+ req['params']['breakpointId'] = b_id
639
+ @q_msg << req
640
+ end
641
+
490
642
  def del_bp bps, k
491
643
  return bps unless idx = bps[k]
492
644
 
@@ -524,43 +676,50 @@ module DEBUGGER__
524
676
  def cleanup_reader
525
677
  super
526
678
  Process.kill :KILL, @chrome_pid if @chrome_pid
679
+ rescue Errno::ESRCH # continue if @chrome_pid process is not found
527
680
  end
528
681
 
529
682
  ## Called by the SESSION thread
530
683
 
531
- def readline prompt
532
- return 'c' unless @q_msg
533
-
534
- @q_msg.pop || 'kill!'
535
- end
536
-
537
- def respond req, **result
538
- send_response req, **result
539
- end
540
-
541
- def respond_fail req, **result
542
- send_fail_response req, **result
543
- end
544
-
545
- def fire_event event, **result
546
- if result.empty?
547
- send_event event
548
- else
549
- send_event event, **result
550
- end
551
- end
684
+ alias respond send_response
685
+ alias respond_fail send_fail_response
686
+ alias fire_event send_event
552
687
 
553
688
  def sock skip: false
554
689
  yield $stderr
555
690
  end
556
691
 
557
- def puts result
692
+ def puts result=''
558
693
  # STDERR.puts "puts: #{result}"
559
694
  # send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
560
695
  end
561
696
  end
562
697
 
563
698
  class Session
699
+ include GlobalVariablesHelper
700
+
701
+ # FIXME: unify this method with ThreadClient#propertyDescriptor.
702
+ def get_type obj
703
+ case obj
704
+ when Array
705
+ ['object', 'array']
706
+ when Hash
707
+ ['object', 'map']
708
+ when String
709
+ ['string']
710
+ when TrueClass, FalseClass
711
+ ['boolean']
712
+ when Symbol
713
+ ['symbol']
714
+ when Integer, Float
715
+ ['number']
716
+ when Exception
717
+ ['object', 'error']
718
+ else
719
+ ['object']
720
+ end
721
+ end
722
+
564
723
  def fail_response req, **result
565
724
  @ui.respond_fail req, **result
566
725
  return :retry
@@ -584,17 +743,42 @@ module DEBUGGER__
584
743
  code: INVALID_PARAMS,
585
744
  message: "'callFrameId' is an invalid"
586
745
  end
587
- when 'Runtime.getProperties'
588
- oid = req.dig('params', 'objectId')
746
+ when 'Runtime.getProperties', 'Runtime.getExceptionDetails'
747
+ oid = req.dig('params', 'objectId') || req.dig('params', 'errorObjectId')
589
748
  if ref = @obj_map[oid]
590
749
  case ref[0]
591
750
  when 'local'
592
751
  frame_id = ref[1]
593
752
  fid = @frame_map[frame_id]
594
753
  request_tc [:cdp, :scope, req, fid]
754
+ when 'global'
755
+ vars = safe_global_variables.sort.map do |name|
756
+ begin
757
+ gv = eval(name.to_s)
758
+ rescue Errno::ENOENT
759
+ gv = nil
760
+ end
761
+ prop = {
762
+ name: name,
763
+ value: {
764
+ description: gv.inspect
765
+ },
766
+ configurable: true,
767
+ enumerable: true
768
+ }
769
+ type, subtype = get_type(gv)
770
+ prop[:value][:type] = type
771
+ prop[:value][:subtype] = subtype if subtype
772
+ prop
773
+ end
774
+
775
+ @ui.respond req, result: vars
776
+ return :retry
595
777
  when 'properties'
596
778
  request_tc [:cdp, :properties, req, oid]
597
- when 'script', 'global'
779
+ when 'exception'
780
+ request_tc [:cdp, :exception, req, oid]
781
+ when 'script'
598
782
  # TODO: Support script and global types
599
783
  @ui.respond req, result: []
600
784
  return :retry
@@ -653,7 +837,7 @@ module DEBUGGER__
653
837
  end
654
838
  end
655
839
 
656
- def cdp_event args
840
+ def process_protocol_result args
657
841
  type, req, result = args
658
842
 
659
843
  case type
@@ -665,27 +849,25 @@ module DEBUGGER__
665
849
  unless s_id = @scr_id_map[path]
666
850
  s_id = (@scr_id_map.size + 1).to_s
667
851
  @scr_id_map[path] = s_id
852
+ lineno = 0
853
+ src = ''
668
854
  if path && File.exist?(path)
669
855
  src = File.read(path)
856
+ @src_map[s_id] = src
857
+ lineno = src.lines.count
670
858
  end
671
- @src_map[s_id] = src
672
- end
673
- if src = @src_map[s_id]
674
- lineno = src.lines.count
675
- else
676
- lineno = 0
677
- end
678
- frame[:location][:scriptId] = s_id
679
- frame[:functionLocation][:scriptId] = s_id
680
- @ui.fire_event 'Debugger.scriptParsed',
859
+ @ui.fire_event 'Debugger.scriptParsed',
681
860
  scriptId: s_id,
682
- url: frame[:url],
861
+ url: path,
683
862
  startLine: 0,
684
863
  startColumn: 0,
685
864
  endLine: lineno,
686
865
  endColumn: 0,
687
866
  executionContextId: 1,
688
867
  hash: src.hash.inspect
868
+ end
869
+ frame[:location][:scriptId] = s_id
870
+ frame[:functionLocation][:scriptId] = s_id
689
871
 
690
872
  frame[:scopeChain].each {|s|
691
873
  oid = s.dig(:object, :objectId)
@@ -730,6 +912,9 @@ module DEBUGGER__
730
912
  frame[:scriptId] = s_id
731
913
  end
732
914
  }
915
+ if oid = exc[:exception][:objectId]
916
+ @obj_map[oid] = ['exception']
917
+ end
733
918
  end
734
919
  rs = result.dig(:response, :result)
735
920
  [rs].each{|obj|
@@ -767,6 +952,8 @@ module DEBUGGER__
767
952
  }
768
953
  }
769
954
  @ui.respond req, **result
955
+ when :exception
956
+ @ui.respond req, **result
770
957
  end
771
958
  end
772
959
  end
@@ -838,7 +1025,7 @@ module DEBUGGER__
838
1025
  result[:data] = evaluate_result exception
839
1026
  result[:reason] = 'exception'
840
1027
  end
841
- event! :cdp_result, :backtrace, req, result
1028
+ event! :protocol_result, :backtrace, req, result
842
1029
  when :evaluate
843
1030
  res = {}
844
1031
  fid, expr, group = args
@@ -857,7 +1044,7 @@ module DEBUGGER__
857
1044
  case expr
858
1045
  # Chrome doesn't read instance variables
859
1046
  when /\A\$\S/
860
- global_variables.each{|gvar|
1047
+ safe_global_variables.each{|gvar|
861
1048
  if gvar.to_s == expr
862
1049
  result = eval(gvar.to_s)
863
1050
  break false
@@ -872,8 +1059,8 @@ module DEBUGGER__
872
1059
  result = b.local_variable_get(expr)
873
1060
  rescue NameError
874
1061
  # try to check method
875
- if b.receiver.respond_to? expr, include_all: true
876
- result = b.receiver.method(expr)
1062
+ if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true)
1063
+ result = M_METHOD.bind_call(b.receiver, expr)
877
1064
  else
878
1065
  message = "Error: Can not evaluate: #{expr.inspect}"
879
1066
  end
@@ -883,38 +1070,10 @@ module DEBUGGER__
883
1070
  begin
884
1071
  orig_stdout = $stdout
885
1072
  $stdout = StringIO.new
886
- result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
1073
+ result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
887
1074
  rescue Exception => e
888
1075
  result = e
889
- b = result.backtrace.map{|e| " #{e}\n"}
890
- frames = [
891
- {
892
- columnNumber: 0,
893
- functionName: 'eval',
894
- lineNumber: 0,
895
- url: ''
896
- }
897
- ]
898
- e.backtrace_locations&.each do |loc|
899
- break if loc.path == __FILE__
900
- path = loc.absolute_path || loc.path
901
- frames << {
902
- columnNumber: 0,
903
- functionName: loc.base_label,
904
- lineNumber: loc.lineno - 1,
905
- url: path
906
- }
907
- end
908
- res[:exceptionDetails] = {
909
- exceptionId: 1,
910
- text: 'Uncaught',
911
- lineNumber: 0,
912
- columnNumber: 0,
913
- exception: evaluate_result(result),
914
- stackTrace: {
915
- callFrames: frames
916
- }
917
- }
1076
+ res[:exceptionDetails] = exceptionDetails(e, 'Uncaught')
918
1077
  ensure
919
1078
  output = $stdout.string
920
1079
  $stdout = orig_stdout
@@ -927,7 +1086,7 @@ module DEBUGGER__
927
1086
  end
928
1087
 
929
1088
  res[:result] = evaluate_result(result)
930
- event! :cdp_result, :evaluate, req, message: message, response: res, output: output
1089
+ event! :protocol_result, :evaluate, req, message: message, response: res, output: output
931
1090
  when :scope
932
1091
  fid = args.shift
933
1092
  frame = @target_frames[fid]
@@ -950,7 +1109,7 @@ module DEBUGGER__
950
1109
  vars.unshift variable(name, val)
951
1110
  end
952
1111
  end
953
- event! :cdp_result, :scope, req, vars
1112
+ event! :protocol_result, :scope, req, vars
954
1113
  when :properties
955
1114
  oid = args.shift
956
1115
  result = []
@@ -987,21 +1146,63 @@ module DEBUGGER__
987
1146
  ]
988
1147
  end
989
1148
 
990
- result += obj.instance_variables.map{|iv|
991
- variable(iv, obj.instance_variable_get(iv))
1149
+ result += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
1150
+ variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
992
1151
  }
993
- prop += [internalProperty('#class', obj.class)]
1152
+ prop += [internalProperty('#class', M_CLASS.bind_call(obj))]
994
1153
  end
995
- event! :cdp_result, :properties, req, result: result, internalProperties: prop
1154
+ event! :protocol_result, :properties, req, result: result, internalProperties: prop
1155
+ when :exception
1156
+ oid = args.shift
1157
+ exc = nil
1158
+ if obj = @obj_map[oid]
1159
+ exc = exceptionDetails obj, obj.to_s
1160
+ end
1161
+ event! :protocol_result, :exception, req, exceptionDetails: exc
1162
+ end
1163
+ end
1164
+
1165
+ def exceptionDetails exc, text
1166
+ frames = [
1167
+ {
1168
+ columnNumber: 0,
1169
+ functionName: 'eval',
1170
+ lineNumber: 0,
1171
+ url: ''
1172
+ }
1173
+ ]
1174
+ exc.backtrace_locations&.each do |loc|
1175
+ break if loc.path == __FILE__
1176
+ path = loc.absolute_path || loc.path
1177
+ frames << {
1178
+ columnNumber: 0,
1179
+ functionName: loc.base_label,
1180
+ lineNumber: loc.lineno - 1,
1181
+ url: path
1182
+ }
996
1183
  end
1184
+ {
1185
+ exceptionId: 1,
1186
+ text: text,
1187
+ lineNumber: 0,
1188
+ columnNumber: 0,
1189
+ exception: evaluate_result(exc),
1190
+ stackTrace: {
1191
+ callFrames: frames
1192
+ }
1193
+ }
997
1194
  end
998
1195
 
999
1196
  def search_const b, expr
1000
1197
  cs = expr.delete_prefix('::').split('::')
1001
- [Object, *b.eval('Module.nesting')].reverse_each{|mod|
1198
+ [Object, *b.eval('::Module.nesting')].reverse_each{|mod|
1002
1199
  if cs.all?{|c|
1003
1200
  if mod.const_defined?(c)
1004
- mod = mod.const_get(c)
1201
+ begin
1202
+ mod = mod.const_get(c)
1203
+ rescue Exception
1204
+ false
1205
+ end
1005
1206
  else
1006
1207
  false
1007
1208
  end
@@ -1045,25 +1246,29 @@ module DEBUGGER__
1045
1246
  v = prop[:value]
1046
1247
  v.delete :value
1047
1248
  v[:subtype] = subtype if subtype
1048
- v[:className] = obj.class
1249
+ v[:className] = (klass = M_CLASS.bind_call(obj)).name || klass.to_s
1049
1250
  end
1050
1251
  prop
1051
1252
  end
1052
1253
 
1053
1254
  def preview_ value, hash, overflow
1255
+ # The reason for not using "map" method is to prevent the object overriding it from causing bugs.
1256
+ # https://github.com/ruby/debug/issues/781
1257
+ props = []
1258
+ hash.each{|k, v|
1259
+ pd = propertyDescriptor k, v
1260
+ props << {
1261
+ name: pd[:name],
1262
+ type: pd[:value][:type],
1263
+ value: pd[:value][:description]
1264
+ }
1265
+ }
1054
1266
  {
1055
1267
  type: value[:type],
1056
1268
  subtype: value[:subtype],
1057
1269
  description: value[:description],
1058
1270
  overflow: overflow,
1059
- properties: hash.map{|k, v|
1060
- pd = propertyDescriptor k, v
1061
- {
1062
- name: pd[:name],
1063
- type: pd[:value][:type],
1064
- value: pd[:value][:description]
1065
- }
1066
- }
1271
+ properties: props
1067
1272
  }
1068
1273
  end
1069
1274