debug 1.2.3 → 1.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,410 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'digest/sha1'
5
+ require 'base64'
6
+ require 'securerandom'
7
+
8
+ module DEBUGGER__
9
+ module UI_CDP
10
+ SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
11
+
12
+ class WebSocket
13
+ def initialize s
14
+ @sock = s
15
+ end
16
+
17
+ def handshake
18
+ req = @sock.readpartial 4096
19
+ $stderr.puts '[>]' + req if SHOW_PROTOCOL
20
+
21
+ if req.match /^Sec-WebSocket-Key: (.*)\r\n/
22
+ accept = Base64.strict_encode64 Digest::SHA1.digest "#{$1}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
23
+ @sock.print "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: #{accept}\r\n\r\n"
24
+ else
25
+ "Unknown request: #{req}"
26
+ end
27
+ end
28
+
29
+ def send **msg
30
+ msg = JSON.generate(msg)
31
+ frame = []
32
+ fin = 0b10000000
33
+ opcode = 0b00000001
34
+ frame << fin + opcode
35
+
36
+ mask = 0b00000000 # A server must not mask any frames in a WebSocket Protocol.
37
+ bytesize = msg.bytesize
38
+ if bytesize < 126
39
+ payload_len = bytesize
40
+ elsif bytesize < 2 ** 16
41
+ payload_len = 0b01111110
42
+ ex_payload_len = [bytesize].pack('n*').bytes
43
+ else
44
+ payload_len = 0b01111111
45
+ ex_payload_len = [bytesize].pack('Q>').bytes
46
+ end
47
+
48
+ frame << mask + payload_len
49
+ frame.push *ex_payload_len if ex_payload_len
50
+ frame.push *msg.bytes
51
+ @sock.print frame.pack 'c*'
52
+ end
53
+
54
+ def extract_data
55
+ first_group = @sock.getbyte
56
+ fin = first_group & 0b10000000 != 128
57
+ raise 'Unsupported' if fin
58
+ opcode = first_group & 0b00001111
59
+ raise "Unsupported: #{opcode}" unless opcode == 1
60
+
61
+ second_group = @sock.getbyte
62
+ mask = second_group & 0b10000000 == 128
63
+ raise 'The client must mask all frames' unless mask
64
+ payload_len = second_group & 0b01111111
65
+ # TODO: Support other payload_lengths
66
+ if payload_len == 126
67
+ payload_len = @sock.gets(2).unpack('n*')[0]
68
+ end
69
+
70
+ masking_key = []
71
+ 4.times { masking_key << @sock.getbyte }
72
+ unmasked = []
73
+ payload_len.times do |n|
74
+ masked = @sock.getbyte
75
+ unmasked << (masked ^ masking_key[n % 4])
76
+ end
77
+ JSON.parse unmasked.pack 'c*'
78
+ end
79
+ end
80
+
81
+ def send_response req, **res
82
+ if res.empty?
83
+ @web_sock.send id: req['id'], result: {}
84
+ else
85
+ @web_sock.send id: req['id'], result: res
86
+ end
87
+ end
88
+
89
+ def send_event method, **params
90
+ if params.empty?
91
+ @web_sock.send method: method, params: {}
92
+ else
93
+ @web_sock.send method: method, params: params
94
+ end
95
+ end
96
+
97
+ def process
98
+ bps = []
99
+ loop do
100
+ req = @web_sock.extract_data
101
+ $stderr.puts '[>]' + req.inspect if SHOW_PROTOCOL
102
+
103
+ case req['method']
104
+
105
+ ## boot/configuration
106
+ when 'Page.getResourceTree'
107
+ abs = File.absolute_path($0)
108
+ src = File.read(abs)
109
+ send_response req,
110
+ frameTree: {
111
+ frame: {
112
+ id: SecureRandom.hex(16),
113
+ loaderId: SecureRandom.hex(16),
114
+ url: 'http://debuggee/',
115
+ securityOrigin: 'http://debuggee',
116
+ mimeType: 'text/plain' },
117
+ resources: [
118
+ ]
119
+ }
120
+ send_event 'Debugger.scriptParsed',
121
+ scriptId: abs,
122
+ url: "http://debuggee#{abs}",
123
+ startLine: 0,
124
+ startColumn: 0,
125
+ endLine: src.count("\n"),
126
+ endColumn: 0,
127
+ executionContextId: 1,
128
+ hash: src.hash
129
+ send_event 'Runtime.executionContextCreated',
130
+ context: {
131
+ id: SecureRandom.hex(16),
132
+ origin: "http://#{@addr}",
133
+ name: ''
134
+ }
135
+ when 'Debugger.getScriptSource'
136
+ s_id = req.dig('params', 'scriptId')
137
+ src = File.read(s_id)
138
+ send_response req, scriptSource: src
139
+ @q_msg << req
140
+ when 'Page.startScreencast', 'Emulation.setTouchEmulationEnabled', 'Emulation.setEmitTouchEventsForMouse',
141
+ 'Runtime.compileScript', 'Page.getResourceContent', 'Overlay.setPausedInDebuggerMessage',
142
+ 'Debugger.setBreakpointsActive', 'Runtime.releaseObjectGroup'
143
+ send_response req
144
+
145
+ ## control
146
+ when 'Debugger.resume'
147
+ @q_msg << 'c'
148
+ @q_msg << req
149
+ send_response req
150
+ send_event 'Debugger.resumed'
151
+ when 'Debugger.stepOver'
152
+ @q_msg << 'n'
153
+ @q_msg << req
154
+ send_response req
155
+ send_event 'Debugger.resumed'
156
+ when 'Debugger.stepInto'
157
+ @q_msg << 's'
158
+ @q_msg << req
159
+ send_response req
160
+ send_event 'Debugger.resumed'
161
+ when 'Debugger.stepOut'
162
+ @q_msg << 'fin'
163
+ @q_msg << req
164
+ send_response req
165
+ send_event 'Debugger.resumed'
166
+
167
+ # breakpoint
168
+ when 'Debugger.getPossibleBreakpoints'
169
+ s_id = req.dig('params', 'start', 'scriptId')
170
+ line = req.dig('params', 'start', 'lineNumber')
171
+ send_response req,
172
+ locations: [
173
+ { scriptId: s_id,
174
+ lineNumber: line,
175
+ }
176
+ ]
177
+ when 'Debugger.setBreakpointByUrl'
178
+ line = req.dig('params', 'lineNumber')
179
+ path = req.dig('params', 'url').match('http://debuggee(.*)')[1]
180
+ cond = req.dig('params', 'condition')
181
+ if cond != ''
182
+ bps << SESSION.add_line_breakpoint(path, line + 1, cond: cond)
183
+ else
184
+ bps << SESSION.add_line_breakpoint(path, line + 1)
185
+ end
186
+ send_response req,
187
+ breakpointId: (bps.size - 1).to_s,
188
+ locations: [
189
+ scriptId: path,
190
+ lineNumber: line
191
+ ]
192
+ when 'Debugger.removeBreakpoint'
193
+ b_id = req.dig('params', 'breakpointId')
194
+ @q_msg << "del #{b_id}"
195
+ send_response req
196
+
197
+ when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
198
+ @q_msg << req
199
+ end
200
+ end
201
+ end
202
+
203
+ ## Called by the SESSION thread
204
+
205
+ def readline prompt
206
+ @q_msg.pop || 'kill!'
207
+ end
208
+
209
+ def respond req, **result
210
+ send_response req, **result
211
+ end
212
+
213
+ def fire_event event, **result
214
+ if result.empty?
215
+ send_event event
216
+ else
217
+ send_event event, **result
218
+ end
219
+ end
220
+
221
+ def sock skip: false
222
+ yield $stderr
223
+ end
224
+
225
+ def puts result
226
+ # STDERR.puts "puts: #{result}"
227
+ # send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
228
+ end
229
+ end
230
+
231
+ class Session
232
+ def process_protocol_request req
233
+ case req['method']
234
+ when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource'
235
+ @tc << [:cdp, :backtrace, req]
236
+ when 'Debugger.evaluateOnCallFrame'
237
+ expr = req.dig('params', 'expression')
238
+ @tc << [:cdp, :evaluate, req, expr]
239
+ when 'Runtime.getProperties'
240
+ oid = req.dig('params', 'objectId')
241
+ case oid
242
+ when /(\d?):local/
243
+ @tc << [:cdp, :properties, req, $1.to_i]
244
+ when /\d?:script/
245
+ # TODO: Support a script type
246
+ @ui.respond req
247
+ return :retry
248
+ when /\d?:global/
249
+ # TODO: Support a global type
250
+ @ui.respond req
251
+ return :retry
252
+ end
253
+ end
254
+ end
255
+
256
+ def cdp_event args
257
+ type, req, result = args
258
+
259
+ case type
260
+ when :backtrace
261
+ result[:callFrames].each do |frame|
262
+ s_id = frame.dig(:location, :scriptId)
263
+ if File.exist?(s_id) && !@script_paths.include?(s_id)
264
+ src = File.read(s_id)
265
+ @ui.fire_event 'Debugger.scriptParsed',
266
+ scriptId: s_id,
267
+ url: frame[:url],
268
+ startLine: 0,
269
+ startColumn: 0,
270
+ endLine: src.count("\n"),
271
+ endColumn: 0,
272
+ executionContextId: @script_paths.size + 1,
273
+ hash: src.hash
274
+ @script_paths << s_id
275
+ end
276
+ end
277
+ result[:reason] = 'other'
278
+ @ui.fire_event 'Debugger.paused', **result
279
+ when :evaluate
280
+ @ui.respond req, result: result
281
+ when :properties
282
+ @ui.respond req, result: result
283
+ end
284
+ end
285
+ end
286
+
287
+ class ThreadClient
288
+ def process_cdp args
289
+ type = args.shift
290
+ req = args.shift
291
+
292
+ case type
293
+ when :backtrace
294
+ event! :cdp_result, :backtrace, req, {
295
+ callFrames: @target_frames.map.with_index{|frame, i|
296
+ path = frame.realpath || frame.path
297
+ if path.match /<internal:(.*)>/
298
+ abs = $1
299
+ else
300
+ abs = path
301
+ end
302
+
303
+ local_scope = {
304
+ callFrameId: SecureRandom.hex(16),
305
+ functionName: frame.name,
306
+ location: {
307
+ scriptId: abs,
308
+ lineNumber: frame.location.lineno - 1 # The line number is 0-based.
309
+ },
310
+ url: "http://debuggee#{abs}",
311
+ scopeChain: [
312
+ {
313
+ type: 'local',
314
+ object: {
315
+ type: 'object',
316
+ objectId: "#{i}:local"
317
+ }
318
+ },
319
+ {
320
+ type: 'script',
321
+ object: {
322
+ type: 'object',
323
+ objectId: "#{i}:script"
324
+ }
325
+ },
326
+ {
327
+ type: 'global',
328
+ object: {
329
+ type: 'object',
330
+ objectId: "#{i}:global"
331
+ }
332
+ }
333
+ ],
334
+ this: {
335
+ type: 'object'
336
+ }
337
+ }
338
+ }
339
+ }
340
+ when :evaluate
341
+ expr = args.shift
342
+ begin
343
+ result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
344
+ rescue Exception => e
345
+ result = e
346
+ end
347
+ event! :cdp_result, :evaluate, req, evaluate_result(result)
348
+ when :properties
349
+ fid = args.shift
350
+ frame = @target_frames[fid]
351
+ if b = frame.binding
352
+ vars = b.local_variables.map{|name|
353
+ v = b.local_variable_get(name)
354
+ variable(name, v)
355
+ }
356
+ vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
357
+ vars.unshift variable('%return', frame.return_value) if frame.has_return_value
358
+ vars.unshift variable('%self', b.receiver)
359
+ elsif lvars = frame.local_variables
360
+ vars = lvars.map{|var, val|
361
+ variable(var, val)
362
+ }
363
+ else
364
+ vars = [variable('%self', frame.self)]
365
+ vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
366
+ vars.push variable('%return', frame.return_value) if frame.has_return_value
367
+ end
368
+ event! :cdp_result, :properties, req, vars
369
+ end
370
+ end
371
+
372
+ def evaluate_result r
373
+ v = variable nil, r
374
+ v[:value]
375
+ end
376
+
377
+ def variable_ name, obj, type, use_short: true
378
+ {
379
+ name: name,
380
+ value: {
381
+ type: type,
382
+ value: DEBUGGER__.short_inspect(obj, use_short)
383
+ },
384
+ configurable: true,
385
+ enumerable: true
386
+ }
387
+ end
388
+
389
+ def variable name, obj
390
+ case obj
391
+ when Array, Hash, Range, NilClass, Time
392
+ variable_ name, obj, 'object'
393
+ when String
394
+ variable_ name, obj, 'string', use_short: false
395
+ when Class, Module, Struct
396
+ variable_ name, obj, 'function'
397
+ when TrueClass, FalseClass
398
+ variable_ name, obj, 'boolean'
399
+ when Symbol
400
+ variable_ name, obj, 'symbol'
401
+ when Float
402
+ variable_ name, obj, 'number'
403
+ when Integer
404
+ variable_ name, obj, 'number'
405
+ else
406
+ variable_ name, obj, 'undefined'
407
+ end
408
+ end
409
+ end
410
+ end
@@ -6,11 +6,17 @@ module DEBUGGER__
6
6
  module UI_DAP
7
7
  SHOW_PROTOCOL = ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
8
8
 
9
+ def show_protocol dir, msg
10
+ if SHOW_PROTOCOL
11
+ $stderr.puts "\##{Process.pid}:[#{dir}] #{msg}"
12
+ end
13
+ end
14
+
9
15
  def dap_setup bytes
10
16
  CONFIG.set_config no_color: true
11
17
  @seq = 0
12
18
 
13
- $stderr.puts '[>]' + bytes if SHOW_PROTOCOL
19
+ show_protocol :>, bytes
14
20
  req = JSON.load(bytes)
15
21
 
16
22
  # capability
@@ -80,10 +86,8 @@ module DEBUGGER__
80
86
  def send **kw
81
87
  kw[:seq] = @seq += 1
82
88
  str = JSON.dump(kw)
83
- $stderr.puts "[<] #{str}" if SHOW_PROTOCOL
84
- # STDERR.puts "[STDERR] [<] #{str}"
85
- @sock.print header = "Content-Length: #{str.size}\r\n\r\n"
86
- @sock.write str
89
+ show_protocol '<', str
90
+ @sock.write "Content-Length: #{str.size}\r\n\r\n#{str}"
87
91
  end
88
92
 
89
93
  def send_response req, success: true, **kw
@@ -111,19 +115,32 @@ module DEBUGGER__
111
115
  end
112
116
  end
113
117
 
118
+ class RetryBecauseCantRead < Exception
119
+ end
120
+
114
121
  def recv_request
115
- case header = @sock.gets
116
- when /Content-Length: (\d+)/
117
- b = @sock.read(2)
118
- raise b.inspect unless b == "\r\n"
119
-
120
- l = @sock.read(s = $1.to_i)
121
- $stderr.puts "[>] #{l}" if SHOW_PROTOCOL
122
- JSON.load(l)
123
- when nil
124
- nil
125
- else
126
- raise "unrecognized line: #{l} (#{l.size} bytes)"
122
+ begin
123
+ r = IO.select([@sock])
124
+
125
+ @session.process_group.sync do
126
+ raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)
127
+
128
+ case header = @sock.gets
129
+ when /Content-Length: (\d+)/
130
+ b = @sock.read(2)
131
+ raise b.inspect unless b == "\r\n"
132
+
133
+ l = @sock.read(s = $1.to_i)
134
+ show_protocol :>, l
135
+ JSON.load(l)
136
+ when nil
137
+ nil
138
+ else
139
+ raise "unrecognized line: #{l} (#{l.size} bytes)"
140
+ end
141
+ end
142
+ rescue RetryBecauseCantRead
143
+ retry
127
144
  end
128
145
  end
129
146
 
@@ -137,6 +154,11 @@ module DEBUGGER__
137
154
  ## boot/configuration
138
155
  when 'launch'
139
156
  send_response req
157
+ @is_attach = false
158
+ when 'attach'
159
+ send_response req
160
+ Process.kill(:SIGURG, Process.pid)
161
+ @is_attach = true
140
162
  when 'setBreakpoints'
141
163
  path = args.dig('source', 'path')
142
164
  bp_args = args['breakpoints']
@@ -179,13 +201,21 @@ module DEBUGGER__
179
201
  send_response req, breakpoints: filters
180
202
  when 'configurationDone'
181
203
  send_response req
182
- @q_msg << 'continue'
183
- when 'attach'
184
- send_response req
185
- Process.kill(:SIGURG, Process.pid)
204
+ if defined?(@is_attach) && @is_attach
205
+ @q_msg << 'p'
206
+ send_event 'stopped', reason: 'pause',
207
+ threadId: 1,
208
+ allThreadsStopped: true
209
+ else
210
+ @q_msg << 'continue'
211
+ end
186
212
  when 'disconnect'
213
+ if args.fetch("terminateDebuggee", false)
214
+ @q_msg << 'kill!'
215
+ else
216
+ @q_msg << 'continue'
217
+ end
187
218
  send_response req
188
- @q_msg << 'continue'
189
219
 
190
220
  ## control
191
221
  when 'continue'
@@ -297,7 +327,7 @@ module DEBUGGER__
297
327
  return :retry
298
328
  end
299
329
 
300
- def process_dap_request req
330
+ def process_protocol_request req
301
331
  case req['command']
302
332
  when 'stepBack'
303
333
  if @tc.recorder&.can_step_back?