debug 1.6.3 → 1.7.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.
data/lib/debug/prelude.rb CHANGED
File without changes
data/lib/debug/server.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'socket'
4
- require 'etc'
5
4
  require_relative 'config'
6
5
  require_relative 'version'
7
6
 
@@ -22,6 +21,7 @@ module DEBUGGER__
22
21
 
23
22
  class Terminate < StandardError; end
24
23
  class GreetingError < StandardError; end
24
+ class RetryConnection < StandardError; end
25
25
 
26
26
  def deactivate
27
27
  @reader_thread.raise Terminate
@@ -78,6 +78,8 @@ module DEBUGGER__
78
78
  next
79
79
  rescue Terminate
80
80
  raise # should catch at outer scope
81
+ rescue RetryConnection
82
+ next
81
83
  rescue => e
82
84
  DEBUGGER__.warn "ReaderThreadError: #{e}"
83
85
  pp e.backtrace
@@ -128,6 +130,8 @@ module DEBUGGER__
128
130
  def greeting
129
131
  case g = @sock.gets
130
132
  when /^info cookie:\s+(.*)$/
133
+ require 'etc'
134
+
131
135
  check_cookie $1
132
136
  @sock.puts "PID: #{Process.pid}, $0: #{$0}"
133
137
  @sock.puts "debug #{VERSION} on #{RUBY_DESCRIPTION}"
@@ -140,7 +144,8 @@ module DEBUGGER__
140
144
 
141
145
  # TODO: protocol version
142
146
  if v != VERSION
143
- raise GreetingError, "Incompatible version (server:#{VERSION} and client:#{$1})"
147
+ @sock.puts msg = "out DEBUGGER: Incompatible version (server:#{VERSION} and client:#{$1})"
148
+ raise GreetingError, msg
144
149
  end
145
150
  parse_option(params)
146
151
 
@@ -157,16 +162,13 @@ module DEBUGGER__
157
162
  @need_pause_at_first = false
158
163
  dap_setup @sock.read($1.to_i)
159
164
 
160
- when /^GET \/.* HTTP\/1.1/
165
+ when /^GET\s\/json\sHTTP\/1.1/, /^GET\s\/json\/version\sHTTP\/1.1/, /^GET\s\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\sHTTP\/1.1/
166
+ # The reason for not using @uuid here is @uuid is nil if users run debugger without `--open=chrome`.
167
+
161
168
  require_relative 'server_cdp'
162
169
 
163
170
  self.extend(UI_CDP)
164
- @repl = false
165
- @need_pause_at_first = false
166
- CONFIG.set_config no_color: true
167
-
168
- @ws_server = UI_CDP::WebSocketServer.new(@sock)
169
- @ws_server.handshake
171
+ send_chrome_response g
170
172
  else
171
173
  raise GreetingError, "Unknown greeting message: #{g}"
172
174
  end
@@ -174,17 +176,17 @@ module DEBUGGER__
174
176
 
175
177
  def process
176
178
  while true
177
- DEBUGGER__.info "sleep IO.select"
178
- r = IO.select([@sock])
179
- DEBUGGER__.info "wakeup IO.select"
179
+ DEBUGGER__.debug{ "sleep IO.select" }
180
+ _r = IO.select([@sock])
181
+ DEBUGGER__.debug{ "wakeup IO.select" }
180
182
 
181
183
  line = @session.process_group.sync do
182
184
  unless IO.select([@sock], nil, nil, 0)
183
- DEBUGGER__.info "UI_Server can not read"
185
+ DEBUGGER__.debug{ "UI_Server can not read" }
184
186
  break :can_not_read
185
187
  end
186
188
  @sock.gets&.chomp.tap{|line|
187
- DEBUGGER__.info "UI_Server received: #{line}"
189
+ DEBUGGER__.debug{ "UI_Server received: #{line}" }
188
190
  }
189
191
  end
190
192
 
@@ -340,12 +342,12 @@ module DEBUGGER__
340
342
  if @repl
341
343
  raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
342
344
  line = "input #{Process.pid}"
343
- DEBUGGER__.info "send: #{line}"
345
+ DEBUGGER__.debug{ "send: #{line}" }
344
346
  s.puts line
345
347
  end
346
348
  sleep 0.01 until @q_msg
347
349
  @q_msg.pop.tap{|msg|
348
- DEBUGGER__.info "readline: #{msg.inspect}"
350
+ DEBUGGER__.debug{ "readline: #{msg.inspect}" }
349
351
  }
350
352
  end || 'continue')
351
353
 
@@ -361,7 +363,7 @@ module DEBUGGER__
361
363
  Process.kill(TRAP_SIGNAL, Process.pid)
362
364
  end
363
365
 
364
- def quit n
366
+ def quit n, &_b
365
367
  # ignore n
366
368
  sock do |s|
367
369
  s.puts "quit"
@@ -395,6 +397,7 @@ module DEBUGGER__
395
397
  raise "Specify digits for port number"
396
398
  end
397
399
  end
400
+ @uuid = nil # for CDP
398
401
 
399
402
  super()
400
403
  end
@@ -402,11 +405,12 @@ module DEBUGGER__
402
405
  def chrome_setup
403
406
  require_relative 'server_cdp'
404
407
 
405
- unless @chrome_pid = UI_CDP.setup_chrome(@local_addr.inspect_sockaddr)
408
+ @uuid = SecureRandom.uuid
409
+ unless @chrome_pid = UI_CDP.setup_chrome(@local_addr.inspect_sockaddr, @uuid)
406
410
  DEBUGGER__.warn <<~EOS
407
411
  With Chrome browser, type the following URL in the address-bar:
408
412
 
409
- devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&ws=#{@local_addr.inspect_sockaddr}/#{SecureRandom.uuid}
413
+ devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&ws=#{@local_addr.inspect_sockaddr}/#{@uuid}
410
414
 
411
415
  EOS
412
416
  end
@@ -434,7 +438,7 @@ module DEBUGGER__
434
438
  #
435
439
  EOS
436
440
 
437
- case CONFIG[:open_frontend]
441
+ case CONFIG[:open]
438
442
  when 'chrome'
439
443
  chrome_setup
440
444
  when 'vscode'
@@ -491,7 +495,7 @@ module DEBUGGER__
491
495
  end
492
496
 
493
497
  ::DEBUGGER__.warn "Debugger can attach via UNIX domain socket (#{@sock_path})"
494
- vscode_setup @sock_path if CONFIG[:open_frontend] == 'vscode'
498
+ vscode_setup @sock_path if CONFIG[:open] == 'vscode'
495
499
 
496
500
  begin
497
501
  Socket.unix_server_loop @sock_path do |sock, client|
@@ -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
@@ -39,6 +44,8 @@ module DEBUGGER__
39
44
  }
40
45
  when res['id'] == 2
41
46
  s_id = res.dig('result', 'sessionId')
47
+ # TODO: change id
48
+ ws_client.send sessionId: s_id, id: 100, method: 'Network.enable'
42
49
  ws_client.send sessionId: s_id, id: 3,
43
50
  method: 'Page.enable'
44
51
  when res['id'] == 3
@@ -51,57 +58,191 @@ module DEBUGGER__
51
58
  ws_client.send sessionId: s_id, id: 5,
52
59
  method: 'Page.navigate',
53
60
  params: {
54
- url: "devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&ws=#{addr}/#{SecureRandom.uuid}",
61
+ url: "devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&ws=#{addr}/#{uuid}",
55
62
  frameId: f_id
56
63
  }
57
- when res['method'] == 'Page.loadEventFired'
64
+ when res['method'] == 'Network.webSocketWillSendHandshakeRequest'
65
+ s_id = res['sessionId']
66
+ # Display the console by entering ESC key
67
+ ws_client.send sessionId: s_id, id: 101, # TODO: change id
68
+ method:"Input.dispatchKeyEvent",
69
+ params: {
70
+ type:"keyDown",
71
+ windowsVirtualKeyCode:27 # ESC key
72
+ }
73
+ when res['id'] == 101
58
74
  break
59
75
  end
60
76
  end
61
77
  pid
62
- rescue Errno::ENOENT
78
+ rescue Errno::ENOENT, UnsupportedError, NotFoundChromeEndpointError
63
79
  nil
64
80
  end
65
81
 
66
- def get_chrome_path
67
- return CONFIG[:chrome_path] if CONFIG[:chrome_path]
82
+ TIMEOUT_SEC = 5
83
+
84
+ def run_new_chrome
85
+ path = CONFIG[:chrome_path]
86
+
87
+ data = nil
88
+ port = nil
89
+ wait_thr = nil
68
90
 
69
91
  # The process to check OS is based on `selenium` project.
70
92
  case RbConfig::CONFIG['host_os']
71
93
  when /mswin|msys|mingw|cygwin|emc/
72
- 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
94
+ if path.nil?
95
+ candidates = ['C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe']
96
+ path = get_chrome_path candidates
97
+ end
98
+ uuid = SecureRandom.uuid
99
+ # The path is based on https://github.com/sindresorhus/open/blob/v8.4.0/index.js#L128.
100
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
101
+ tf = Tempfile.create(['debug-', '.txt'])
102
+
103
+ 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}")
104
+ stdin.close
105
+ stdout.close
106
+ stderr.close
107
+ port, path = get_devtools_endpoint(tf.path)
108
+
109
+ at_exit{
110
+ DEBUGGER__.skip_all
111
+
112
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
113
+ stdin.puts("Stop-process -Name chrome")
114
+ stdin.close
115
+ stdout.close
116
+ stderr.close
117
+ tf.close
118
+ begin
119
+ File.unlink(tf)
120
+ rescue Errno::EACCES
121
+ end
122
+ }
73
123
  when /darwin|mac os/
74
- '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
124
+ path = path || '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
125
+ dir = Dir.mktmpdir
126
+ # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
127
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
128
+ stdin.close
129
+ stdout.close
130
+ data = stderr.readpartial 4096
131
+ stderr.close
132
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
133
+ port = $1
134
+ path = $2
135
+ end
136
+
137
+ at_exit{
138
+ DEBUGGER__.skip_all
139
+ FileUtils.rm_rf dir
140
+ }
75
141
  when /linux/
76
- 'google-chrome'
142
+ path = path || 'google-chrome'
143
+ dir = Dir.mktmpdir
144
+ # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
145
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
146
+ stdin.close
147
+ stdout.close
148
+ data = ''
149
+ begin
150
+ Timeout.timeout(TIMEOUT_SEC) do
151
+ until data.match?(/DevTools listening on ws:\/\/127.0.0.1:\d+.*/)
152
+ data = stderr.readpartial 4096
153
+ end
154
+ end
155
+ rescue Exception
156
+ raise NotFoundChromeEndpointError
157
+ end
158
+ stderr.close
159
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
160
+ port = $1
161
+ path = $2
162
+ end
163
+
164
+ at_exit{
165
+ DEBUGGER__.skip_all
166
+ FileUtils.rm_rf dir
167
+ }
77
168
  else
78
- raise "Unsupported OS"
169
+ raise UnsupportedError
79
170
  end
171
+
172
+ [port, path, wait_thr.pid]
80
173
  end
81
174
 
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
175
+ def get_chrome_path candidates
176
+ candidates.each{|c|
177
+ if File.exist? c
178
+ return c
179
+ end
180
+ }
181
+ raise UnsupportedError
182
+ end
183
+
184
+ ITERATIONS = 50
185
+
186
+ def get_devtools_endpoint tf
187
+ i = 1
188
+ while i < ITERATIONS
189
+ i += 1
190
+ if File.exist?(tf) && data = File.read(tf)
191
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
192
+ port = $1
193
+ path = $2
194
+ return [port, path]
195
+ end
196
+ end
197
+ sleep 0.1
93
198
  end
94
- stderr.close
199
+ raise NotFoundChromeEndpointError
200
+ end
201
+ end
95
202
 
96
- at_exit{
97
- CONFIG[:skip_path] = [//] # skip all
98
- FileUtils.rm_rf dir
203
+ def send_chrome_response req
204
+ @repl = false
205
+ case req
206
+ when /^GET\s\/json\/version\sHTTP\/1.1/
207
+ body = {
208
+ Browser: "ruby/v#{RUBY_VERSION}",
209
+ 'Protocol-Version': "1.1"
99
210
  }
100
-
101
- [port, path, wait_thr.pid]
211
+ send_http_res body
212
+ raise UI_ServerBase::RetryConnection
213
+
214
+ when /^GET\s\/json\sHTTP\/1.1/
215
+ @uuid = @uuid || SecureRandom.uuid
216
+ addr = @local_addr.inspect_sockaddr
217
+ body = [{
218
+ description: "ruby instance",
219
+ devtoolsFrontendUrl: "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=#{addr}/#{@uuid}",
220
+ id: @uuid,
221
+ title: $0,
222
+ type: "node",
223
+ url: "file://#{File.absolute_path($0)}",
224
+ webSocketDebuggerUrl: "ws://#{addr}/#{@uuid}"
225
+ }]
226
+ send_http_res body
227
+ raise UI_ServerBase::RetryConnection
228
+
229
+ when /^GET\s\/(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\sHTTP\/1.1/
230
+ raise 'Incorrect uuid' unless $1 == @uuid
231
+
232
+ @need_pause_at_first = false
233
+ CONFIG.set_config no_color: true
234
+
235
+ @ws_server = WebSocketServer.new(@sock)
236
+ @ws_server.handshake
102
237
  end
103
238
  end
104
239
 
240
+ def send_http_res body
241
+ json = JSON.generate body
242
+ 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"
243
+ @sock.puts "#{header}#{json}"
244
+ end
245
+
105
246
  module WebSocketUtils
106
247
  class Frame
107
248
  attr_reader :b
@@ -528,12 +669,6 @@ module DEBUGGER__
528
669
 
529
670
  ## Called by the SESSION thread
530
671
 
531
- def readline prompt
532
- return 'c' unless @q_msg
533
-
534
- @q_msg.pop || 'kill!'
535
- end
536
-
537
672
  def respond req, **result
538
673
  send_response req, **result
539
674
  end
@@ -561,6 +696,28 @@ module DEBUGGER__
561
696
  end
562
697
 
563
698
  class Session
699
+ # FIXME: unify this method with ThreadClient#propertyDescriptor.
700
+ def get_type obj
701
+ case obj
702
+ when Array
703
+ ['object', 'array']
704
+ when Hash
705
+ ['object', 'map']
706
+ when String
707
+ ['string']
708
+ when TrueClass, FalseClass
709
+ ['boolean']
710
+ when Symbol
711
+ ['symbol']
712
+ when Integer, Float
713
+ ['number']
714
+ when Exception
715
+ ['object', 'error']
716
+ else
717
+ ['object']
718
+ end
719
+ end
720
+
564
721
  def fail_response req, **result
565
722
  @ui.respond_fail req, **result
566
723
  return :retry
@@ -584,17 +741,38 @@ module DEBUGGER__
584
741
  code: INVALID_PARAMS,
585
742
  message: "'callFrameId' is an invalid"
586
743
  end
587
- when 'Runtime.getProperties'
588
- oid = req.dig('params', 'objectId')
744
+ when 'Runtime.getProperties', 'Runtime.getExceptionDetails'
745
+ oid = req.dig('params', 'objectId') || req.dig('params', 'errorObjectId')
589
746
  if ref = @obj_map[oid]
590
747
  case ref[0]
591
748
  when 'local'
592
749
  frame_id = ref[1]
593
750
  fid = @frame_map[frame_id]
594
751
  request_tc [:cdp, :scope, req, fid]
752
+ when 'global'
753
+ vars = global_variables.sort.map do |name|
754
+ gv = eval(name.to_s)
755
+ prop = {
756
+ name: name,
757
+ value: {
758
+ description: gv.inspect
759
+ },
760
+ configurable: true,
761
+ enumerable: true
762
+ }
763
+ type, subtype = get_type(gv)
764
+ prop[:value][:type] = type
765
+ prop[:value][:subtype] = subtype if subtype
766
+ prop
767
+ end
768
+
769
+ @ui.respond req, result: vars
770
+ return :retry
595
771
  when 'properties'
596
772
  request_tc [:cdp, :properties, req, oid]
597
- when 'script', 'global'
773
+ when 'exception'
774
+ request_tc [:cdp, :exception, req, oid]
775
+ when 'script'
598
776
  # TODO: Support script and global types
599
777
  @ui.respond req, result: []
600
778
  return :retry
@@ -730,6 +908,9 @@ module DEBUGGER__
730
908
  frame[:scriptId] = s_id
731
909
  end
732
910
  }
911
+ if oid = exc[:exception][:objectId]
912
+ @obj_map[oid] = ['exception']
913
+ end
733
914
  end
734
915
  rs = result.dig(:response, :result)
735
916
  [rs].each{|obj|
@@ -767,6 +948,8 @@ module DEBUGGER__
767
948
  }
768
949
  }
769
950
  @ui.respond req, **result
951
+ when :exception
952
+ @ui.respond req, **result
770
953
  end
771
954
  end
772
955
  end
@@ -872,8 +1055,8 @@ module DEBUGGER__
872
1055
  result = b.local_variable_get(expr)
873
1056
  rescue NameError
874
1057
  # try to check method
875
- if b.receiver.respond_to? expr, include_all: true
876
- result = b.receiver.method(expr)
1058
+ if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true)
1059
+ result = M_METHOD.bind_call(b.receiver, expr)
877
1060
  else
878
1061
  message = "Error: Can not evaluate: #{expr.inspect}"
879
1062
  end
@@ -886,35 +1069,7 @@ module DEBUGGER__
886
1069
  result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
887
1070
  rescue Exception => e
888
1071
  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
- }
1072
+ res[:exceptionDetails] = exceptionDetails(e, 'Uncaught')
918
1073
  ensure
919
1074
  output = $stdout.string
920
1075
  $stdout = orig_stdout
@@ -987,13 +1142,51 @@ module DEBUGGER__
987
1142
  ]
988
1143
  end
989
1144
 
990
- result += obj.instance_variables.map{|iv|
991
- variable(iv, obj.instance_variable_get(iv))
1145
+ result += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
1146
+ variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
992
1147
  }
993
- prop += [internalProperty('#class', obj.class)]
1148
+ prop += [internalProperty('#class', M_CLASS.bind_call(obj))]
994
1149
  end
995
1150
  event! :cdp_result, :properties, req, result: result, internalProperties: prop
1151
+ when :exception
1152
+ oid = args.shift
1153
+ exc = nil
1154
+ if obj = @obj_map[oid]
1155
+ exc = exceptionDetails obj, obj.to_s
1156
+ end
1157
+ event! :cdp_result, :exception, req, exceptionDetails: exc
1158
+ end
1159
+ end
1160
+
1161
+ def exceptionDetails exc, text
1162
+ frames = [
1163
+ {
1164
+ columnNumber: 0,
1165
+ functionName: 'eval',
1166
+ lineNumber: 0,
1167
+ url: ''
1168
+ }
1169
+ ]
1170
+ exc.backtrace_locations&.each do |loc|
1171
+ break if loc.path == __FILE__
1172
+ path = loc.absolute_path || loc.path
1173
+ frames << {
1174
+ columnNumber: 0,
1175
+ functionName: loc.base_label,
1176
+ lineNumber: loc.lineno - 1,
1177
+ url: path
1178
+ }
996
1179
  end
1180
+ {
1181
+ exceptionId: 1,
1182
+ text: text,
1183
+ lineNumber: 0,
1184
+ columnNumber: 0,
1185
+ exception: evaluate_result(exc),
1186
+ stackTrace: {
1187
+ callFrames: frames
1188
+ }
1189
+ }
997
1190
  end
998
1191
 
999
1192
  def search_const b, expr
@@ -1001,7 +1194,11 @@ module DEBUGGER__
1001
1194
  [Object, *b.eval('::Module.nesting')].reverse_each{|mod|
1002
1195
  if cs.all?{|c|
1003
1196
  if mod.const_defined?(c)
1004
- mod = mod.const_get(c)
1197
+ begin
1198
+ mod = mod.const_get(c)
1199
+ rescue Exception
1200
+ false
1201
+ end
1005
1202
  else
1006
1203
  false
1007
1204
  end
@@ -1045,25 +1242,29 @@ module DEBUGGER__
1045
1242
  v = prop[:value]
1046
1243
  v.delete :value
1047
1244
  v[:subtype] = subtype if subtype
1048
- v[:className] = obj.class
1245
+ v[:className] = (klass = M_CLASS.bind_call(obj)).name || klass.to_s
1049
1246
  end
1050
1247
  prop
1051
1248
  end
1052
1249
 
1053
1250
  def preview_ value, hash, overflow
1251
+ # The reason for not using "map" method is to prevent the object overriding it from causing bugs.
1252
+ # https://github.com/ruby/debug/issues/781
1253
+ props = []
1254
+ hash.each{|k, v|
1255
+ pd = propertyDescriptor k, v
1256
+ props << {
1257
+ name: pd[:name],
1258
+ type: pd[:value][:type],
1259
+ value: pd[:value][:description]
1260
+ }
1261
+ }
1054
1262
  {
1055
1263
  type: value[:type],
1056
1264
  subtype: value[:subtype],
1057
1265
  description: value[:description],
1058
1266
  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
- }
1267
+ properties: props
1067
1268
  }
1068
1269
  end
1069
1270