debug 1.6.3 → 1.7.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,218 @@ 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&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
+ uuid = SecureRandom.uuid
102
+ # The path is based on https://github.com/sindresorhus/open/blob/v8.4.0/index.js#L128.
103
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
104
+ tf = Tempfile.create(['debug-', '.txt'])
105
+
106
+ 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}")
107
+ stdin.close
108
+ stdout.close
109
+ stderr.close
110
+ port, path = get_devtools_endpoint(tf.path)
111
+
112
+ at_exit{
113
+ DEBUGGER__.skip_all
114
+
115
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{ENV['SystemRoot']}\\System32\\WindowsPowerShell\\v1.0\\powershell")
116
+ stdin.puts("Stop-process -Name chrome")
117
+ stdin.close
118
+ stdout.close
119
+ stderr.close
120
+ tf.close
121
+ begin
122
+ File.unlink(tf)
123
+ rescue Errno::EACCES
124
+ end
125
+ }
73
126
  when /darwin|mac os/
74
- '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
127
+ path = path || '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
128
+ dir = Dir.mktmpdir
129
+ # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
130
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
131
+ stdin.close
132
+ stdout.close
133
+ data = stderr.readpartial 4096
134
+ stderr.close
135
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
136
+ port = $1
137
+ path = $2
138
+ end
139
+
140
+ at_exit{
141
+ DEBUGGER__.skip_all
142
+ FileUtils.rm_rf dir
143
+ }
75
144
  when /linux/
76
- 'google-chrome'
145
+ path = path || 'google-chrome'
146
+ dir = Dir.mktmpdir
147
+ # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting.
148
+ stdin, stdout, stderr, wait_thr = *Open3.popen3("#{path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}")
149
+ stdin.close
150
+ stdout.close
151
+ data = ''
152
+ begin
153
+ Timeout.timeout(TIMEOUT_SEC) do
154
+ until data.match?(/DevTools listening on ws:\/\/127.0.0.1:\d+.*/)
155
+ data = stderr.readpartial 4096
156
+ end
157
+ end
158
+ rescue Exception
159
+ raise NotFoundChromeEndpointError
160
+ end
161
+ stderr.close
162
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
163
+ port = $1
164
+ path = $2
165
+ end
166
+
167
+ at_exit{
168
+ DEBUGGER__.skip_all
169
+ FileUtils.rm_rf dir
170
+ }
77
171
  else
78
- raise "Unsupported OS"
172
+ raise UnsupportedError
79
173
  end
174
+
175
+ [port, path, wait_thr.pid]
80
176
  end
81
177
 
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
178
+ def get_chrome_path candidates
179
+ candidates.each{|c|
180
+ if File.exist? c
181
+ return c
182
+ end
183
+ }
184
+ raise UnsupportedError
185
+ end
186
+
187
+ ITERATIONS = 50
188
+
189
+ def get_devtools_endpoint tf
190
+ i = 1
191
+ while i < ITERATIONS
192
+ i += 1
193
+ if File.exist?(tf) && data = File.read(tf)
194
+ if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
195
+ port = $1
196
+ path = $2
197
+ return [port, path]
198
+ end
199
+ end
200
+ sleep 0.1
93
201
  end
94
- stderr.close
202
+ raise NotFoundChromeEndpointError
203
+ end
204
+ end
95
205
 
96
- at_exit{
97
- CONFIG[:skip_path] = [//] # skip all
98
- FileUtils.rm_rf dir
206
+ def send_chrome_response req
207
+ @repl = false
208
+ case req
209
+ when /^GET\s\/json\/version\sHTTP\/1.1/
210
+ body = {
211
+ Browser: "ruby/v#{RUBY_VERSION}",
212
+ 'Protocol-Version': "1.1"
99
213
  }
100
-
101
- [port, path, wait_thr.pid]
214
+ send_http_res body
215
+ raise UI_ServerBase::RetryConnection
216
+
217
+ when /^GET\s\/json\sHTTP\/1.1/
218
+ @uuid = @uuid || SecureRandom.uuid
219
+ addr = @local_addr.inspect_sockaddr
220
+ body = [{
221
+ description: "ruby instance",
222
+ devtoolsFrontendUrl: "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=#{addr}/#{@uuid}",
223
+ id: @uuid,
224
+ title: $0,
225
+ type: "node",
226
+ url: "file://#{File.absolute_path($0)}",
227
+ webSocketDebuggerUrl: "ws://#{addr}/#{@uuid}"
228
+ }]
229
+ send_http_res body
230
+ raise UI_ServerBase::RetryConnection
231
+
232
+ when /^GET\s\/(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\sHTTP\/1.1/
233
+ raise 'Incorrect uuid' unless $1 == @uuid
234
+
235
+ @need_pause_at_first = false
236
+ CONFIG.set_config no_color: true
237
+
238
+ @ws_server = WebSocketServer.new(@sock)
239
+ @ws_server.handshake
102
240
  end
103
241
  end
104
242
 
243
+ def send_http_res body
244
+ json = JSON.generate body
245
+ 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"
246
+ @sock.puts "#{header}#{json}"
247
+ end
248
+
105
249
  module WebSocketUtils
106
250
  class Frame
107
251
  attr_reader :b
@@ -528,12 +672,6 @@ module DEBUGGER__
528
672
 
529
673
  ## Called by the SESSION thread
530
674
 
531
- def readline prompt
532
- return 'c' unless @q_msg
533
-
534
- @q_msg.pop || 'kill!'
535
- end
536
-
537
675
  def respond req, **result
538
676
  send_response req, **result
539
677
  end
@@ -561,6 +699,30 @@ module DEBUGGER__
561
699
  end
562
700
 
563
701
  class Session
702
+ include GlobalVariablesHelper
703
+
704
+ # FIXME: unify this method with ThreadClient#propertyDescriptor.
705
+ def get_type obj
706
+ case obj
707
+ when Array
708
+ ['object', 'array']
709
+ when Hash
710
+ ['object', 'map']
711
+ when String
712
+ ['string']
713
+ when TrueClass, FalseClass
714
+ ['boolean']
715
+ when Symbol
716
+ ['symbol']
717
+ when Integer, Float
718
+ ['number']
719
+ when Exception
720
+ ['object', 'error']
721
+ else
722
+ ['object']
723
+ end
724
+ end
725
+
564
726
  def fail_response req, **result
565
727
  @ui.respond_fail req, **result
566
728
  return :retry
@@ -584,17 +746,38 @@ module DEBUGGER__
584
746
  code: INVALID_PARAMS,
585
747
  message: "'callFrameId' is an invalid"
586
748
  end
587
- when 'Runtime.getProperties'
588
- oid = req.dig('params', 'objectId')
749
+ when 'Runtime.getProperties', 'Runtime.getExceptionDetails'
750
+ oid = req.dig('params', 'objectId') || req.dig('params', 'errorObjectId')
589
751
  if ref = @obj_map[oid]
590
752
  case ref[0]
591
753
  when 'local'
592
754
  frame_id = ref[1]
593
755
  fid = @frame_map[frame_id]
594
756
  request_tc [:cdp, :scope, req, fid]
757
+ when 'global'
758
+ vars = safe_global_variables.sort.map do |name|
759
+ gv = eval(name.to_s)
760
+ prop = {
761
+ name: name,
762
+ value: {
763
+ description: gv.inspect
764
+ },
765
+ configurable: true,
766
+ enumerable: true
767
+ }
768
+ type, subtype = get_type(gv)
769
+ prop[:value][:type] = type
770
+ prop[:value][:subtype] = subtype if subtype
771
+ prop
772
+ end
773
+
774
+ @ui.respond req, result: vars
775
+ return :retry
595
776
  when 'properties'
596
777
  request_tc [:cdp, :properties, req, oid]
597
- when 'script', 'global'
778
+ when 'exception'
779
+ request_tc [:cdp, :exception, req, oid]
780
+ when 'script'
598
781
  # TODO: Support script and global types
599
782
  @ui.respond req, result: []
600
783
  return :retry
@@ -730,6 +913,9 @@ module DEBUGGER__
730
913
  frame[:scriptId] = s_id
731
914
  end
732
915
  }
916
+ if oid = exc[:exception][:objectId]
917
+ @obj_map[oid] = ['exception']
918
+ end
733
919
  end
734
920
  rs = result.dig(:response, :result)
735
921
  [rs].each{|obj|
@@ -767,6 +953,8 @@ module DEBUGGER__
767
953
  }
768
954
  }
769
955
  @ui.respond req, **result
956
+ when :exception
957
+ @ui.respond req, **result
770
958
  end
771
959
  end
772
960
  end
@@ -857,7 +1045,7 @@ module DEBUGGER__
857
1045
  case expr
858
1046
  # Chrome doesn't read instance variables
859
1047
  when /\A\$\S/
860
- global_variables.each{|gvar|
1048
+ safe_global_variables.each{|gvar|
861
1049
  if gvar.to_s == expr
862
1050
  result = eval(gvar.to_s)
863
1051
  break false
@@ -872,8 +1060,8 @@ module DEBUGGER__
872
1060
  result = b.local_variable_get(expr)
873
1061
  rescue NameError
874
1062
  # try to check method
875
- if b.receiver.respond_to? expr, include_all: true
876
- result = b.receiver.method(expr)
1063
+ if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true)
1064
+ result = M_METHOD.bind_call(b.receiver, expr)
877
1065
  else
878
1066
  message = "Error: Can not evaluate: #{expr.inspect}"
879
1067
  end
@@ -886,35 +1074,7 @@ module DEBUGGER__
886
1074
  result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
887
1075
  rescue Exception => e
888
1076
  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
- }
1077
+ res[:exceptionDetails] = exceptionDetails(e, 'Uncaught')
918
1078
  ensure
919
1079
  output = $stdout.string
920
1080
  $stdout = orig_stdout
@@ -987,13 +1147,51 @@ module DEBUGGER__
987
1147
  ]
988
1148
  end
989
1149
 
990
- result += obj.instance_variables.map{|iv|
991
- variable(iv, obj.instance_variable_get(iv))
1150
+ result += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
1151
+ variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
992
1152
  }
993
- prop += [internalProperty('#class', obj.class)]
1153
+ prop += [internalProperty('#class', M_CLASS.bind_call(obj))]
994
1154
  end
995
1155
  event! :cdp_result, :properties, req, result: result, internalProperties: prop
1156
+ when :exception
1157
+ oid = args.shift
1158
+ exc = nil
1159
+ if obj = @obj_map[oid]
1160
+ exc = exceptionDetails obj, obj.to_s
1161
+ end
1162
+ event! :cdp_result, :exception, req, exceptionDetails: exc
1163
+ end
1164
+ end
1165
+
1166
+ def exceptionDetails exc, text
1167
+ frames = [
1168
+ {
1169
+ columnNumber: 0,
1170
+ functionName: 'eval',
1171
+ lineNumber: 0,
1172
+ url: ''
1173
+ }
1174
+ ]
1175
+ exc.backtrace_locations&.each do |loc|
1176
+ break if loc.path == __FILE__
1177
+ path = loc.absolute_path || loc.path
1178
+ frames << {
1179
+ columnNumber: 0,
1180
+ functionName: loc.base_label,
1181
+ lineNumber: loc.lineno - 1,
1182
+ url: path
1183
+ }
996
1184
  end
1185
+ {
1186
+ exceptionId: 1,
1187
+ text: text,
1188
+ lineNumber: 0,
1189
+ columnNumber: 0,
1190
+ exception: evaluate_result(exc),
1191
+ stackTrace: {
1192
+ callFrames: frames
1193
+ }
1194
+ }
997
1195
  end
998
1196
 
999
1197
  def search_const b, expr
@@ -1001,7 +1199,11 @@ module DEBUGGER__
1001
1199
  [Object, *b.eval('::Module.nesting')].reverse_each{|mod|
1002
1200
  if cs.all?{|c|
1003
1201
  if mod.const_defined?(c)
1004
- mod = mod.const_get(c)
1202
+ begin
1203
+ mod = mod.const_get(c)
1204
+ rescue Exception
1205
+ false
1206
+ end
1005
1207
  else
1006
1208
  false
1007
1209
  end
@@ -1045,25 +1247,29 @@ module DEBUGGER__
1045
1247
  v = prop[:value]
1046
1248
  v.delete :value
1047
1249
  v[:subtype] = subtype if subtype
1048
- v[:className] = obj.class
1250
+ v[:className] = (klass = M_CLASS.bind_call(obj)).name || klass.to_s
1049
1251
  end
1050
1252
  prop
1051
1253
  end
1052
1254
 
1053
1255
  def preview_ value, hash, overflow
1256
+ # The reason for not using "map" method is to prevent the object overriding it from causing bugs.
1257
+ # https://github.com/ruby/debug/issues/781
1258
+ props = []
1259
+ hash.each{|k, v|
1260
+ pd = propertyDescriptor k, v
1261
+ props << {
1262
+ name: pd[:name],
1263
+ type: pd[:value][:type],
1264
+ value: pd[:value][:description]
1265
+ }
1266
+ }
1054
1267
  {
1055
1268
  type: value[:type],
1056
1269
  subtype: value[:subtype],
1057
1270
  description: value[:description],
1058
1271
  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
- }
1272
+ properties: props
1067
1273
  }
1068
1274
  end
1069
1275