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.
- checksums.yaml +4 -4
- data/README.md +116 -5
- data/ext/debug/debug.c +2 -1
- data/ext/debug/extconf.rb +2 -0
- data/lib/debug/breakpoint.rb +2 -2
- data/lib/debug/client.rb +69 -46
- data/lib/debug/config.rb +74 -21
- data/lib/debug/console.rb +92 -25
- data/lib/debug/local.rb +22 -1
- data/lib/debug/prelude.rb +49 -0
- data/lib/debug/server.rb +186 -25
- data/lib/debug/server_cdp.rb +412 -0
- data/lib/debug/server_dap.rb +53 -23
- data/lib/debug/session.rb +387 -120
- data/lib/debug/thread_client.rb +4 -2
- data/lib/debug/version.rb +1 -1
- data/misc/README.md.erb +95 -1
- metadata +8 -6
@@ -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
|
data/lib/debug/server_dap.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
84
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
@
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
330
|
+
def process_protocol_request req
|
301
331
|
case req['command']
|
302
332
|
when 'stepBack'
|
303
333
|
if @tc.recorder&.can_step_back?
|