debug 1.2.2 → 1.3.1

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