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.
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/client'
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
- if config[:remote]
12
- start_mode = "debug/open"
13
- else
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__.config_to_env(config)
18
- exec("#{RbConfig.ruby}", "-I#{File.expand_path(File.dirname(__dir__))}/lib", "-r" + start_mode, *ARGV)
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
- connect [config[:host], config[:port]].compact
28
+ DEBUGGER__::Client.new([config[:host], config[:port]].compact).connect
26
29
  else
27
- connect ARGV
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
- VALUE loc = RARRAY_AREF(locs, i);
60
- VALUE e = di_entry(loc,
61
- rb_debug_inspector_frame_self_get(dc, i),
62
- rb_debug_inspector_frame_binding_get(dc, i),
63
- iseq,
64
- rb_debug_inspector_frame_class_get(dc, i),
65
- INT2FIX(len - i));
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("singleton_method_method_added")))) {
94
+ mid == ID2SYM(rb_intern("singleton_method_added")))) {
94
95
  VALUE args[] = {
95
96
  tpval,
96
97
  };
data/lib/debug.rb CHANGED
@@ -1 +1,4 @@
1
- require_relative 'debug/run'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'debug/session'
4
+ DEBUGGER__::start no_sigint_hook: true, nonstop: true
@@ -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.disable
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
- if @cond
53
- " if #{@cond}"
54
- else
55
- ""
56
- end
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
- if !@cond
83
- @tp = TracePoint.new(@type) do |tp|
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 && iseq.first_lineno <= self.line
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
- "line bp #{@path}:#{@line} (#{@type})#{oneshot}" + super
221
+ "#{generate_label("Line")} #{@path}:#{@line} (#{@type})#{oneshot}" + super
187
222
  else
188
- "line bp (pending) #{@path}:#{@line}#{oneshot}" + super
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
- suspend if pat === cls.name
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
- "catch bp #{@pat.inspect}"
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
- "check bp: #{@expr}"
291
+ "#{generate_label("Check")} #{@expr}"
240
292
  end
241
293
  end
242
294
 
243
- class WatchExprBreakpoint < Breakpoint
244
- def initialize expr, current
245
- @expr = expr.freeze
246
- @key = [:watch, @expr].freeze
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 b
253
- result = b.eval(@expr)
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 => e
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(tp.binding)
325
+ watch_eval
273
326
  }
274
327
  end
275
328
 
276
329
  def to_s
277
- if defined? @prev
278
- "watch bp: #{@expr} = #{@prev} -> #{@current}"
279
- else
280
- "watch bp: #{@expr} = #{@current}"
281
- end
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 quiet: false
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 => e
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 = @sig_method_name + '__orig__'
348
- alias_method orig_name, @sig_method_name
349
- define_method(@sig_method_name) do |*args|
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 quiet
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
- "method bp: #{sig}"
430
+ "#{generate_label("Method")} #{sig} at #{@method.source_location.join(':')}"
366
431
  else
367
- "method bp (pending): #{sig}"
432
+ "#{generate_label("Method (pending)")} #{sig}"
368
433
  end + super
369
434
  end
370
435
  end