debug 1.4.0 → 1.9.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.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +210 -6
- data/Gemfile +2 -0
- data/LICENSE.txt +0 -0
- data/README.md +161 -85
- data/Rakefile +33 -10
- data/TODO.md +8 -8
- data/debug.gemspec +9 -7
- data/exe/rdbg +23 -4
- data/ext/debug/debug.c +111 -21
- data/ext/debug/extconf.rb +23 -0
- data/ext/debug/iseq_collector.c +2 -0
- data/lib/debug/abbrev_command.rb +77 -0
- data/lib/debug/breakpoint.rb +102 -74
- data/lib/debug/client.rb +46 -12
- data/lib/debug/color.rb +0 -0
- data/lib/debug/config.rb +129 -36
- data/lib/debug/console.rb +46 -40
- data/lib/debug/dap_custom/traceInspector.rb +336 -0
- data/lib/debug/frame_info.rb +40 -25
- data/lib/debug/irb_integration.rb +37 -0
- data/lib/debug/local.rb +17 -11
- data/lib/debug/open.rb +0 -0
- data/lib/debug/open_nonstop.rb +0 -0
- data/lib/debug/prelude.rb +3 -2
- data/lib/debug/server.rb +126 -56
- data/lib/debug/server_cdp.rb +673 -248
- data/lib/debug/server_dap.rb +497 -261
- data/lib/debug/session.rb +899 -441
- data/lib/debug/source_repository.rb +122 -49
- data/lib/debug/start.rb +1 -1
- data/lib/debug/thread_client.rb +460 -155
- data/lib/debug/tracer.rb +10 -16
- data/lib/debug/version.rb +1 -1
- data/lib/debug.rb +7 -2
- data/misc/README.md.erb +106 -56
- metadata +14 -24
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
- data/.github/ISSUE_TEMPLATE/custom.md +0 -10
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
- data/.github/pull_request_template.md +0 -9
- data/.github/workflows/ruby.yml +0 -34
- data/.gitignore +0 -12
- data/bin/console +0 -14
- data/bin/gentest +0 -30
- data/bin/setup +0 -8
- data/lib/debug/bp.vim +0 -68
data/lib/debug/server_dap.rb
CHANGED
@@ -3,52 +3,51 @@
|
|
3
3
|
require 'json'
|
4
4
|
require 'irb/completion'
|
5
5
|
require 'tmpdir'
|
6
|
-
require 'json'
|
7
6
|
require 'fileutils'
|
8
7
|
|
9
8
|
module DEBUGGER__
|
10
9
|
module UI_DAP
|
11
10
|
SHOW_PROTOCOL = ENV['DEBUG_DAP_SHOW_PROTOCOL'] == '1' || ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
|
12
11
|
|
13
|
-
def self.setup
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
+
|
19
27
|
Dir.chdir(dir) do
|
20
|
-
Dir.mkdir('.vscode')
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
# This file will be removed at the end of the debuggee process.
|
25
|
-
#
|
26
|
-
# Note that vscode-rdbg extension is needed. Please install if you don't have.
|
27
|
-
MSG
|
28
|
-
}
|
29
|
-
open('.vscode/launch.json', 'w'){|f|
|
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|
|
30
32
|
f.puts JSON.pretty_generate({
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
debugPort: sock_path,
|
39
|
-
autoAttach: true,
|
40
|
-
}
|
41
|
-
]
|
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,
|
42
40
|
})
|
43
|
-
|
41
|
+
end
|
44
42
|
end
|
45
43
|
|
46
|
-
cmds = ['code', "#{dir}/"
|
44
|
+
cmds = ['code', "#{dir}/"]
|
47
45
|
cmdline = cmds.join(' ')
|
48
|
-
ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/
|
46
|
+
ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/"
|
49
47
|
|
50
48
|
STDERR.puts "Launching: #{cmdline}"
|
51
49
|
env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h
|
50
|
+
env['RUBY_DEBUG_AUTOATTACH'] = key
|
52
51
|
|
53
52
|
unless system(env, *cmds)
|
54
53
|
DEBUGGER__.warn <<~MESSAGE
|
@@ -71,9 +70,71 @@ module DEBUGGER__
|
|
71
70
|
end
|
72
71
|
end
|
73
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
|
+
|
74
125
|
def dap_setup bytes
|
75
126
|
CONFIG.set_config no_color: true
|
76
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
|
77
138
|
|
78
139
|
show_protocol :>, bytes
|
79
140
|
req = JSON.load(bytes)
|
@@ -90,14 +151,13 @@ module DEBUGGER__
|
|
90
151
|
{
|
91
152
|
filter: 'any',
|
92
153
|
label: 'rescue any exception',
|
93
|
-
|
154
|
+
supportsCondition: true,
|
94
155
|
#conditionDescription: '',
|
95
156
|
},
|
96
157
|
{
|
97
158
|
filter: 'RuntimeError',
|
98
159
|
label: 'rescue RuntimeError',
|
99
|
-
|
100
|
-
#supportsCondition: true,
|
160
|
+
supportsCondition: true,
|
101
161
|
#conditionDescription: '',
|
102
162
|
},
|
103
163
|
],
|
@@ -140,13 +200,26 @@ module DEBUGGER__
|
|
140
200
|
# supportsInstructionBreakpoints:
|
141
201
|
)
|
142
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
|
143
210
|
end
|
144
211
|
|
145
212
|
def send **kw
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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"
|
150
223
|
end
|
151
224
|
|
152
225
|
def send_response req, success: true, message: nil, **kw
|
@@ -178,17 +251,17 @@ module DEBUGGER__
|
|
178
251
|
end
|
179
252
|
|
180
253
|
def recv_request
|
181
|
-
|
254
|
+
IO.select([@sock])
|
182
255
|
|
183
256
|
@session.process_group.sync do
|
184
257
|
raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)
|
185
258
|
|
186
|
-
case
|
259
|
+
case @sock.gets
|
187
260
|
when /Content-Length: (\d+)/
|
188
261
|
b = @sock.read(2)
|
189
262
|
raise b.inspect unless b == "\r\n"
|
190
263
|
|
191
|
-
l = @sock.read(
|
264
|
+
l = @sock.read($1.to_i)
|
192
265
|
show_protocol :>, l
|
193
266
|
JSON.load(l)
|
194
267
|
when nil
|
@@ -201,26 +274,81 @@ module DEBUGGER__
|
|
201
274
|
retry
|
202
275
|
end
|
203
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
|
+
|
204
296
|
def process
|
205
297
|
while req = recv_request
|
206
|
-
|
207
|
-
|
298
|
+
process_request(req)
|
299
|
+
end
|
300
|
+
ensure
|
301
|
+
send_event :terminated unless @sock.closed?
|
302
|
+
end
|
208
303
|
|
209
|
-
|
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
|
210
349
|
|
211
|
-
## boot/configuration
|
212
|
-
when 'launch'
|
213
|
-
send_response req
|
214
|
-
@is_attach = false
|
215
|
-
when 'attach'
|
216
|
-
send_response req
|
217
|
-
Process.kill(:SIGURG, Process.pid)
|
218
|
-
@is_attach = true
|
219
|
-
when 'setBreakpoints'
|
220
|
-
path = args.dig('source', 'path')
|
221
|
-
bp_args = args['breakpoints']
|
222
350
|
bps = []
|
223
|
-
|
351
|
+
args['breakpoints'].each{|bp|
|
224
352
|
line = bp['line']
|
225
353
|
if cond = bp['condition']
|
226
354
|
bps << SESSION.add_line_breakpoint(path, line, cond: cond)
|
@@ -229,114 +357,138 @@ module DEBUGGER__
|
|
229
357
|
end
|
230
358
|
}
|
231
359
|
send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
|
232
|
-
|
233
|
-
send_response req
|
234
|
-
|
235
|
-
|
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 =
|
236
370
|
case filter_id
|
237
371
|
when 'any'
|
238
|
-
|
372
|
+
SESSION.add_catch_breakpoint 'Exception', cond: cond
|
239
373
|
when 'RuntimeError'
|
240
|
-
|
374
|
+
SESSION.add_catch_breakpoint 'RuntimeError', cond: cond
|
241
375
|
else
|
242
|
-
|
376
|
+
nil
|
243
377
|
end
|
244
378
|
{
|
245
|
-
verified: bp
|
379
|
+
verified: !bp.nil?,
|
246
380
|
message: bp.inspect,
|
247
381
|
}
|
248
382
|
}
|
249
383
|
|
384
|
+
SESSION.clear_catch_breakpoints 'Exception', 'RuntimeError'
|
385
|
+
|
250
386
|
filters = args.fetch('filters').map {|filter_id|
|
251
387
|
process_filter.call(filter_id)
|
252
388
|
}
|
253
389
|
|
254
390
|
filters += args.fetch('filterOptions', {}).map{|bp_info|
|
255
|
-
|
256
|
-
|
391
|
+
process_filter.call(bp_info['filterId'], bp_info['condition'])
|
392
|
+
}
|
257
393
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
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!'
|
266
405
|
else
|
267
406
|
@q_msg << 'continue'
|
268
407
|
end
|
269
|
-
|
270
|
-
if
|
408
|
+
else
|
409
|
+
if terminate
|
271
410
|
@q_msg << 'kill!'
|
272
|
-
|
273
|
-
@q_msg << 'continue'
|
411
|
+
pause
|
274
412
|
end
|
275
|
-
|
413
|
+
end
|
276
414
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
send_response req
|
286
|
-
rescue PostmortemError
|
287
|
-
send_response req,
|
288
|
-
success: false, message: 'postmortem mode',
|
289
|
-
result: "'Next' is not supported while postmortem mode"
|
290
|
-
end
|
291
|
-
when 'stepIn'
|
292
|
-
begin
|
293
|
-
@session.check_postmortem
|
294
|
-
@q_msg << 's'
|
295
|
-
send_response req
|
296
|
-
rescue PostmortemError
|
297
|
-
send_response req,
|
298
|
-
success: false, message: 'postmortem mode',
|
299
|
-
result: "'stepIn' is not supported while postmortem mode"
|
300
|
-
end
|
301
|
-
when 'stepOut'
|
302
|
-
begin
|
303
|
-
@session.check_postmortem
|
304
|
-
@q_msg << 'fin'
|
305
|
-
send_response req
|
306
|
-
rescue PostmortemError
|
307
|
-
send_response req,
|
308
|
-
success: false, message: 'postmortem mode',
|
309
|
-
result: "'stepOut' is not supported while postmortem mode"
|
310
|
-
end
|
311
|
-
when 'terminate'
|
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'
|
312
423
|
send_response req
|
313
|
-
|
314
|
-
|
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'
|
315
433
|
send_response req
|
316
|
-
|
317
|
-
when 'reverseContinue'
|
434
|
+
rescue PostmortemError
|
318
435
|
send_response req,
|
319
|
-
success: false, message: '
|
320
|
-
result: "
|
321
|
-
|
322
|
-
|
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
|
323
461
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
}
|
462
|
+
## query
|
463
|
+
when 'threads'
|
464
|
+
send_response req, threads: SESSION.managed_thread_clients.map{|tc|
|
465
|
+
{ id: tc.id,
|
466
|
+
name: tc.name,
|
330
467
|
}
|
468
|
+
}
|
331
469
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
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
|
338
480
|
@q_msg << req
|
481
|
+
end
|
482
|
+
when 'stackTrace',
|
483
|
+
'scopes',
|
484
|
+
'variables',
|
485
|
+
'source',
|
486
|
+
'completions'
|
487
|
+
@q_msg << req
|
339
488
|
|
489
|
+
else
|
490
|
+
if respond_to? mid = "custom_dap_request_#{req['command']}"
|
491
|
+
__send__ mid, req
|
340
492
|
else
|
341
493
|
raise "Unknown request: #{req.inspect}"
|
342
494
|
end
|
@@ -345,25 +497,31 @@ module DEBUGGER__
|
|
345
497
|
|
346
498
|
## called by the SESSION thread
|
347
499
|
|
348
|
-
def readline prompt
|
349
|
-
@q_msg.pop || 'kill!'
|
350
|
-
end
|
351
|
-
|
352
|
-
def sock skip: false
|
353
|
-
yield $stderr
|
354
|
-
end
|
355
|
-
|
356
500
|
def respond req, res
|
357
501
|
send_response(req, **res)
|
358
502
|
end
|
359
503
|
|
360
504
|
def puts result
|
361
505
|
# STDERR.puts "puts: #{result}"
|
362
|
-
|
506
|
+
send_event 'output', category: 'console', output: "#{result&.chomp}\n"
|
507
|
+
end
|
508
|
+
|
509
|
+
def ignore_output_on_suspend?
|
510
|
+
true
|
363
511
|
end
|
364
512
|
|
365
513
|
def event type, *args
|
366
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
|
367
525
|
when :suspend_bp
|
368
526
|
_i, bp, tid = *args
|
369
527
|
if bp.kind_of?(CatchBreakpoint)
|
@@ -394,6 +552,8 @@ module DEBUGGER__
|
|
394
552
|
end
|
395
553
|
|
396
554
|
class Session
|
555
|
+
include GlobalVariablesHelper
|
556
|
+
|
397
557
|
def find_waiting_tc id
|
398
558
|
@th_clients.each{|th, tc|
|
399
559
|
return tc if tc.id == id && tc.waiting?
|
@@ -410,15 +570,16 @@ module DEBUGGER__
|
|
410
570
|
case req['command']
|
411
571
|
when 'stepBack'
|
412
572
|
if @tc.recorder&.can_step_back?
|
413
|
-
|
573
|
+
request_tc [:step, :back]
|
414
574
|
else
|
415
575
|
fail_response req, message: 'cancelled'
|
416
576
|
end
|
417
577
|
|
418
578
|
when 'stackTrace'
|
419
579
|
tid = req.dig('arguments', 'threadId')
|
420
|
-
|
421
|
-
|
580
|
+
|
581
|
+
if find_waiting_tc(tid)
|
582
|
+
request_tc [:dap, :backtrace, req]
|
422
583
|
else
|
423
584
|
fail_response req
|
424
585
|
end
|
@@ -426,8 +587,8 @@ module DEBUGGER__
|
|
426
587
|
frame_id = req.dig('arguments', 'frameId')
|
427
588
|
if @frame_map[frame_id]
|
428
589
|
tid, fid = @frame_map[frame_id]
|
429
|
-
if
|
430
|
-
|
590
|
+
if find_waiting_tc(tid)
|
591
|
+
request_tc [:dap, :scopes, req, fid]
|
431
592
|
else
|
432
593
|
fail_response req
|
433
594
|
end
|
@@ -439,8 +600,12 @@ module DEBUGGER__
|
|
439
600
|
if ref = @var_map[varid]
|
440
601
|
case ref[0]
|
441
602
|
when :globals
|
442
|
-
vars =
|
443
|
-
|
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
|
444
609
|
{
|
445
610
|
name: name,
|
446
611
|
value: gv.inspect,
|
@@ -458,8 +623,8 @@ module DEBUGGER__
|
|
458
623
|
frame_id = ref[1]
|
459
624
|
tid, fid = @frame_map[frame_id]
|
460
625
|
|
461
|
-
if
|
462
|
-
|
626
|
+
if find_waiting_tc(tid)
|
627
|
+
request_tc [:dap, :scope, req, fid]
|
463
628
|
else
|
464
629
|
fail_response req
|
465
630
|
end
|
@@ -467,8 +632,8 @@ module DEBUGGER__
|
|
467
632
|
when :variable
|
468
633
|
tid, vid = ref[1], ref[2]
|
469
634
|
|
470
|
-
if
|
471
|
-
|
635
|
+
if find_waiting_tc(tid)
|
636
|
+
request_tc [:dap, :variable, req, vid]
|
472
637
|
else
|
473
638
|
fail_response req
|
474
639
|
end
|
@@ -485,8 +650,10 @@ module DEBUGGER__
|
|
485
650
|
if @frame_map[frame_id]
|
486
651
|
tid, fid = @frame_map[frame_id]
|
487
652
|
expr = req.dig('arguments', 'expression')
|
488
|
-
|
489
|
-
|
653
|
+
|
654
|
+
if find_waiting_tc(tid)
|
655
|
+
restart_all_threads
|
656
|
+
request_tc [:dap, :evaluate, req, fid, expr, context]
|
490
657
|
else
|
491
658
|
fail_response req
|
492
659
|
end
|
@@ -496,7 +663,7 @@ module DEBUGGER__
|
|
496
663
|
when 'source'
|
497
664
|
ref = req.dig('arguments', 'sourceReference')
|
498
665
|
if src = @src_map[ref]
|
499
|
-
@ui.respond req, content: src.join
|
666
|
+
@ui.respond req, content: src.join("\n")
|
500
667
|
else
|
501
668
|
fail_response req, message: 'not found...'
|
502
669
|
end
|
@@ -506,34 +673,43 @@ module DEBUGGER__
|
|
506
673
|
frame_id = req.dig('arguments', 'frameId')
|
507
674
|
tid, fid = @frame_map[frame_id]
|
508
675
|
|
509
|
-
if
|
676
|
+
if find_waiting_tc(tid)
|
510
677
|
text = req.dig('arguments', 'text')
|
511
678
|
line = req.dig('arguments', 'line')
|
512
679
|
if col = req.dig('arguments', 'column')
|
513
680
|
text = text.split(/\n/)[line.to_i - 1][0...(col.to_i - 1)]
|
514
681
|
end
|
515
|
-
|
682
|
+
request_tc [:dap, :completions, req, fid, text]
|
516
683
|
else
|
517
684
|
fail_response req
|
518
685
|
end
|
519
686
|
else
|
520
|
-
|
687
|
+
if respond_to? mid = "custom_dap_request_#{req['command']}"
|
688
|
+
__send__ mid, req
|
689
|
+
else
|
690
|
+
raise "Unknown request: #{req.inspect}"
|
691
|
+
end
|
521
692
|
end
|
522
693
|
end
|
523
694
|
|
524
|
-
def
|
695
|
+
def process_protocol_result args
|
525
696
|
# puts({dap_event: args}.inspect)
|
526
697
|
type, req, result = args
|
527
698
|
|
528
699
|
case type
|
529
700
|
when :backtrace
|
530
|
-
result[:stackFrames].each
|
701
|
+
result[:stackFrames].each{|fi|
|
702
|
+
frame_depth = fi[:id]
|
531
703
|
fi[:id] = id = @frame_map.size + 1
|
532
|
-
@frame_map[id] = [req.dig('arguments', 'threadId'),
|
533
|
-
if fi[:source]
|
534
|
-
|
535
|
-
|
536
|
-
|
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
|
537
713
|
end
|
538
714
|
}
|
539
715
|
@ui.respond req, result
|
@@ -553,6 +729,7 @@ module DEBUGGER__
|
|
553
729
|
register_vars result[:variables], tid
|
554
730
|
@ui.respond req, result
|
555
731
|
when :evaluate
|
732
|
+
stop_all_threads
|
556
733
|
message = result.delete :message
|
557
734
|
if message
|
558
735
|
@ui.respond req, success: false, message: message
|
@@ -564,7 +741,11 @@ module DEBUGGER__
|
|
564
741
|
when :completions
|
565
742
|
@ui.respond req, result
|
566
743
|
else
|
567
|
-
|
744
|
+
if respond_to? mid = "custom_dap_request_event_#{type}"
|
745
|
+
__send__ mid, req, result
|
746
|
+
else
|
747
|
+
raise "unsupported: #{args.inspect}"
|
748
|
+
end
|
568
749
|
end
|
569
750
|
end
|
570
751
|
|
@@ -584,7 +765,37 @@ module DEBUGGER__
|
|
584
765
|
end
|
585
766
|
end
|
586
767
|
|
768
|
+
class NaiveString
|
769
|
+
attr_reader :str
|
770
|
+
def initialize str
|
771
|
+
@str = str
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
587
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
|
+
|
588
799
|
def process_dap args
|
589
800
|
# pp tc: self, args: args
|
590
801
|
type = args.shift
|
@@ -592,27 +803,43 @@ module DEBUGGER__
|
|
592
803
|
|
593
804
|
case type
|
594
805
|
when :backtrace
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
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
|
599
822
|
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
}
|
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
|
+
},
|
611
833
|
}
|
834
|
+
end
|
835
|
+
|
836
|
+
event! :protocol_result, :backtrace, req, {
|
837
|
+
stackFrames: frames,
|
838
|
+
totalFrames: @target_frames.size,
|
612
839
|
}
|
613
840
|
when :scopes
|
614
841
|
fid = args.shift
|
615
|
-
frame =
|
842
|
+
frame = get_frame(fid)
|
616
843
|
|
617
844
|
lnum =
|
618
845
|
if frame.binding
|
@@ -623,7 +850,7 @@ module DEBUGGER__
|
|
623
850
|
0
|
624
851
|
end
|
625
852
|
|
626
|
-
event! :
|
853
|
+
event! :protocol_result, :scopes, req, scopes: [{
|
627
854
|
name: 'Local variables',
|
628
855
|
presentationHint: 'locals',
|
629
856
|
# variablesReference: N, # filled by SESSION
|
@@ -634,34 +861,18 @@ module DEBUGGER__
|
|
634
861
|
name: 'Global variables',
|
635
862
|
presentationHint: 'globals',
|
636
863
|
variablesReference: 1, # GLOBAL
|
637
|
-
namedVariables:
|
864
|
+
namedVariables: safe_global_variables.size,
|
638
865
|
indexedVariables: 0,
|
639
866
|
expensive: false,
|
640
867
|
}]
|
641
868
|
when :scope
|
642
869
|
fid = args.shift
|
643
|
-
frame =
|
644
|
-
|
645
|
-
|
646
|
-
v = b.local_variable_get(name)
|
647
|
-
variable(name, v)
|
648
|
-
}
|
649
|
-
special_local_variables frame do |name, val|
|
650
|
-
vars.unshift variable(name, val)
|
651
|
-
end
|
652
|
-
vars.unshift variable('%self', b.receiver)
|
653
|
-
elsif lvars = frame.local_variables
|
654
|
-
vars = lvars.map{|var, val|
|
655
|
-
variable(var, val)
|
656
|
-
}
|
657
|
-
else
|
658
|
-
vars = [variable('%self', frame.self)]
|
659
|
-
special_local_variables frame do |name, val|
|
660
|
-
vars.push variable(name, val)
|
661
|
-
end
|
870
|
+
frame = get_frame(fid)
|
871
|
+
vars = collect_locals(frame).map do |var, val|
|
872
|
+
variable(var, val)
|
662
873
|
end
|
663
|
-
event! :dap_result, :scope, req, variables: vars, tid: self.id
|
664
874
|
|
875
|
+
event! :protocol_result, :scope, req, variables: vars, tid: self.id
|
665
876
|
when :variable
|
666
877
|
vid = args.shift
|
667
878
|
obj = @var_map[vid]
|
@@ -679,7 +890,7 @@ module DEBUGGER__
|
|
679
890
|
case obj
|
680
891
|
when Hash
|
681
892
|
vars = obj.map{|k, v|
|
682
|
-
variable(
|
893
|
+
variable(value_inspect(k), v,)
|
683
894
|
}
|
684
895
|
when Struct
|
685
896
|
vars = obj.members.map{|m|
|
@@ -688,13 +899,12 @@ module DEBUGGER__
|
|
688
899
|
when String
|
689
900
|
vars = [
|
690
901
|
variable('#length', obj.length),
|
691
|
-
variable('#encoding', obj.encoding)
|
902
|
+
variable('#encoding', obj.encoding),
|
692
903
|
]
|
904
|
+
printed_str = value_inspect(obj)
|
905
|
+
vars << variable('#dump', NaiveString.new(obj)) if printed_str.end_with?('...')
|
693
906
|
when Class, Module
|
694
|
-
vars
|
695
|
-
variable(iv, obj.instance_variable_get(iv))
|
696
|
-
}
|
697
|
-
vars.unshift variable('%ancestors', obj.ancestors[1..])
|
907
|
+
vars << variable('%ancestors', obj.ancestors[1..])
|
698
908
|
when Range
|
699
909
|
vars = [
|
700
910
|
variable('#begin', obj.begin),
|
@@ -702,62 +912,57 @@ module DEBUGGER__
|
|
702
912
|
]
|
703
913
|
end
|
704
914
|
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
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
|
709
921
|
end
|
710
922
|
end
|
711
|
-
event! :
|
923
|
+
event! :protocol_result, :variable, req, variables: (vars || []), tid: self.id
|
712
924
|
|
713
925
|
when :evaluate
|
714
926
|
fid, expr, context = args
|
715
|
-
frame =
|
927
|
+
frame = get_frame(fid)
|
716
928
|
message = nil
|
717
929
|
|
718
|
-
if frame && (b = frame.
|
719
|
-
|
720
|
-
special_local_variables current_frame do |name, var|
|
930
|
+
if frame && (b = frame.eval_binding)
|
931
|
+
special_local_variables frame do |name, var|
|
721
932
|
b.local_variable_set(name, var) if /\%/ !~ name
|
722
933
|
end
|
723
934
|
|
724
935
|
case context
|
725
936
|
when 'repl', 'watch'
|
726
|
-
|
727
|
-
result = b.eval(expr.to_s, '(DEBUG CONSOLE)')
|
728
|
-
rescue Exception => e
|
729
|
-
result = e
|
730
|
-
end
|
731
|
-
|
937
|
+
result = dap_eval b, expr, context, prompt: '(DEBUG CONSOLE)'
|
732
938
|
when 'hover'
|
733
939
|
case expr
|
734
940
|
when /\A\@\S/
|
735
941
|
begin
|
736
|
-
|
737
|
-
result = r.instance_variable_get(expr)
|
942
|
+
result = M_INSTANCE_VARIABLE_GET.bind_call(b.receiver, expr)
|
738
943
|
rescue NameError
|
739
944
|
message = "Error: Not defined instance variable: #{expr.inspect}"
|
740
945
|
end
|
741
946
|
when /\A\$\S/
|
742
|
-
|
947
|
+
safe_global_variables.each{|gvar|
|
743
948
|
if gvar.to_s == expr
|
744
949
|
result = eval(gvar.to_s)
|
745
950
|
break false
|
746
951
|
end
|
747
952
|
} and (message = "Error: Not defined global variable: #{expr.inspect}")
|
748
|
-
when /\
|
749
|
-
|
953
|
+
when /\Aself$/
|
954
|
+
result = b.receiver
|
955
|
+
when /(\A((::[A-Z]|[A-Z])\w*)+)/
|
956
|
+
unless result = search_const(b, $1)
|
750
957
|
message = "Error: Not defined constants: #{expr.inspect}"
|
751
958
|
end
|
752
959
|
else
|
753
960
|
begin
|
754
|
-
# try to check local variables
|
755
|
-
b.local_variable_defined?(expr) or raise NameError
|
756
961
|
result = b.local_variable_get(expr)
|
757
962
|
rescue NameError
|
758
963
|
# try to check method
|
759
|
-
if b.receiver
|
760
|
-
result = b.receiver
|
964
|
+
if M_RESPOND_TO_P.bind_call(b.receiver, expr, include_all: true)
|
965
|
+
result = M_METHOD.bind_call(b.receiver, expr)
|
761
966
|
else
|
762
967
|
message = "Error: Can not evaluate: #{expr.inspect}"
|
763
968
|
end
|
@@ -770,17 +975,19 @@ module DEBUGGER__
|
|
770
975
|
result = 'Error: Can not evaluate on this frame'
|
771
976
|
end
|
772
977
|
|
773
|
-
event! :
|
978
|
+
event! :protocol_result, :evaluate, req, message: message, tid: self.id, **evaluate_result(result)
|
774
979
|
|
775
980
|
when :completions
|
776
981
|
fid, text = args
|
777
|
-
frame =
|
982
|
+
frame = get_frame(fid)
|
778
983
|
|
779
984
|
if (b = frame&.binding) && word = text&.split(/[\s\{]/)&.last
|
780
985
|
words = IRB::InputCompletor::retrieve_completion_data(word, bind: b).compact
|
781
986
|
end
|
782
987
|
|
783
|
-
event! :
|
988
|
+
event! :protocol_result, :completions, req, targets: (words || []).map{|phrase|
|
989
|
+
detail = nil
|
990
|
+
|
784
991
|
if /\b([_a-zA-Z]\w*[!\?]?)\z/ =~ phrase
|
785
992
|
w = $1
|
786
993
|
else
|
@@ -788,30 +995,37 @@ module DEBUGGER__
|
|
788
995
|
end
|
789
996
|
|
790
997
|
begin
|
791
|
-
|
792
|
-
|
793
|
-
phrase += " (variable:#{DEBUGGER__.safe_inspect(v)})"
|
794
|
-
end
|
998
|
+
v = b.local_variable_get(w)
|
999
|
+
detail ="(variable: #{value_inspect(v)})"
|
795
1000
|
rescue NameError
|
796
1001
|
end
|
797
1002
|
|
798
1003
|
{
|
799
1004
|
label: phrase,
|
800
1005
|
text: w,
|
1006
|
+
detail: detail,
|
801
1007
|
}
|
802
1008
|
}
|
803
1009
|
|
804
1010
|
else
|
805
|
-
|
1011
|
+
if respond_to? mid = "custom_dap_request_#{type}"
|
1012
|
+
__send__ mid, req
|
1013
|
+
else
|
1014
|
+
raise "Unknown request: #{args.inspect}"
|
1015
|
+
end
|
806
1016
|
end
|
807
1017
|
end
|
808
1018
|
|
809
1019
|
def search_const b, expr
|
810
|
-
cs = expr.split('::')
|
811
|
-
[Object, *b.eval('Module.nesting')].reverse_each{|mod|
|
1020
|
+
cs = expr.delete_prefix('::').split('::')
|
1021
|
+
[Object, *b.eval('::Module.nesting')].reverse_each{|mod|
|
812
1022
|
if cs.all?{|c|
|
813
1023
|
if mod.const_defined?(c)
|
814
|
-
|
1024
|
+
begin
|
1025
|
+
mod = mod.const_get(c)
|
1026
|
+
rescue Exception
|
1027
|
+
false
|
1028
|
+
end
|
815
1029
|
else
|
816
1030
|
false
|
817
1031
|
end
|
@@ -824,11 +1038,17 @@ module DEBUGGER__
|
|
824
1038
|
end
|
825
1039
|
|
826
1040
|
def evaluate_result r
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
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
|
832
1052
|
end
|
833
1053
|
|
834
1054
|
def variable_ name, obj, indexedVariables: 0, namedVariables: 0
|
@@ -839,15 +1059,31 @@ module DEBUGGER__
|
|
839
1059
|
vid = 0
|
840
1060
|
end
|
841
1061
|
|
842
|
-
|
1062
|
+
namedVariables += M_INSTANCE_VARIABLES.bind_call(obj).size
|
843
1063
|
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
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
|
851
1087
|
end
|
852
1088
|
|
853
1089
|
def variable name, obj
|
@@ -857,7 +1093,7 @@ module DEBUGGER__
|
|
857
1093
|
when Hash
|
858
1094
|
variable_ name, obj, namedVariables: obj.size
|
859
1095
|
when String
|
860
|
-
variable_ name, obj, namedVariables: 3 # #
|
1096
|
+
variable_ name, obj, namedVariables: 3 # #length, #encoding, #to_str
|
861
1097
|
when Struct
|
862
1098
|
variable_ name, obj, namedVariables: obj.size
|
863
1099
|
when Class, Module
|