debug 1.6.3 → 1.7.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,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