debug 1.2.3 → 1.3.2

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,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?