debug 1.6.1 → 1.9.1

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