debug 1.2.4 → 1.3.3

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.
@@ -0,0 +1,426 @@
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
+ @src_map = {}
100
+ loop do
101
+ req = @web_sock.extract_data
102
+ $stderr.puts '[>]' + req.inspect if SHOW_PROTOCOL
103
+
104
+ case req['method']
105
+
106
+ ## boot/configuration
107
+ when 'Page.getResourceTree'
108
+ abs = File.absolute_path($0)
109
+ src = File.read(abs)
110
+ @src_map[abs] = src
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 = get_source_code 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', 'start', 'lineNumber')
173
+ src = get_source_code s_id
174
+ end_line = src.count("\n")
175
+ line = end_line if line > end_line
176
+ send_response req,
177
+ locations: [
178
+ { scriptId: s_id,
179
+ lineNumber: line,
180
+ }
181
+ ]
182
+ when 'Debugger.setBreakpointByUrl'
183
+ line = req.dig('params', 'lineNumber')
184
+ path = req.dig('params', 'url').match('http://debuggee(.*)')[1]
185
+ cond = req.dig('params', 'condition')
186
+ src = get_source_code path
187
+ end_line = src.count("\n")
188
+ line = end_line if line > end_line
189
+ if cond != ''
190
+ bps << SESSION.add_line_breakpoint(path, line + 1, cond: cond)
191
+ else
192
+ bps << SESSION.add_line_breakpoint(path, line + 1)
193
+ end
194
+ send_response req,
195
+ breakpointId: (bps.size - 1).to_s,
196
+ locations: [
197
+ scriptId: path,
198
+ lineNumber: line
199
+ ]
200
+ when 'Debugger.removeBreakpoint'
201
+ b_id = req.dig('params', 'breakpointId')
202
+ @q_msg << "del #{b_id}"
203
+ send_response req
204
+
205
+ when 'Debugger.evaluateOnCallFrame', 'Runtime.getProperties'
206
+ @q_msg << req
207
+ end
208
+ end
209
+ end
210
+
211
+ def get_source_code path
212
+ return @src_map[path] if @src_map[path]
213
+
214
+ src = File.read(path)
215
+ @src_map[path] = src
216
+ src
217
+ end
218
+
219
+ ## Called by the SESSION thread
220
+
221
+ def readline prompt
222
+ @q_msg.pop || 'kill!'
223
+ end
224
+
225
+ def respond req, **result
226
+ send_response req, **result
227
+ end
228
+
229
+ def fire_event event, **result
230
+ if result.empty?
231
+ send_event event
232
+ else
233
+ send_event event, **result
234
+ end
235
+ end
236
+
237
+ def sock skip: false
238
+ yield $stderr
239
+ end
240
+
241
+ def puts result
242
+ # STDERR.puts "puts: #{result}"
243
+ # send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
244
+ end
245
+ end
246
+
247
+ class Session
248
+ def process_protocol_request req
249
+ case req['method']
250
+ when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource'
251
+ @tc << [:cdp, :backtrace, req]
252
+ when 'Debugger.evaluateOnCallFrame'
253
+ expr = req.dig('params', 'expression')
254
+ @tc << [:cdp, :evaluate, req, expr]
255
+ when 'Runtime.getProperties'
256
+ oid = req.dig('params', 'objectId')
257
+ case oid
258
+ when /(\d?):local/
259
+ @tc << [:cdp, :properties, req, $1.to_i]
260
+ when /\d?:script/
261
+ # TODO: Support a script type
262
+ @ui.respond req
263
+ return :retry
264
+ when /\d?:global/
265
+ # TODO: Support a global type
266
+ @ui.respond req
267
+ return :retry
268
+ end
269
+ end
270
+ end
271
+
272
+ def cdp_event args
273
+ type, req, result = args
274
+
275
+ case type
276
+ when :backtrace
277
+ result[:callFrames].each do |frame|
278
+ s_id = frame.dig(:location, :scriptId)
279
+ if File.exist?(s_id) && !@script_paths.include?(s_id)
280
+ src = File.read(s_id)
281
+ @ui.fire_event 'Debugger.scriptParsed',
282
+ scriptId: s_id,
283
+ url: frame[:url],
284
+ startLine: 0,
285
+ startColumn: 0,
286
+ endLine: src.count("\n"),
287
+ endColumn: 0,
288
+ executionContextId: @script_paths.size + 1,
289
+ hash: src.hash
290
+ @script_paths << s_id
291
+ end
292
+ end
293
+ result[:reason] = 'other'
294
+ @ui.fire_event 'Debugger.paused', **result
295
+ when :evaluate
296
+ @ui.respond req, result: result
297
+ when :properties
298
+ @ui.respond req, result: result
299
+ end
300
+ end
301
+ end
302
+
303
+ class ThreadClient
304
+ def process_cdp args
305
+ type = args.shift
306
+ req = args.shift
307
+
308
+ case type
309
+ when :backtrace
310
+ event! :cdp_result, :backtrace, req, {
311
+ callFrames: @target_frames.map.with_index{|frame, i|
312
+ path = frame.realpath || frame.path
313
+ if path.match /<internal:(.*)>/
314
+ abs = $1
315
+ else
316
+ abs = path
317
+ end
318
+
319
+ local_scope = {
320
+ callFrameId: SecureRandom.hex(16),
321
+ functionName: frame.name,
322
+ location: {
323
+ scriptId: abs,
324
+ lineNumber: frame.location.lineno - 1 # The line number is 0-based.
325
+ },
326
+ url: "http://debuggee#{abs}",
327
+ scopeChain: [
328
+ {
329
+ type: 'local',
330
+ object: {
331
+ type: 'object',
332
+ objectId: "#{i}:local"
333
+ }
334
+ },
335
+ {
336
+ type: 'script',
337
+ object: {
338
+ type: 'object',
339
+ objectId: "#{i}:script"
340
+ }
341
+ },
342
+ {
343
+ type: 'global',
344
+ object: {
345
+ type: 'object',
346
+ objectId: "#{i}:global"
347
+ }
348
+ }
349
+ ],
350
+ this: {
351
+ type: 'object'
352
+ }
353
+ }
354
+ }
355
+ }
356
+ when :evaluate
357
+ expr = args.shift
358
+ begin
359
+ result = current_frame.binding.eval(expr.to_s, '(DEBUG CONSOLE)')
360
+ rescue Exception => e
361
+ result = e
362
+ end
363
+ event! :cdp_result, :evaluate, req, evaluate_result(result)
364
+ when :properties
365
+ fid = args.shift
366
+ frame = @target_frames[fid]
367
+ if b = frame.binding
368
+ vars = b.local_variables.map{|name|
369
+ v = b.local_variable_get(name)
370
+ variable(name, v)
371
+ }
372
+ vars.unshift variable('%raised', frame.raised_exception) if frame.has_raised_exception
373
+ vars.unshift variable('%return', frame.return_value) if frame.has_return_value
374
+ vars.unshift variable('%self', b.receiver)
375
+ elsif lvars = frame.local_variables
376
+ vars = lvars.map{|var, val|
377
+ variable(var, val)
378
+ }
379
+ else
380
+ vars = [variable('%self', frame.self)]
381
+ vars.push variable('%raised', frame.raised_exception) if frame.has_raised_exception
382
+ vars.push variable('%return', frame.return_value) if frame.has_return_value
383
+ end
384
+ event! :cdp_result, :properties, req, vars
385
+ end
386
+ end
387
+
388
+ def evaluate_result r
389
+ v = variable nil, r
390
+ v[:value]
391
+ end
392
+
393
+ def variable_ name, obj, type, use_short: true
394
+ {
395
+ name: name,
396
+ value: {
397
+ type: type,
398
+ value: DEBUGGER__.short_inspect(obj, use_short)
399
+ },
400
+ configurable: true,
401
+ enumerable: true
402
+ }
403
+ end
404
+
405
+ def variable name, obj
406
+ case obj
407
+ when Array, Hash, Range, NilClass, Time
408
+ variable_ name, obj, 'object'
409
+ when String
410
+ variable_ name, obj, 'string', use_short: false
411
+ when Class, Module, Struct
412
+ variable_ name, obj, 'function'
413
+ when TrueClass, FalseClass
414
+ variable_ name, obj, 'boolean'
415
+ when Symbol
416
+ variable_ name, obj, 'symbol'
417
+ when Float
418
+ variable_ name, obj, 'number'
419
+ when Integer
420
+ variable_ name, obj, 'number'
421
+ else
422
+ variable_ name, obj, 'undefined'
423
+ end
424
+ end
425
+ end
426
+ 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?
@@ -461,7 +491,7 @@ module DEBUGGER__
461
491
  case type
462
492
  when :backtrace
463
493
  event! :dap_result, :backtrace, req, {
464
- stackFrames: @target_frames.map.with_index{|frame, i|
494
+ stackFrames: @target_frames.map.{|frame|
465
495
  path = frame.realpath || frame.path
466
496
  ref = frame.file_lines unless path && File.exist?(path)
467
497