ed-precompiled_debug 1.11.0-x86_64-linux
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 +7 -0
- data/CONTRIBUTING.md +573 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +996 -0
- data/Rakefile +57 -0
- data/TODO.md +23 -0
- data/debug.gemspec +33 -0
- data/exe/rdbg +53 -0
- data/ext/debug/debug.c +228 -0
- data/ext/debug/extconf.rb +27 -0
- data/ext/debug/iseq_collector.c +93 -0
- data/lib/debug/3.0/debug.so +0 -0
- data/lib/debug/3.1/debug.so +0 -0
- data/lib/debug/3.2/debug.so +0 -0
- data/lib/debug/3.3/debug.so +0 -0
- data/lib/debug/3.4/debug.so +0 -0
- data/lib/debug/abbrev_command.rb +77 -0
- data/lib/debug/breakpoint.rb +556 -0
- data/lib/debug/client.rb +263 -0
- data/lib/debug/color.rb +123 -0
- data/lib/debug/config.rb +592 -0
- data/lib/debug/console.rb +224 -0
- data/lib/debug/dap_custom/traceInspector.rb +336 -0
- data/lib/debug/debug.so +0 -0
- data/lib/debug/frame_info.rb +191 -0
- data/lib/debug/irb_integration.rb +37 -0
- data/lib/debug/local.rb +115 -0
- data/lib/debug/open.rb +13 -0
- data/lib/debug/open_nonstop.rb +15 -0
- data/lib/debug/prelude.rb +50 -0
- data/lib/debug/server.rb +534 -0
- data/lib/debug/server_cdp.rb +1348 -0
- data/lib/debug/server_dap.rb +1108 -0
- data/lib/debug/session.rb +2667 -0
- data/lib/debug/source_repository.rb +150 -0
- data/lib/debug/start.rb +5 -0
- data/lib/debug/thread_client.rb +1457 -0
- data/lib/debug/tracer.rb +241 -0
- data/lib/debug/version.rb +5 -0
- data/lib/debug.rb +9 -0
- data/misc/README.md.erb +660 -0
- metadata +118 -0
@@ -0,0 +1,1108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'irb/completion'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
module DEBUGGER__
|
9
|
+
module UI_DAP
|
10
|
+
SHOW_PROTOCOL = ENV['DEBUG_DAP_SHOW_PROTOCOL'] == '1' || ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
|
11
|
+
|
12
|
+
def self.setup debug_port
|
13
|
+
if File.directory? '.vscode'
|
14
|
+
dir = Dir.pwd
|
15
|
+
else
|
16
|
+
dir = Dir.mktmpdir("ruby-debug-vscode-")
|
17
|
+
tempdir = true
|
18
|
+
end
|
19
|
+
|
20
|
+
at_exit do
|
21
|
+
DEBUGGER__.skip_all
|
22
|
+
FileUtils.rm_rf dir if tempdir
|
23
|
+
end
|
24
|
+
|
25
|
+
key = rand.to_s
|
26
|
+
|
27
|
+
Dir.chdir(dir) do
|
28
|
+
Dir.mkdir('.vscode') if tempdir
|
29
|
+
|
30
|
+
# vscode-rdbg 0.0.9 or later is needed
|
31
|
+
open('.vscode/rdbg_autoattach.json', 'w') do |f|
|
32
|
+
f.puts JSON.pretty_generate({
|
33
|
+
type: "rdbg",
|
34
|
+
name: "Attach with rdbg",
|
35
|
+
request: "attach",
|
36
|
+
rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
|
37
|
+
debugPort: debug_port,
|
38
|
+
localfs: true,
|
39
|
+
autoAttach: key,
|
40
|
+
})
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
cmds = ['code', "#{dir}/"]
|
45
|
+
cmdline = cmds.join(' ')
|
46
|
+
ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/"
|
47
|
+
|
48
|
+
STDERR.puts "Launching: #{cmdline}"
|
49
|
+
env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
|
50
|
+
env['RUBY_DEBUG_AUTOATTACH'] = key
|
51
|
+
|
52
|
+
unless system(env, *cmds)
|
53
|
+
DEBUGGER__.warn <<~MESSAGE
|
54
|
+
Can not invoke the command.
|
55
|
+
Use the command-line on your terminal (with modification if you need).
|
56
|
+
|
57
|
+
#{cmdline}
|
58
|
+
|
59
|
+
If your application is running on a SSH remote host, please try:
|
60
|
+
|
61
|
+
#{ssh_cmdline}
|
62
|
+
|
63
|
+
MESSAGE
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def show_protocol dir, msg
|
68
|
+
if SHOW_PROTOCOL
|
69
|
+
$stderr.puts "\##{Process.pid}:[#{dir}] #{msg}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# true: all localfs
|
74
|
+
# Array: part of localfs
|
75
|
+
# nil: no localfs
|
76
|
+
@local_fs_map = nil
|
77
|
+
|
78
|
+
def self.remote_to_local_path path
|
79
|
+
case @local_fs_map
|
80
|
+
when nil
|
81
|
+
nil
|
82
|
+
when true
|
83
|
+
path
|
84
|
+
else # Array
|
85
|
+
@local_fs_map.each do |(remote_path_prefix, local_path_prefix)|
|
86
|
+
if path.start_with? remote_path_prefix
|
87
|
+
return path.sub(remote_path_prefix){ local_path_prefix }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.local_to_remote_path path
|
96
|
+
case @local_fs_map
|
97
|
+
when nil
|
98
|
+
nil
|
99
|
+
when true
|
100
|
+
path
|
101
|
+
else # Array
|
102
|
+
@local_fs_map.each do |(remote_path_prefix, local_path_prefix)|
|
103
|
+
if path.start_with? local_path_prefix
|
104
|
+
return path.sub(local_path_prefix){ remote_path_prefix }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.local_fs_map_set map
|
113
|
+
return if @local_fs_map # already setup
|
114
|
+
|
115
|
+
case map
|
116
|
+
when String
|
117
|
+
@local_fs_map = map.split(',').map{|e| e.split(':').map{|path| path.delete_suffix('/') + '/'}}
|
118
|
+
when true
|
119
|
+
@local_fs_map = map
|
120
|
+
when nil
|
121
|
+
@local_fs_map = CONFIG[:local_fs_map]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def dap_setup bytes
|
126
|
+
CONFIG.set_config no_color: true
|
127
|
+
@seq = 0
|
128
|
+
@send_lock = Mutex.new
|
129
|
+
|
130
|
+
case self
|
131
|
+
when UI_UnixDomainServer
|
132
|
+
# If the user specified a mapping, respect it, otherwise, make sure that no mapping is used
|
133
|
+
UI_DAP.local_fs_map_set CONFIG[:local_fs_map] || true
|
134
|
+
when UI_TcpServer
|
135
|
+
# TODO: loopback address can be used to connect other FS env, like Docker containers
|
136
|
+
# UI_DAP.local_fs_set if @local_addr.ipv4_loopback? || @local_addr.ipv6_loopback?
|
137
|
+
end
|
138
|
+
|
139
|
+
show_protocol :>, bytes
|
140
|
+
req = JSON.load(bytes)
|
141
|
+
|
142
|
+
# capability
|
143
|
+
send_response(req,
|
144
|
+
## Supported
|
145
|
+
supportsConfigurationDoneRequest: true,
|
146
|
+
supportsFunctionBreakpoints: true,
|
147
|
+
supportsConditionalBreakpoints: true,
|
148
|
+
supportTerminateDebuggee: true,
|
149
|
+
supportsTerminateRequest: true,
|
150
|
+
exceptionBreakpointFilters: [
|
151
|
+
{
|
152
|
+
filter: 'any',
|
153
|
+
label: 'rescue any exception',
|
154
|
+
supportsCondition: true,
|
155
|
+
#conditionDescription: '',
|
156
|
+
},
|
157
|
+
{
|
158
|
+
filter: 'RuntimeError',
|
159
|
+
label: 'rescue RuntimeError',
|
160
|
+
supportsCondition: true,
|
161
|
+
#conditionDescription: '',
|
162
|
+
},
|
163
|
+
],
|
164
|
+
supportsExceptionFilterOptions: true,
|
165
|
+
supportsStepBack: true,
|
166
|
+
supportsEvaluateForHovers: true,
|
167
|
+
supportsCompletionsRequest: true,
|
168
|
+
|
169
|
+
## Will be supported
|
170
|
+
# supportsExceptionOptions: true,
|
171
|
+
# supportsHitConditionalBreakpoints:
|
172
|
+
# supportsSetVariable: true,
|
173
|
+
# supportSuspendDebuggee:
|
174
|
+
# supportsLogPoints:
|
175
|
+
# supportsLoadedSourcesRequest:
|
176
|
+
# supportsDataBreakpoints:
|
177
|
+
# supportsBreakpointLocationsRequest:
|
178
|
+
|
179
|
+
## Possible?
|
180
|
+
# supportsRestartFrame:
|
181
|
+
# completionTriggerCharacters:
|
182
|
+
# supportsModulesRequest:
|
183
|
+
# additionalModuleColumns:
|
184
|
+
# supportedChecksumAlgorithms:
|
185
|
+
# supportsRestartRequest:
|
186
|
+
# supportsValueFormattingOptions:
|
187
|
+
# supportsExceptionInfoRequest:
|
188
|
+
# supportsDelayedStackTraceLoading:
|
189
|
+
# supportsTerminateThreadsRequest:
|
190
|
+
# supportsSetExpression:
|
191
|
+
# supportsClipboardContext:
|
192
|
+
|
193
|
+
## Never
|
194
|
+
# supportsGotoTargetsRequest:
|
195
|
+
# supportsStepInTargetsRequest:
|
196
|
+
# supportsReadMemoryRequest:
|
197
|
+
# supportsDisassembleRequest:
|
198
|
+
# supportsCancelRequest:
|
199
|
+
# supportsSteppingGranularity:
|
200
|
+
# supportsInstructionBreakpoints:
|
201
|
+
)
|
202
|
+
send_event 'initialized'
|
203
|
+
puts <<~WELCOME
|
204
|
+
Ruby REPL: You can run any Ruby expression here.
|
205
|
+
Note that output to the STDOUT/ERR printed on the TERMINAL.
|
206
|
+
[experimental]
|
207
|
+
`,COMMAND` runs `COMMAND` debug command (ex: `,info`).
|
208
|
+
`,help` to list all debug commands.
|
209
|
+
WELCOME
|
210
|
+
end
|
211
|
+
|
212
|
+
def send **kw
|
213
|
+
if sock = @sock
|
214
|
+
kw[:seq] = @seq += 1
|
215
|
+
str = JSON.dump(kw)
|
216
|
+
@send_lock.synchronize do
|
217
|
+
sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
|
218
|
+
end
|
219
|
+
show_protocol '<', str
|
220
|
+
end
|
221
|
+
rescue Errno::EPIPE => e
|
222
|
+
$stderr.puts "#{e.inspect} rescued during sending message"
|
223
|
+
end
|
224
|
+
|
225
|
+
def send_response req, success: true, message: nil, **kw
|
226
|
+
if kw.empty?
|
227
|
+
send type: 'response',
|
228
|
+
command: req['command'],
|
229
|
+
request_seq: req['seq'],
|
230
|
+
success: success,
|
231
|
+
message: message || (success ? 'Success' : 'Failed')
|
232
|
+
else
|
233
|
+
send type: 'response',
|
234
|
+
command: req['command'],
|
235
|
+
request_seq: req['seq'],
|
236
|
+
success: success,
|
237
|
+
message: message || (success ? 'Success' : 'Failed'),
|
238
|
+
body: kw
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def send_event name, **kw
|
243
|
+
if kw.empty?
|
244
|
+
send type: 'event', event: name
|
245
|
+
else
|
246
|
+
send type: 'event', event: name, body: kw
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
class RetryBecauseCantRead < Exception
|
251
|
+
end
|
252
|
+
|
253
|
+
def recv_request
|
254
|
+
IO.select([@sock])
|
255
|
+
|
256
|
+
@session.process_group.sync do
|
257
|
+
raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)
|
258
|
+
|
259
|
+
case @sock.gets
|
260
|
+
when /Content-Length: (\d+)/
|
261
|
+
b = @sock.read(2)
|
262
|
+
raise b.inspect unless b == "\r\n"
|
263
|
+
|
264
|
+
l = @sock.read($1.to_i)
|
265
|
+
show_protocol :>, l
|
266
|
+
JSON.load(l)
|
267
|
+
when nil
|
268
|
+
nil
|
269
|
+
else
|
270
|
+
raise "unrecognized line: #{l} (#{l.size} bytes)"
|
271
|
+
end
|
272
|
+
end
|
273
|
+
rescue RetryBecauseCantRead
|
274
|
+
retry
|
275
|
+
end
|
276
|
+
|
277
|
+
def load_extensions req
|
278
|
+
if exts = req.dig('arguments', 'rdbgExtensions')
|
279
|
+
exts.each{|ext|
|
280
|
+
require_relative "dap_custom/#{File.basename(ext)}"
|
281
|
+
}
|
282
|
+
end
|
283
|
+
|
284
|
+
if scripts = req.dig('arguments', 'rdbgInitialScripts')
|
285
|
+
scripts.each do |script|
|
286
|
+
begin
|
287
|
+
eval(script)
|
288
|
+
rescue Exception => e
|
289
|
+
puts e.message
|
290
|
+
puts e.backtrace.inspect
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def process
|
297
|
+
while req = recv_request
|
298
|
+
process_request(req)
|
299
|
+
end
|
300
|
+
ensure
|
301
|
+
send_event :terminated unless @sock.closed?
|
302
|
+
end
|
303
|
+
|
304
|
+
def process_request req
|
305
|
+
raise "not a request: #{req.inspect}" unless req['type'] == 'request'
|
306
|
+
args = req.dig('arguments')
|
307
|
+
|
308
|
+
case req['command']
|
309
|
+
|
310
|
+
## boot/configuration
|
311
|
+
when 'launch'
|
312
|
+
send_response req
|
313
|
+
# `launch` runs on debuggee on the same file system
|
314
|
+
UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap') || true
|
315
|
+
@nonstop = true
|
316
|
+
|
317
|
+
load_extensions req
|
318
|
+
|
319
|
+
when 'attach'
|
320
|
+
send_response req
|
321
|
+
UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap')
|
322
|
+
|
323
|
+
if req.dig('arguments', 'nonstop') == true
|
324
|
+
@nonstop = true
|
325
|
+
else
|
326
|
+
@nonstop = false
|
327
|
+
end
|
328
|
+
|
329
|
+
load_extensions req
|
330
|
+
|
331
|
+
when 'configurationDone'
|
332
|
+
send_response req
|
333
|
+
|
334
|
+
if @nonstop
|
335
|
+
@q_msg << 'continue'
|
336
|
+
else
|
337
|
+
if SESSION.in_subsession?
|
338
|
+
send_event 'stopped', reason: 'pause',
|
339
|
+
threadId: 1, # maybe ...
|
340
|
+
allThreadsStopped: true
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
when 'setBreakpoints'
|
345
|
+
req_path = args.dig('source', 'path')
|
346
|
+
path = UI_DAP.local_to_remote_path(req_path)
|
347
|
+
if path
|
348
|
+
SESSION.clear_line_breakpoints path
|
349
|
+
|
350
|
+
bps = []
|
351
|
+
args['breakpoints'].each{|bp|
|
352
|
+
line = bp['line']
|
353
|
+
if cond = bp['condition']
|
354
|
+
bps << SESSION.add_line_breakpoint(path, line, cond: cond)
|
355
|
+
else
|
356
|
+
bps << SESSION.add_line_breakpoint(path, line)
|
357
|
+
end
|
358
|
+
}
|
359
|
+
send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
|
360
|
+
else
|
361
|
+
send_response req, success: false, message: "#{req_path} is not available"
|
362
|
+
end
|
363
|
+
|
364
|
+
when 'setFunctionBreakpoints'
|
365
|
+
send_response req
|
366
|
+
|
367
|
+
when 'setExceptionBreakpoints'
|
368
|
+
process_filter = ->(filter_id, cond = nil) {
|
369
|
+
bp =
|
370
|
+
case filter_id
|
371
|
+
when 'any'
|
372
|
+
SESSION.add_catch_breakpoint 'Exception', cond: cond
|
373
|
+
when 'RuntimeError'
|
374
|
+
SESSION.add_catch_breakpoint 'RuntimeError', cond: cond
|
375
|
+
else
|
376
|
+
nil
|
377
|
+
end
|
378
|
+
{
|
379
|
+
verified: !bp.nil?,
|
380
|
+
message: bp.inspect,
|
381
|
+
}
|
382
|
+
}
|
383
|
+
|
384
|
+
SESSION.clear_catch_breakpoints 'Exception', 'RuntimeError'
|
385
|
+
|
386
|
+
filters = args.fetch('filters').map {|filter_id|
|
387
|
+
process_filter.call(filter_id)
|
388
|
+
}
|
389
|
+
|
390
|
+
filters += args.fetch('filterOptions', {}).map{|bp_info|
|
391
|
+
process_filter.call(bp_info['filterId'], bp_info['condition'])
|
392
|
+
}
|
393
|
+
|
394
|
+
send_response req, breakpoints: filters
|
395
|
+
|
396
|
+
when 'disconnect'
|
397
|
+
terminate = args.fetch("terminateDebuggee", false)
|
398
|
+
|
399
|
+
SESSION.clear_all_breakpoints
|
400
|
+
send_response req
|
401
|
+
|
402
|
+
if SESSION.in_subsession?
|
403
|
+
if terminate
|
404
|
+
@q_msg << 'kill!'
|
405
|
+
else
|
406
|
+
@q_msg << 'continue'
|
407
|
+
end
|
408
|
+
else
|
409
|
+
if terminate
|
410
|
+
@q_msg << 'kill!'
|
411
|
+
pause
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
## control
|
416
|
+
when 'continue'
|
417
|
+
@q_msg << 'c'
|
418
|
+
send_response req, allThreadsContinued: true
|
419
|
+
when 'next'
|
420
|
+
begin
|
421
|
+
@session.check_postmortem
|
422
|
+
@q_msg << 'n'
|
423
|
+
send_response req
|
424
|
+
rescue PostmortemError
|
425
|
+
send_response req,
|
426
|
+
success: false, message: 'postmortem mode',
|
427
|
+
result: "'Next' is not supported while postmortem mode"
|
428
|
+
end
|
429
|
+
when 'stepIn'
|
430
|
+
begin
|
431
|
+
@session.check_postmortem
|
432
|
+
@q_msg << 's'
|
433
|
+
send_response req
|
434
|
+
rescue PostmortemError
|
435
|
+
send_response req,
|
436
|
+
success: false, message: 'postmortem mode',
|
437
|
+
result: "'stepIn' is not supported while postmortem mode"
|
438
|
+
end
|
439
|
+
when 'stepOut'
|
440
|
+
begin
|
441
|
+
@session.check_postmortem
|
442
|
+
@q_msg << 'fin'
|
443
|
+
send_response req
|
444
|
+
rescue PostmortemError
|
445
|
+
send_response req,
|
446
|
+
success: false, message: 'postmortem mode',
|
447
|
+
result: "'stepOut' is not supported while postmortem mode"
|
448
|
+
end
|
449
|
+
when 'terminate'
|
450
|
+
send_response req
|
451
|
+
exit
|
452
|
+
when 'pause'
|
453
|
+
send_response req
|
454
|
+
Process.kill(UI_ServerBase::TRAP_SIGNAL, Process.pid)
|
455
|
+
when 'reverseContinue'
|
456
|
+
send_response req,
|
457
|
+
success: false, message: 'cancelled',
|
458
|
+
result: "Reverse Continue is not supported. Only \"Step back\" is supported."
|
459
|
+
when 'stepBack'
|
460
|
+
@q_msg << req
|
461
|
+
|
462
|
+
## query
|
463
|
+
when 'threads'
|
464
|
+
send_response req, threads: SESSION.managed_thread_clients.map{|tc|
|
465
|
+
{ id: tc.id,
|
466
|
+
name: tc.name,
|
467
|
+
}
|
468
|
+
}
|
469
|
+
|
470
|
+
when 'evaluate'
|
471
|
+
expr = req.dig('arguments', 'expression')
|
472
|
+
if /\A\s*,(.+)\z/ =~ expr
|
473
|
+
dbg_expr = $1.strip
|
474
|
+
dbg_expr.split(';;') { |cmd| @q_msg << cmd }
|
475
|
+
|
476
|
+
send_response req,
|
477
|
+
result: "(rdbg:command) #{dbg_expr}",
|
478
|
+
variablesReference: 0
|
479
|
+
else
|
480
|
+
@q_msg << req
|
481
|
+
end
|
482
|
+
when 'stackTrace',
|
483
|
+
'scopes',
|
484
|
+
'variables',
|
485
|
+
'source',
|
486
|
+
'completions'
|
487
|
+
@q_msg << req
|
488
|
+
|
489
|
+
else
|
490
|
+
if respond_to? mid = "custom_dap_request_#{req['command']}"
|
491
|
+
__send__ mid, req
|
492
|
+
else
|
493
|
+
raise "Unknown request: #{req.inspect}"
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
## called by the SESSION thread
|
499
|
+
|
500
|
+
def respond req, res
|
501
|
+
send_response(req, **res)
|
502
|
+
end
|
503
|
+
|
504
|
+
def puts result = ""
|
505
|
+
# STDERR.puts "puts: #{result}"
|
506
|
+
send_event 'output', category: 'console', output: "#{result&.chomp}\n"
|
507
|
+
end
|
508
|
+
|
509
|
+
def ignore_output_on_suspend?
|
510
|
+
true
|
511
|
+
end
|
512
|
+
|
513
|
+
def event type, *args
|
514
|
+
case type
|
515
|
+
when :load
|
516
|
+
file_path, reloaded = *args
|
517
|
+
|
518
|
+
if file_path
|
519
|
+
send_event 'loadedSource',
|
520
|
+
reason: (reloaded ? :changed : :new),
|
521
|
+
source: {
|
522
|
+
path: file_path,
|
523
|
+
}
|
524
|
+
end
|
525
|
+
when :suspend_bp
|
526
|
+
_i, bp, tid = *args
|
527
|
+
if bp.kind_of?(CatchBreakpoint)
|
528
|
+
reason = 'exception'
|
529
|
+
text = bp.description
|
530
|
+
else
|
531
|
+
reason = 'breakpoint'
|
532
|
+
text = bp ? bp.description : 'temporary bp'
|
533
|
+
end
|
534
|
+
|
535
|
+
send_event 'stopped', reason: reason,
|
536
|
+
description: text,
|
537
|
+
text: text,
|
538
|
+
threadId: tid,
|
539
|
+
allThreadsStopped: true
|
540
|
+
when :suspend_trap
|
541
|
+
_sig, tid = *args
|
542
|
+
send_event 'stopped', reason: 'pause',
|
543
|
+
threadId: tid,
|
544
|
+
allThreadsStopped: true
|
545
|
+
when :suspended
|
546
|
+
tid, = *args
|
547
|
+
send_event 'stopped', reason: 'step',
|
548
|
+
threadId: tid,
|
549
|
+
allThreadsStopped: true
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
class Session
|
555
|
+
include GlobalVariablesHelper
|
556
|
+
|
557
|
+
def find_waiting_tc id
|
558
|
+
@th_clients.each{|th, tc|
|
559
|
+
return tc if tc.id == id && tc.waiting?
|
560
|
+
}
|
561
|
+
return nil
|
562
|
+
end
|
563
|
+
|
564
|
+
def fail_response req, **kw
|
565
|
+
@ui.respond req, success: false, **kw
|
566
|
+
return :retry
|
567
|
+
end
|
568
|
+
|
569
|
+
def process_protocol_request req
|
570
|
+
case req['command']
|
571
|
+
when 'stepBack'
|
572
|
+
if @tc.recorder&.can_step_back?
|
573
|
+
request_tc [:step, :back]
|
574
|
+
else
|
575
|
+
fail_response req, message: 'cancelled'
|
576
|
+
end
|
577
|
+
|
578
|
+
when 'stackTrace'
|
579
|
+
tid = req.dig('arguments', 'threadId')
|
580
|
+
|
581
|
+
if find_waiting_tc(tid)
|
582
|
+
request_tc [:dap, :backtrace, req]
|
583
|
+
else
|
584
|
+
fail_response req
|
585
|
+
end
|
586
|
+
when 'scopes'
|
587
|
+
frame_id = req.dig('arguments', 'frameId')
|
588
|
+
if @frame_map[frame_id]
|
589
|
+
tid, fid = @frame_map[frame_id]
|
590
|
+
if find_waiting_tc(tid)
|
591
|
+
request_tc [:dap, :scopes, req, fid]
|
592
|
+
else
|
593
|
+
fail_response req
|
594
|
+
end
|
595
|
+
else
|
596
|
+
fail_response req
|
597
|
+
end
|
598
|
+
when 'variables'
|
599
|
+
varid = req.dig('arguments', 'variablesReference')
|
600
|
+
if ref = @var_map[varid]
|
601
|
+
case ref[0]
|
602
|
+
when :globals
|
603
|
+
vars = safe_global_variables.sort.map do |name|
|
604
|
+
begin
|
605
|
+
gv = eval(name.to_s)
|
606
|
+
rescue Exception => e
|
607
|
+
gv = e.inspect
|
608
|
+
end
|
609
|
+
{
|
610
|
+
name: name,
|
611
|
+
value: gv.inspect,
|
612
|
+
type: (gv.class.name || gv.class.to_s),
|
613
|
+
variablesReference: 0,
|
614
|
+
}
|
615
|
+
end
|
616
|
+
|
617
|
+
@ui.respond req, {
|
618
|
+
variables: vars,
|
619
|
+
}
|
620
|
+
return :retry
|
621
|
+
|
622
|
+
when :scope
|
623
|
+
frame_id = ref[1]
|
624
|
+
tid, fid = @frame_map[frame_id]
|
625
|
+
|
626
|
+
if find_waiting_tc(tid)
|
627
|
+
request_tc [:dap, :scope, req, fid]
|
628
|
+
else
|
629
|
+
fail_response req
|
630
|
+
end
|
631
|
+
|
632
|
+
when :variable
|
633
|
+
tid, vid = ref[1], ref[2]
|
634
|
+
|
635
|
+
if find_waiting_tc(tid)
|
636
|
+
request_tc [:dap, :variable, req, vid]
|
637
|
+
else
|
638
|
+
fail_response req
|
639
|
+
end
|
640
|
+
else
|
641
|
+
raise "Unknown type: #{ref.inspect}"
|
642
|
+
end
|
643
|
+
else
|
644
|
+
fail_response req
|
645
|
+
end
|
646
|
+
when 'evaluate'
|
647
|
+
frame_id = req.dig('arguments', 'frameId')
|
648
|
+
context = req.dig('arguments', 'context')
|
649
|
+
|
650
|
+
if @frame_map[frame_id]
|
651
|
+
tid, fid = @frame_map[frame_id]
|
652
|
+
expr = req.dig('arguments', 'expression')
|
653
|
+
|
654
|
+
if find_waiting_tc(tid)
|
655
|
+
restart_all_threads
|
656
|
+
request_tc [:dap, :evaluate, req, fid, expr, context]
|
657
|
+
else
|
658
|
+
fail_response req
|
659
|
+
end
|
660
|
+
else
|
661
|
+
fail_response req, result: "can't evaluate"
|
662
|
+
end
|
663
|
+
when 'source'
|
664
|
+
ref = req.dig('arguments', 'sourceReference')
|
665
|
+
if src = @src_map[ref]
|
666
|
+
@ui.respond req, content: src.join("\n")
|
667
|
+
else
|
668
|
+
fail_response req, message: 'not found...'
|
669
|
+
end
|
670
|
+
return :retry
|
671
|
+
|
672
|
+
when 'completions'
|
673
|
+
frame_id = req.dig('arguments', 'frameId')
|
674
|
+
tid, fid = @frame_map[frame_id]
|
675
|
+
|
676
|
+
if find_waiting_tc(tid)
|
677
|
+
text = req.dig('arguments', 'text')
|
678
|
+
line = req.dig('arguments', 'line')
|
679
|
+
if col = req.dig('arguments', 'column')
|
680
|
+
text = text.split(/\n/)[line.to_i - 1][0...(col.to_i - 1)]
|
681
|
+
end
|
682
|
+
request_tc [:dap, :completions, req, fid, text]
|
683
|
+
else
|
684
|
+
fail_response req
|
685
|
+
end
|
686
|
+
else
|
687
|
+
if respond_to? mid = "custom_dap_request_#{req['command']}"
|
688
|
+
__send__ mid, req
|
689
|
+
else
|
690
|
+
raise "Unknown request: #{req.inspect}"
|
691
|
+
end
|
692
|
+
end
|
693
|
+
end
|
694
|
+
|
695
|
+
def process_protocol_result args
|
696
|
+
# puts({dap_event: args}.inspect)
|
697
|
+
type, req, result = args
|
698
|
+
|
699
|
+
case type
|
700
|
+
when :backtrace
|
701
|
+
result[:stackFrames].each{|fi|
|
702
|
+
frame_depth = fi[:id]
|
703
|
+
fi[:id] = id = @frame_map.size + 1
|
704
|
+
@frame_map[id] = [req.dig('arguments', 'threadId'), frame_depth]
|
705
|
+
if fi[:source]
|
706
|
+
if src = fi[:source][:sourceReference]
|
707
|
+
src_id = @src_map.size + 1
|
708
|
+
@src_map[src_id] = src
|
709
|
+
fi[:source][:sourceReference] = src_id
|
710
|
+
else
|
711
|
+
fi[:source][:sourceReference] = 0
|
712
|
+
end
|
713
|
+
end
|
714
|
+
}
|
715
|
+
@ui.respond req, result
|
716
|
+
when :scopes
|
717
|
+
frame_id = req.dig('arguments', 'frameId')
|
718
|
+
local_scope = result[:scopes].first
|
719
|
+
local_scope[:variablesReference] = id = @var_map.size + 1
|
720
|
+
|
721
|
+
@var_map[id] = [:scope, frame_id]
|
722
|
+
@ui.respond req, result
|
723
|
+
when :scope
|
724
|
+
tid = result.delete :tid
|
725
|
+
register_vars result[:variables], tid
|
726
|
+
@ui.respond req, result
|
727
|
+
when :variable
|
728
|
+
tid = result.delete :tid
|
729
|
+
register_vars result[:variables], tid
|
730
|
+
@ui.respond req, result
|
731
|
+
when :evaluate
|
732
|
+
stop_all_threads
|
733
|
+
message = result.delete :message
|
734
|
+
if message
|
735
|
+
@ui.respond req, success: false, message: message
|
736
|
+
else
|
737
|
+
tid = result.delete :tid
|
738
|
+
register_var result, tid
|
739
|
+
@ui.respond req, result
|
740
|
+
end
|
741
|
+
when :completions
|
742
|
+
@ui.respond req, result
|
743
|
+
else
|
744
|
+
if respond_to? mid = "custom_dap_request_event_#{type}"
|
745
|
+
__send__ mid, req, result
|
746
|
+
else
|
747
|
+
raise "unsupported: #{args.inspect}"
|
748
|
+
end
|
749
|
+
end
|
750
|
+
end
|
751
|
+
|
752
|
+
def register_var v, tid
|
753
|
+
if (tl_vid = v[:variablesReference]) > 0
|
754
|
+
vid = @var_map.size + 1
|
755
|
+
@var_map[vid] = [:variable, tid, tl_vid]
|
756
|
+
v[:variablesReference] = vid
|
757
|
+
end
|
758
|
+
end
|
759
|
+
|
760
|
+
def register_vars vars, tid
|
761
|
+
raise tid.inspect unless tid.kind_of?(Integer)
|
762
|
+
vars.each{|v|
|
763
|
+
register_var v, tid
|
764
|
+
}
|
765
|
+
end
|
766
|
+
end
|
767
|
+
|
768
|
+
class NaiveString
|
769
|
+
attr_reader :str
|
770
|
+
def initialize str
|
771
|
+
@str = str
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
class ThreadClient
|
776
|
+
MAX_LENGTH = 180
|
777
|
+
|
778
|
+
def value_inspect obj, short: true
|
779
|
+
# TODO: max length should be configuarable?
|
780
|
+
str = DEBUGGER__.safe_inspect obj, short: short, max_length: MAX_LENGTH
|
781
|
+
|
782
|
+
if str.encoding == Encoding::UTF_8
|
783
|
+
str.scrub
|
784
|
+
else
|
785
|
+
str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
786
|
+
end
|
787
|
+
end
|
788
|
+
|
789
|
+
def dap_eval b, expr, _context, prompt: '(repl_eval)'
|
790
|
+
begin
|
791
|
+
tp_allow_reentry do
|
792
|
+
b.eval(expr.to_s, prompt)
|
793
|
+
end
|
794
|
+
rescue Exception => e
|
795
|
+
e
|
796
|
+
end
|
797
|
+
end
|
798
|
+
|
799
|
+
def process_dap args
|
800
|
+
# pp tc: self, args: args
|
801
|
+
type = args.shift
|
802
|
+
req = args.shift
|
803
|
+
|
804
|
+
case type
|
805
|
+
when :backtrace
|
806
|
+
start_frame = req.dig('arguments', 'startFrame') || 0
|
807
|
+
levels = req.dig('arguments', 'levels') || 1_000
|
808
|
+
frames = []
|
809
|
+
@target_frames.each_with_index do |frame, i|
|
810
|
+
next if i < start_frame
|
811
|
+
|
812
|
+
path = frame.realpath || frame.path
|
813
|
+
next if skip_path?(path) && !SESSION.stop_stepping?(path, frame.location.lineno)
|
814
|
+
break if (levels -= 1) < 0
|
815
|
+
source_name = path ? File.basename(path) : frame.location.to_s
|
816
|
+
|
817
|
+
if (path && File.exist?(path)) && (local_path = UI_DAP.remote_to_local_path(path))
|
818
|
+
# ok
|
819
|
+
else
|
820
|
+
ref = frame.file_lines
|
821
|
+
end
|
822
|
+
|
823
|
+
frames << {
|
824
|
+
id: i, # id is refilled by SESSION
|
825
|
+
name: frame.name,
|
826
|
+
line: frame.location.lineno,
|
827
|
+
column: 1,
|
828
|
+
source: {
|
829
|
+
name: source_name,
|
830
|
+
path: (local_path || path),
|
831
|
+
sourceReference: ref,
|
832
|
+
},
|
833
|
+
}
|
834
|
+
end
|
835
|
+
|
836
|
+
event! :protocol_result, :backtrace, req, {
|
837
|
+
stackFrames: frames,
|
838
|
+
totalFrames: @target_frames.size,
|
839
|
+
}
|
840
|
+
when :scopes
|
841
|
+
fid = args.shift
|
842
|
+
frame = get_frame(fid)
|
843
|
+
|
844
|
+
lnum =
|
845
|
+
if frame.binding
|
846
|
+
frame.binding.local_variables.size
|
847
|
+
elsif vars = frame.local_variables
|
848
|
+
vars.size
|
849
|
+
else
|
850
|
+
0
|
851
|
+
end
|
852
|
+
|
853
|
+
event! :protocol_result, :scopes, req, scopes: [{
|
854
|
+
name: 'Local variables',
|
855
|
+
presentationHint: 'locals',
|
856
|
+
# variablesReference: N, # filled by SESSION
|
857
|
+
namedVariables: lnum,
|
858
|
+
indexedVariables: 0,
|
859
|
+
expensive: false,
|
860
|
+
}, {
|
861
|
+
name: 'Global variables',
|
862
|
+
presentationHint: 'globals',
|
863
|
+
variablesReference: 1, # GLOBAL
|
864
|
+
namedVariables: safe_global_variables.size,
|
865
|
+
indexedVariables: 0,
|
866
|
+
expensive: false,
|
867
|
+
}]
|
868
|
+
when :scope
|
869
|
+
fid = args.shift
|
870
|
+
frame = get_frame(fid)
|
871
|
+
vars = collect_locals(frame).map do |var, val|
|
872
|
+
variable(var, val)
|
873
|
+
end
|
874
|
+
|
875
|
+
event! :protocol_result, :scope, req, variables: vars, tid: self.id
|
876
|
+
when :variable
|
877
|
+
vid = args.shift
|
878
|
+
obj = @var_map[vid]
|
879
|
+
if obj
|
880
|
+
case req.dig('arguments', 'filter')
|
881
|
+
when 'indexed'
|
882
|
+
start = req.dig('arguments', 'start') || 0
|
883
|
+
count = req.dig('arguments', 'count') || obj.size
|
884
|
+
vars = (start ... (start + count)).map{|i|
|
885
|
+
variable(i.to_s, obj[i])
|
886
|
+
}
|
887
|
+
else
|
888
|
+
vars = []
|
889
|
+
|
890
|
+
case obj
|
891
|
+
when Hash
|
892
|
+
vars = obj.map{|k, v|
|
893
|
+
variable(value_inspect(k), v,)
|
894
|
+
}
|
895
|
+
when Struct
|
896
|
+
vars = obj.members.map{|m|
|
897
|
+
variable(m, obj[m])
|
898
|
+
}
|
899
|
+
when String
|
900
|
+
vars = [
|
901
|
+
variable('#length', obj.length),
|
902
|
+
variable('#encoding', obj.encoding),
|
903
|
+
]
|
904
|
+
printed_str = value_inspect(obj)
|
905
|
+
vars << variable('#dump', NaiveString.new(obj)) if printed_str.end_with?('...')
|
906
|
+
when Class, Module
|
907
|
+
vars << variable('%ancestors', obj.ancestors[1..])
|
908
|
+
when Range
|
909
|
+
vars = [
|
910
|
+
variable('#begin', obj.begin),
|
911
|
+
variable('#end', obj.end),
|
912
|
+
]
|
913
|
+
end
|
914
|
+
|
915
|
+
unless NaiveString === obj
|
916
|
+
vars += M_INSTANCE_VARIABLES.bind_call(obj).sort.map{|iv|
|
917
|
+
variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
|
918
|
+
}
|
919
|
+
vars.unshift variable('#class', M_CLASS.bind_call(obj))
|
920
|
+
end
|
921
|
+
end
|
922
|
+
end
|
923
|
+
event! :protocol_result, :variable, req, variables: (vars || []), tid: self.id
|
924
|
+
|
925
|
+
when :evaluate
|
926
|
+
fid, expr, context = args
|
927
|
+
frame = get_frame(fid)
|
928
|
+
message = nil
|
929
|
+
|
930
|
+
if frame && (b = frame.eval_binding)
|
931
|
+
special_local_variables frame do |name, var|
|
932
|
+
b.local_variable_set(name, var) if /\%/ !~ name
|
933
|
+
end
|
934
|
+
|
935
|
+
case context
|
936
|
+
when 'repl', 'watch'
|
937
|
+
result = dap_eval b, expr, context, prompt: '(DEBUG CONSOLE)'
|
938
|
+
when 'hover'
|
939
|
+
case expr
|
940
|
+
when /\A\@\S/
|
941
|
+
begin
|
942
|
+
result = M_INSTANCE_VARIABLE_GET.bind_call(b.receiver, expr)
|
943
|
+
rescue NameError
|
944
|
+
message = "Error: Not defined instance variable: #{expr.inspect}"
|
945
|
+
end
|
946
|
+
when /\A\$\S/
|
947
|
+
safe_global_variables.each{|gvar|
|
948
|
+
if gvar.to_s == expr
|
949
|
+
result = eval(gvar.to_s)
|
950
|
+
break false
|
951
|
+
end
|
952
|
+
} and (message = "Error: Not defined global variable: #{expr.inspect}")
|
953
|
+
when /\Aself$/
|
954
|
+
result = b.receiver
|
955
|
+
when /(\A((::[A-Z]|[A-Z])\w*)+)/
|
956
|
+
unless result = search_const(b, $1)
|
957
|
+
message = "Error: Not defined constants: #{expr.inspect}"
|
958
|
+
end
|
959
|
+
else
|
960
|
+
begin
|
961
|
+
result = b.local_variable_get(expr)
|
962
|
+
rescue NameError
|
963
|
+
# try to check method
|
964
|
+
if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true)
|
965
|
+
result = M_METHOD.bind_call(b.receiver, expr)
|
966
|
+
else
|
967
|
+
message = "Error: Can not evaluate: #{expr.inspect}"
|
968
|
+
end
|
969
|
+
end
|
970
|
+
end
|
971
|
+
else
|
972
|
+
message = "Error: unknown context: #{context}"
|
973
|
+
end
|
974
|
+
else
|
975
|
+
result = 'Error: Can not evaluate on this frame'
|
976
|
+
end
|
977
|
+
|
978
|
+
event! :protocol_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result)
|
979
|
+
|
980
|
+
when :completions
|
981
|
+
fid, text = args
|
982
|
+
frame = get_frame(fid)
|
983
|
+
|
984
|
+
if (b = frame&.binding) && word = text&.split(/[\s\{]/)&.last
|
985
|
+
words = IRB::InputCompletor::retrieve_completion_data(word, bind: b).compact
|
986
|
+
end
|
987
|
+
|
988
|
+
event! :protocol_result, :completions, req, targets: (words || []).map{|phrase|
|
989
|
+
detail = nil
|
990
|
+
|
991
|
+
if /\b([_a-zA-Z]\w*[!\?]?)\z/ =~ phrase
|
992
|
+
w = $1
|
993
|
+
else
|
994
|
+
w = phrase
|
995
|
+
end
|
996
|
+
|
997
|
+
begin
|
998
|
+
v = b.local_variable_get(w)
|
999
|
+
detail ="(variable: #{value_inspect(v)})"
|
1000
|
+
rescue NameError
|
1001
|
+
end
|
1002
|
+
|
1003
|
+
{
|
1004
|
+
label: phrase,
|
1005
|
+
text: w,
|
1006
|
+
detail: detail,
|
1007
|
+
}
|
1008
|
+
}
|
1009
|
+
|
1010
|
+
else
|
1011
|
+
if respond_to? mid = "custom_dap_request_#{type}"
|
1012
|
+
__send__ mid, req
|
1013
|
+
else
|
1014
|
+
raise "Unknown request: #{args.inspect}"
|
1015
|
+
end
|
1016
|
+
end
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
def search_const b, expr
|
1020
|
+
cs = expr.delete_prefix('::').split('::')
|
1021
|
+
[Object, *b.eval('::Module.nesting')].reverse_each{|mod|
|
1022
|
+
if cs.all?{|c|
|
1023
|
+
if mod.const_defined?(c)
|
1024
|
+
begin
|
1025
|
+
mod = mod.const_get(c)
|
1026
|
+
rescue Exception
|
1027
|
+
false
|
1028
|
+
end
|
1029
|
+
else
|
1030
|
+
false
|
1031
|
+
end
|
1032
|
+
}
|
1033
|
+
# if-body
|
1034
|
+
return mod
|
1035
|
+
end
|
1036
|
+
}
|
1037
|
+
false
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
def evaluate_result r
|
1041
|
+
variable nil, r
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
def type_name obj
|
1045
|
+
klass = M_CLASS.bind_call(obj)
|
1046
|
+
|
1047
|
+
begin
|
1048
|
+
M_NAME.bind_call(klass) || klass.to_s
|
1049
|
+
rescue Exception => e
|
1050
|
+
"<Error: #{e.message} (#{e.backtrace.first}>"
|
1051
|
+
end
|
1052
|
+
end
|
1053
|
+
|
1054
|
+
def variable_ name, obj, indexedVariables: 0, namedVariables: 0
|
1055
|
+
if indexedVariables > 0 || namedVariables > 0
|
1056
|
+
vid = @var_map.size + 1
|
1057
|
+
@var_map[vid] = obj
|
1058
|
+
else
|
1059
|
+
vid = 0
|
1060
|
+
end
|
1061
|
+
|
1062
|
+
namedVariables += M_INSTANCE_VARIABLES.bind_call(obj).size
|
1063
|
+
|
1064
|
+
if NaiveString === obj
|
1065
|
+
str = obj.str.dump
|
1066
|
+
vid = indexedVariables = namedVariables = 0
|
1067
|
+
else
|
1068
|
+
str = value_inspect(obj)
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
if name
|
1072
|
+
{ name: name,
|
1073
|
+
value: str,
|
1074
|
+
type: type_name(obj),
|
1075
|
+
variablesReference: vid,
|
1076
|
+
indexedVariables: indexedVariables,
|
1077
|
+
namedVariables: namedVariables,
|
1078
|
+
}
|
1079
|
+
else
|
1080
|
+
{ result: str,
|
1081
|
+
type: type_name(obj),
|
1082
|
+
variablesReference: vid,
|
1083
|
+
indexedVariables: indexedVariables,
|
1084
|
+
namedVariables: namedVariables,
|
1085
|
+
}
|
1086
|
+
end
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
def variable name, obj
|
1090
|
+
case obj
|
1091
|
+
when Array
|
1092
|
+
variable_ name, obj, indexedVariables: obj.size
|
1093
|
+
when Hash
|
1094
|
+
variable_ name, obj, namedVariables: obj.size
|
1095
|
+
when String
|
1096
|
+
variable_ name, obj, namedVariables: 3 # #length, #encoding, #to_str
|
1097
|
+
when Struct
|
1098
|
+
variable_ name, obj, namedVariables: obj.size
|
1099
|
+
when Class, Module
|
1100
|
+
variable_ name, obj, namedVariables: 1 # %ancestors (#ancestors without self)
|
1101
|
+
when Range
|
1102
|
+
variable_ name, obj, namedVariables: 2 # #begin, #end
|
1103
|
+
else
|
1104
|
+
variable_ name, obj, namedVariables: 1 # #class
|
1105
|
+
end
|
1106
|
+
end
|
1107
|
+
end
|
1108
|
+
end
|