debug 1.2.2 → 1.3.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.
@@ -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?