debug 1.0.0.beta2 → 1.0.0.beta7
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/.github/workflows/ruby.yml +34 -0
- data/.gitignore +3 -0
- data/CONTRIBUTING.md +336 -0
- data/Gemfile +2 -2
- data/README.md +190 -73
- data/TODO.md +27 -0
- data/bin/gentest +22 -0
- data/debug.gemspec +2 -0
- data/exe/rdbg +14 -11
- data/ext/debug/debug.c +10 -9
- data/lib/debug.rb +4 -1
- data/lib/debug/breakpoint.rb +110 -45
- data/lib/debug/client.rb +55 -13
- data/lib/debug/color.rb +76 -0
- data/lib/debug/config.rb +157 -33
- data/lib/debug/console.rb +26 -2
- data/lib/debug/frame_info.rb +145 -0
- data/lib/debug/open.rb +3 -0
- data/lib/debug/run.rb +4 -1
- data/lib/debug/server.rb +103 -50
- data/lib/debug/server_dap.rb +607 -0
- data/lib/debug/session.rb +534 -169
- data/lib/debug/source_repository.rb +64 -12
- data/lib/debug/thread_client.rb +211 -126
- data/lib/debug/version.rb +3 -1
- data/misc/README.md.erb +95 -47
- metadata +24 -3
data/TODO.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# TODO
|
2
|
+
|
3
|
+
## Basic functionality
|
4
|
+
|
5
|
+
* Support Ractors
|
6
|
+
* Signal (SIGINT) support
|
7
|
+
|
8
|
+
## UI
|
9
|
+
|
10
|
+
* Interactive breakpoint setting
|
11
|
+
* irb integration
|
12
|
+
* Web browser integrated UI
|
13
|
+
|
14
|
+
## Debug command
|
15
|
+
|
16
|
+
* Breakpoints
|
17
|
+
* Lightweight pending method break points with Ruby 3.1 feature (TP:method_added)
|
18
|
+
* Non-stop breakpoint but runs some code.
|
19
|
+
* Watch points
|
20
|
+
* Lightweight watchpoints for instance variables with Ruby 3.1 features (TP:ivar_set)
|
21
|
+
* Faster `next`/`finish` command by specifying target code.
|
22
|
+
* `set`/`show` configurations
|
23
|
+
* In-memory line traces
|
24
|
+
* Timemachine debugging
|
25
|
+
|
26
|
+
## Tests
|
27
|
+
|
data/bin/gentest
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
require_relative '../test/tool/test_builder'
|
6
|
+
|
7
|
+
file_info = {}
|
8
|
+
|
9
|
+
OptionParser.new do |opt|
|
10
|
+
opt.banner = 'Usage: bin/gentest [file] [option]'
|
11
|
+
opt.on('-m METHOD', 'Method name in the test file') do |m|
|
12
|
+
file_info[:method] = m
|
13
|
+
end
|
14
|
+
opt.on('-c CLASS', 'Class name in the test file') do |c|
|
15
|
+
file_info[:class] = c
|
16
|
+
end
|
17
|
+
opt.parse!(ARGV)
|
18
|
+
end
|
19
|
+
|
20
|
+
exit if ARGV.empty?
|
21
|
+
|
22
|
+
DEBUGGER__::TestBuilder.new(ARGV, file_info[:method], file_info[:class]).start
|
data/debug.gemspec
CHANGED
@@ -24,4 +24,6 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
25
|
spec.require_paths = ["lib"]
|
26
26
|
spec.extensions = ['ext/debug/extconf.rb']
|
27
|
+
|
28
|
+
spec.add_dependency "irb" # for its color_printer class, which was added after 1.3
|
27
29
|
end
|
data/exe/rdbg
CHANGED
@@ -1,30 +1,33 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require_relative '../lib/debug/
|
4
|
-
|
3
|
+
require_relative '../lib/debug/config'
|
5
4
|
config = DEBUGGER__.parse_argv(ARGV)
|
6
5
|
|
7
6
|
case config[:mode]
|
8
7
|
when :start
|
9
8
|
require 'rbconfig'
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
start_mode = "debug/run"
|
15
|
-
end
|
10
|
+
libpath = File.join(File.expand_path(File.dirname(__dir__)), 'lib/debug')
|
11
|
+
start_mode = config[:remote] ? "open" : 'run'
|
12
|
+
cmd = config[:command] ? ARGV.shift : RbConfig.ruby
|
16
13
|
|
17
|
-
::DEBUGGER__.
|
18
|
-
|
14
|
+
env = ::DEBUGGER__.config_to_env_hash(config)
|
15
|
+
env['RUBYOPT'] = "-r #{libpath}/#{start_mode}"
|
16
|
+
|
17
|
+
exec(env, cmd, *ARGV)
|
19
18
|
|
20
19
|
when :attach
|
21
20
|
require_relative "../lib/debug/client"
|
22
21
|
|
22
|
+
config.each{|k, v|
|
23
|
+
DEBUGGER__::CONFIG[k] = v
|
24
|
+
}
|
25
|
+
|
23
26
|
begin
|
24
27
|
if ARGV.empty? && config[:port]
|
25
|
-
|
28
|
+
DEBUGGER__::Client.new([config[:host], config[:port]].compact).connect
|
26
29
|
else
|
27
|
-
connect
|
30
|
+
DEBUGGER__::Client.new(ARGV).connect
|
28
31
|
end
|
29
32
|
rescue DEBUGGER__::CommandLineOptionError
|
30
33
|
puts opt.help
|
data/ext/debug/debug.c
CHANGED
@@ -22,7 +22,7 @@ static VALUE rb_cFrameInfo;
|
|
22
22
|
static VALUE
|
23
23
|
di_entry(VALUE loc, VALUE self, VALUE binding, VALUE iseq, VALUE klass, VALUE depth)
|
24
24
|
{
|
25
|
-
return rb_struct_new(rb_cFrameInfo, loc, self, binding, iseq, klass, depth, Qnil, Qnil, Qnil);
|
25
|
+
return rb_struct_new(rb_cFrameInfo, loc, self, binding, iseq, klass, depth, Qnil, Qnil, Qnil, Qnil, Qnil);
|
26
26
|
}
|
27
27
|
|
28
28
|
static int
|
@@ -49,6 +49,7 @@ di_body(const rb_debug_inspector_t *dc, void *ptr)
|
|
49
49
|
long i;
|
50
50
|
|
51
51
|
for (i=1; i<len; i++) {
|
52
|
+
VALUE loc, e;
|
52
53
|
VALUE iseq = rb_debug_inspector_frame_iseq_get(dc, i);
|
53
54
|
|
54
55
|
if (!NIL_P(iseq)) {
|
@@ -56,13 +57,13 @@ di_body(const rb_debug_inspector_t *dc, void *ptr)
|
|
56
57
|
if (!NIL_P(path) && str_start_with(path, skip_path_prefix)) continue;
|
57
58
|
}
|
58
59
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
60
|
+
loc = RARRAY_AREF(locs, i);
|
61
|
+
e = di_entry(loc,
|
62
|
+
rb_debug_inspector_frame_self_get(dc, i),
|
63
|
+
rb_debug_inspector_frame_binding_get(dc, i),
|
64
|
+
iseq,
|
65
|
+
rb_debug_inspector_frame_class_get(dc, i),
|
66
|
+
INT2FIX(len - i));
|
66
67
|
rb_ary_push(ary, e);
|
67
68
|
}
|
68
69
|
|
@@ -90,7 +91,7 @@ method_added_tracker(VALUE tpval, void *ptr)
|
|
90
91
|
VALUE mid = rb_tracearg_callee_id(arg);
|
91
92
|
|
92
93
|
if (RB_UNLIKELY(mid == ID2SYM(rb_intern("method_added")) ||
|
93
|
-
mid == ID2SYM(rb_intern("
|
94
|
+
mid == ID2SYM(rb_intern("singleton_method_added")))) {
|
94
95
|
VALUE args[] = {
|
95
96
|
tpval,
|
96
97
|
};
|
data/lib/debug.rb
CHANGED
data/lib/debug/breakpoint.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'color'
|
4
|
+
|
1
5
|
module DEBUGGER__
|
2
6
|
class Breakpoint
|
3
7
|
attr_reader :key
|
@@ -28,7 +32,7 @@ module DEBUGGER__
|
|
28
32
|
end
|
29
33
|
|
30
34
|
def disable
|
31
|
-
@tp
|
35
|
+
@tp&.disable
|
32
36
|
end
|
33
37
|
|
34
38
|
def enabled?
|
@@ -45,27 +49,50 @@ module DEBUGGER__
|
|
45
49
|
end
|
46
50
|
|
47
51
|
def suspend
|
52
|
+
if @command
|
53
|
+
provider, pre_cmds, do_cmds = @command
|
54
|
+
nonstop = true if do_cmds
|
55
|
+
cmds = [*pre_cmds&.split(';;'), *do_cmds&.split(';;')]
|
56
|
+
SESSION.add_preset_commands provider, cmds, kick: false, continue: nonstop
|
57
|
+
end
|
58
|
+
|
48
59
|
ThreadClient.current.on_breakpoint @tp, self
|
49
60
|
end
|
50
61
|
|
51
62
|
def to_s
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
63
|
+
s = ''.dup
|
64
|
+
s << " if: #{@cond}" if defined?(@cond) && @cond
|
65
|
+
s << " pre: #{@command[1]}" if defined?(@command) && @command && @command[1]
|
66
|
+
s << " do: #{@command[2]}" if defined?(@command) && @command && @command[2]
|
67
|
+
s
|
68
|
+
end
|
69
|
+
|
70
|
+
def description
|
71
|
+
to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
def duplicable?
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
include Color
|
79
|
+
|
80
|
+
def generate_label(name)
|
81
|
+
colorize(" BP - #{name} ", [:YELLOW, :BOLD, :REVERSE])
|
57
82
|
end
|
58
83
|
end
|
59
84
|
|
60
85
|
class LineBreakpoint < Breakpoint
|
61
86
|
attr_reader :path, :line, :iseq
|
62
87
|
|
63
|
-
def initialize path, line, cond: nil, oneshot: false, hook_call: true
|
88
|
+
def initialize path, line, cond: nil, oneshot: false, hook_call: true, command: nil
|
64
89
|
@path = path
|
65
90
|
@line = line
|
66
91
|
@cond = cond
|
67
92
|
@oneshot = oneshot
|
68
93
|
@hook_call = hook_call
|
94
|
+
@command = command
|
95
|
+
@pending = false
|
69
96
|
|
70
97
|
@iseq = nil
|
71
98
|
@type = nil
|
@@ -73,23 +100,21 @@ module DEBUGGER__
|
|
73
100
|
@key = [@path, @line].freeze
|
74
101
|
|
75
102
|
super()
|
103
|
+
|
76
104
|
try_activate
|
105
|
+
@pending = !@iseq
|
77
106
|
end
|
78
107
|
|
79
108
|
def setup
|
80
109
|
return unless @type
|
81
110
|
|
82
|
-
|
83
|
-
|
84
|
-
delete if @oneshot
|
85
|
-
suspend
|
86
|
-
end
|
87
|
-
else
|
88
|
-
@tp = TracePoint.new(@type) do |tp|
|
111
|
+
@tp = TracePoint.new(@type) do |tp|
|
112
|
+
if @cond
|
89
113
|
next unless safe_eval tp.binding, @cond
|
90
|
-
delete if @oneshot
|
91
|
-
suspend
|
92
114
|
end
|
115
|
+
delete if @oneshot
|
116
|
+
|
117
|
+
suspend
|
93
118
|
end
|
94
119
|
end
|
95
120
|
|
@@ -115,9 +140,12 @@ module DEBUGGER__
|
|
115
140
|
|
116
141
|
@key = [@path, @line].freeze
|
117
142
|
SESSION.rehash_bps
|
118
|
-
|
119
143
|
setup
|
120
144
|
enable
|
145
|
+
|
146
|
+
if @pending && !@oneshot
|
147
|
+
DEBUGGER__.warn "#{self} is activated."
|
148
|
+
end
|
121
149
|
end
|
122
150
|
|
123
151
|
def activate_exact iseq, events, line
|
@@ -138,13 +166,20 @@ module DEBUGGER__
|
|
138
166
|
end
|
139
167
|
end
|
140
168
|
|
169
|
+
def duplicable?
|
170
|
+
@oneshot
|
171
|
+
end
|
172
|
+
|
141
173
|
NearestISeq = Struct.new(:iseq, :line, :events)
|
142
174
|
|
143
175
|
def try_activate
|
144
176
|
nearest = nil # NearestISeq
|
145
177
|
|
146
178
|
ObjectSpace.each_iseq{|iseq|
|
147
|
-
if (iseq.absolute_path || iseq.path) == self.path &&
|
179
|
+
if (iseq.absolute_path || iseq.path) == self.path &&
|
180
|
+
iseq.first_lineno <= self.line &&
|
181
|
+
iseq.type != :ensure # ensure iseq is copied (duplicated)
|
182
|
+
|
148
183
|
iseq.traceable_lines_norec(line_events = {})
|
149
184
|
lines = line_events.keys.sort
|
150
185
|
|
@@ -183,9 +218,9 @@ module DEBUGGER__
|
|
183
218
|
oneshot = @oneshot ? " (oneshot)" : ""
|
184
219
|
|
185
220
|
if @iseq
|
186
|
-
"
|
221
|
+
"#{generate_label("Line")} #{@path}:#{@line} (#{@type})#{oneshot}" + super
|
187
222
|
else
|
188
|
-
"
|
223
|
+
"#{generate_label("Line (pending)")} #{@path}:#{@line}#{oneshot}" + super
|
189
224
|
end
|
190
225
|
end
|
191
226
|
|
@@ -195,9 +230,12 @@ module DEBUGGER__
|
|
195
230
|
end
|
196
231
|
|
197
232
|
class CatchBreakpoint < Breakpoint
|
233
|
+
attr_reader :last_exc
|
234
|
+
|
198
235
|
def initialize pat
|
199
236
|
@pat = pat.freeze
|
200
237
|
@key = [:catch, @pat].freeze
|
238
|
+
@last_exc = nil
|
201
239
|
|
202
240
|
super()
|
203
241
|
end
|
@@ -205,14 +243,26 @@ module DEBUGGER__
|
|
205
243
|
def setup
|
206
244
|
@tp = TracePoint.new(:raise){|tp|
|
207
245
|
exc = tp.raised_exception
|
246
|
+
next if SystemExit === exc
|
247
|
+
should_suspend = false
|
248
|
+
|
208
249
|
exc.class.ancestors.each{|cls|
|
209
|
-
|
250
|
+
if @pat === cls.name
|
251
|
+
should_suspend = true
|
252
|
+
@last_exc = exc
|
253
|
+
break
|
254
|
+
end
|
210
255
|
}
|
256
|
+
suspend if should_suspend
|
211
257
|
}
|
212
258
|
end
|
213
259
|
|
214
260
|
def to_s
|
215
|
-
"
|
261
|
+
"#{generate_label("Catch")} #{@pat.inspect}"
|
262
|
+
end
|
263
|
+
|
264
|
+
def description
|
265
|
+
"#{@last_exc.inspect} is raised."
|
216
266
|
end
|
217
267
|
end
|
218
268
|
|
@@ -228,6 +278,8 @@ module DEBUGGER__
|
|
228
278
|
@tp = TracePoint.new(:line){|tp|
|
229
279
|
next if tp.path.start_with? __dir__
|
230
280
|
next if tp.path.start_with? '<internal:'
|
281
|
+
# Skip when `JSON.generate` is called during tests
|
282
|
+
next if tp.defined_class.to_s == '#<Class:JSON>' and ENV['RUBY_DEBUG_TEST_MODE']
|
231
283
|
|
232
284
|
if safe_eval tp.binding, @expr
|
233
285
|
suspend
|
@@ -236,21 +288,22 @@ module DEBUGGER__
|
|
236
288
|
end
|
237
289
|
|
238
290
|
def to_s
|
239
|
-
"
|
291
|
+
"#{generate_label("Check")} #{@expr}"
|
240
292
|
end
|
241
293
|
end
|
242
294
|
|
243
|
-
class
|
244
|
-
def initialize
|
245
|
-
@
|
246
|
-
@
|
295
|
+
class WatchIVarBreakpoint < Breakpoint
|
296
|
+
def initialize ivar, object, current
|
297
|
+
@ivar = ivar.to_sym
|
298
|
+
@object = object
|
299
|
+
@key = [:watch, @ivar].freeze
|
247
300
|
|
248
301
|
@current = current
|
249
302
|
super()
|
250
303
|
end
|
251
304
|
|
252
|
-
def watch_eval
|
253
|
-
result =
|
305
|
+
def watch_eval
|
306
|
+
result = @object.instance_variable_get(@ivar)
|
254
307
|
if result != @current
|
255
308
|
begin
|
256
309
|
@prev = @current
|
@@ -260,7 +313,7 @@ module DEBUGGER__
|
|
260
313
|
remove_instance_variable(:@prev)
|
261
314
|
end
|
262
315
|
end
|
263
|
-
rescue Exception
|
316
|
+
rescue Exception
|
264
317
|
false
|
265
318
|
end
|
266
319
|
|
@@ -269,23 +322,25 @@ module DEBUGGER__
|
|
269
322
|
next if tp.path.start_with? __dir__
|
270
323
|
next if tp.path.start_with? '<internal:'
|
271
324
|
|
272
|
-
watch_eval
|
325
|
+
watch_eval
|
273
326
|
}
|
274
327
|
end
|
275
328
|
|
276
329
|
def to_s
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
330
|
+
value_str =
|
331
|
+
if defined?(@prev)
|
332
|
+
"#{@prev} -> #{@current}"
|
333
|
+
else
|
334
|
+
"#{@current}"
|
335
|
+
end
|
336
|
+
"#{generate_label("Watch")} #{@object} #{@ivar} = #{value_str}"
|
282
337
|
end
|
283
338
|
end
|
284
339
|
|
285
340
|
class MethodBreakpoint < Breakpoint
|
286
341
|
attr_reader :sig_method_name, :method
|
287
342
|
|
288
|
-
def initialize b, klass_name, op, method_name, cond
|
343
|
+
def initialize b, klass_name, op, method_name, cond, command: nil
|
289
344
|
@sig_klass_name = klass_name
|
290
345
|
@sig_op = op
|
291
346
|
@sig_method_name = method_name
|
@@ -294,6 +349,7 @@ module DEBUGGER__
|
|
294
349
|
@klass = nil
|
295
350
|
@method = nil
|
296
351
|
@cond = cond
|
352
|
+
@command = command
|
297
353
|
@key = "#{klass_name}#{op}#{method_name}".freeze
|
298
354
|
|
299
355
|
super(false)
|
@@ -330,30 +386,39 @@ module DEBUGGER__
|
|
330
386
|
end
|
331
387
|
end
|
332
388
|
|
333
|
-
def enable
|
389
|
+
def enable
|
390
|
+
try_enable
|
391
|
+
end
|
392
|
+
|
393
|
+
def try_enable added: false
|
334
394
|
eval_class_name
|
335
395
|
search_method
|
336
396
|
|
337
397
|
begin
|
338
398
|
retried = false
|
339
399
|
@tp.enable(target: @method)
|
400
|
+
DEBUGGER__.warn "#{self} is activated." if added
|
340
401
|
|
341
|
-
rescue ArgumentError
|
402
|
+
rescue ArgumentError
|
342
403
|
raise if retried
|
343
404
|
retried = true
|
405
|
+
sig_method_name = @sig_method_name
|
344
406
|
|
345
407
|
# maybe C method
|
346
408
|
@klass.module_eval do
|
347
|
-
orig_name =
|
348
|
-
alias_method orig_name,
|
349
|
-
define_method(
|
409
|
+
orig_name = sig_method_name + '__orig__'
|
410
|
+
alias_method orig_name, sig_method_name
|
411
|
+
define_method(sig_method_name) do |*args|
|
350
412
|
send(orig_name, *args)
|
351
413
|
end
|
352
414
|
end
|
415
|
+
|
416
|
+
# re-collect the method object after the above patch
|
417
|
+
search_method
|
353
418
|
retry
|
354
419
|
end
|
355
420
|
rescue Exception
|
356
|
-
raise unless
|
421
|
+
raise unless added
|
357
422
|
end
|
358
423
|
|
359
424
|
def sig
|
@@ -362,9 +427,9 @@ module DEBUGGER__
|
|
362
427
|
|
363
428
|
def to_s
|
364
429
|
if @method
|
365
|
-
"
|
430
|
+
"#{generate_label("Method")} #{sig} at #{@method.source_location.join(':')}"
|
366
431
|
else
|
367
|
-
"
|
432
|
+
"#{generate_label("Method (pending)")} #{sig}"
|
368
433
|
end + super
|
369
434
|
end
|
370
435
|
end
|