debug 1.0.0.alpha1 → 1.0.0.beta5

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/Rakefile CHANGED
@@ -7,4 +7,24 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList["test/**/*_test.rb"]
8
8
  end
9
9
 
10
- task :default => :test
10
+ require "rake/extensiontask"
11
+
12
+ task :build => :compile
13
+
14
+ Rake::ExtensionTask.new("debug") do |ext|
15
+ ext.lib_dir = "lib/debug"
16
+ end
17
+
18
+ task :default => [:clobber, :compile, :test, 'README.md']
19
+
20
+ file 'README.md' => ['lib/debug/session.rb', 'exe/rdbg', 'misc/README.md.erb'] do
21
+ require_relative 'lib/debug/session'
22
+ require 'erb'
23
+ File.write 'README.md', ERB.new(File.read('misc/README.md.erb')).result
24
+ puts 'README.md is updated.'
25
+ end
26
+
27
+ task :run => :compile do
28
+ system(RbConfig.ruby, *%w(-I ./lib test.rb))
29
+ end
30
+
data/TODO.md ADDED
@@ -0,0 +1,33 @@
1
+ # TODO
2
+
3
+ ## Basic functionality
4
+
5
+ * VScode DAP support
6
+ * Support Ractors
7
+ * Signal (SIGINT) support
8
+ * Remote connection with openssl
9
+
10
+ ## UI
11
+
12
+ * Coloring
13
+ * Interactive breakpoint setting
14
+ * irb integration
15
+ * Web browser integrated UI
16
+
17
+ ## Debug command
18
+
19
+ * Breakpoints
20
+ * Lightweight pending method break points with Ruby 3.1 feature (TP:method_added)
21
+ * Non-stop breakpoint but runs some code.
22
+ * Watch points
23
+ * Lightweight watchpoints for instance variables with Ruby 3.1 features (TP:ivar_set)
24
+ * Faster `next`/`finish` command by specifying target code.
25
+ * `set`/`show` configurations
26
+ * In-memory line traces
27
+ * Timemachine debugging
28
+
29
+ ## Tests
30
+
31
+ * Test framework
32
+ * Tests for commands
33
+ * Tests for remote debugging
data/debug.gemspec CHANGED
@@ -6,17 +6,14 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Koichi Sasada"]
7
7
  spec.email = ["ko1@atdot.net"]
8
8
 
9
- spec.summary = %q{debug.rb}
10
- spec.description = %q{debug.rb}
11
- spec.homepage = "https://github.com/ko1/debug"
12
- spec.license = "MIT"
9
+ spec.summary = %q{Debugging functionality for Ruby}
10
+ spec.description = %q{Debugging functionality for Ruby. This is completely rewritten debug.rb which was contained by the encient Ruby versions.}
11
+ spec.homepage = "https://github.com/ruby/debug"
12
+ spec.licenses = ["Ruby", "BSD-2-Clause"]
13
13
  spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
14
14
 
15
- # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
16
-
17
15
  spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = "https://github.com/ko1/debug"
19
- # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
16
+ spec.metadata["source_code_uri"] = spec.homepage
20
17
 
21
18
  # Specify which files should be added to the gem when it is released.
22
19
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -26,7 +23,7 @@ Gem::Specification.new do |spec|
26
23
  spec.bindir = "exe"
27
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
25
  spec.require_paths = ["lib"]
26
+ spec.extensions = ['ext/debug/extconf.rb']
29
27
 
30
- spec.add_runtime_dependency 'debug_inspector'
31
- spec.add_runtime_dependency 'iseq_collector'
28
+ spec.add_dependency "irb" # for its color_printer class, which was added after 1.3
32
29
  end
data/exe/rdbg CHANGED
@@ -1,3 +1,41 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "debug"
3
+ require_relative '../lib/debug/client'
4
+
5
+ config = DEBUGGER__.parse_argv(ARGV)
6
+
7
+ case config[:mode]
8
+ when :start
9
+ require 'rbconfig'
10
+
11
+ if config[:remote]
12
+ start_mode = "debug/open"
13
+ else
14
+ start_mode = "debug/run"
15
+ end
16
+
17
+ ::DEBUGGER__.config_to_env(config)
18
+
19
+ cmd = config[:command] ? ARGV.shift : RbConfig.ruby
20
+ exec({'RUBYOPT' => "-I#{File.expand_path(File.dirname(__dir__))}/lib -r #{start_mode}"},
21
+ cmd, *ARGV)
22
+
23
+ when :attach
24
+ require_relative "../lib/debug/client"
25
+
26
+ config.each{|k, v|
27
+ DEBUGGER__::CONFIG[k] = v
28
+ }
29
+
30
+ begin
31
+ if ARGV.empty? && config[:port]
32
+ connect [config[:host], config[:port]].compact
33
+ else
34
+ connect ARGV
35
+ end
36
+ rescue DEBUGGER__::CommandLineOptionError
37
+ puts opt.help
38
+ end
39
+ else
40
+ raise # assert
41
+ end
data/ext/debug/debug.c ADDED
@@ -0,0 +1,119 @@
1
+
2
+ #include "ruby/ruby.h"
3
+ #include "ruby/debug.h"
4
+ #include "ruby/encoding.h"
5
+
6
+ //
7
+ static VALUE rb_mDebugger;
8
+
9
+ // iseq
10
+ typedef struct rb_iseq_struct rb_iseq_t;
11
+ VALUE rb_iseq_realpath(const rb_iseq_t *iseq);
12
+
13
+ static VALUE
14
+ iseq_realpath(VALUE iseqw)
15
+ {
16
+ rb_iseq_t *iseq = DATA_PTR(iseqw);
17
+ return rb_iseq_realpath(iseq);
18
+ }
19
+
20
+ static VALUE rb_cFrameInfo;
21
+
22
+ static VALUE
23
+ di_entry(VALUE loc, VALUE self, VALUE binding, VALUE iseq, VALUE klass, VALUE depth)
24
+ {
25
+ return rb_struct_new(rb_cFrameInfo, loc, self, binding, iseq, klass, depth, Qnil, Qnil, Qnil, Qnil, Qnil);
26
+ }
27
+
28
+ static int
29
+ str_start_with(VALUE str, VALUE prefix)
30
+ {
31
+ StringValue(prefix);
32
+ rb_enc_check(str, prefix);
33
+ if (RSTRING_LEN(str) >= RSTRING_LEN(prefix) &&
34
+ memcmp(RSTRING_PTR(str), RSTRING_PTR(prefix), RSTRING_LEN(prefix)) == 0) {
35
+ return 1;
36
+ }
37
+ else {
38
+ return 0;
39
+ }
40
+ }
41
+
42
+ static VALUE
43
+ di_body(const rb_debug_inspector_t *dc, void *ptr)
44
+ {
45
+ VALUE skip_path_prefix = (VALUE)ptr;
46
+ VALUE locs = rb_debug_inspector_backtrace_locations(dc);
47
+ VALUE ary = rb_ary_new();
48
+ long len = RARRAY_LEN(locs);
49
+ long i;
50
+
51
+ for (i=1; i<len; i++) {
52
+ VALUE loc, e;
53
+ VALUE iseq = rb_debug_inspector_frame_iseq_get(dc, i);
54
+
55
+ if (!NIL_P(iseq)) {
56
+ VALUE path = iseq_realpath(iseq);
57
+ if (!NIL_P(path) && str_start_with(path, skip_path_prefix)) continue;
58
+ }
59
+
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));
67
+ rb_ary_push(ary, e);
68
+ }
69
+
70
+ return ary;
71
+ }
72
+
73
+ static VALUE
74
+ capture_frames(VALUE self, VALUE skip_path_prefix)
75
+ {
76
+ return rb_debug_inspector_open(di_body, (void *)skip_path_prefix);
77
+ }
78
+
79
+ static VALUE
80
+ frame_depth(VALUE self)
81
+ {
82
+ // TODO: more efficient API
83
+ VALUE bt = rb_make_backtrace();
84
+ return INT2FIX(RARRAY_LEN(bt));
85
+ }
86
+
87
+ static void
88
+ method_added_tracker(VALUE tpval, void *ptr)
89
+ {
90
+ rb_trace_arg_t *arg = rb_tracearg_from_tracepoint(tpval);
91
+ VALUE mid = rb_tracearg_callee_id(arg);
92
+
93
+ if (RB_UNLIKELY(mid == ID2SYM(rb_intern("method_added")) ||
94
+ mid == ID2SYM(rb_intern("singleton_method_added")))) {
95
+ VALUE args[] = {
96
+ tpval,
97
+ };
98
+ rb_funcallv(rb_mDebugger, rb_intern("method_added"), 1, args);
99
+ }
100
+ }
101
+
102
+ static VALUE
103
+ create_method_added_tracker(VALUE self)
104
+ {
105
+ return rb_tracepoint_new(0, RUBY_EVENT_CALL, method_added_tracker, NULL);
106
+ }
107
+
108
+ void Init_iseq_collector(void);
109
+
110
+ void
111
+ Init_debug(void)
112
+ {
113
+ rb_mDebugger = rb_const_get(rb_cObject, rb_intern("DEBUGGER__"));
114
+ rb_cFrameInfo = rb_const_get(rb_mDebugger, rb_intern("FrameInfo"));
115
+ rb_define_singleton_method(rb_mDebugger, "capture_frames", capture_frames, 1);
116
+ rb_define_singleton_method(rb_mDebugger, "frame_depth", frame_depth, 0);
117
+ rb_define_singleton_method(rb_mDebugger, "create_method_added_tracker", create_method_added_tracker, 0);
118
+ Init_iseq_collector();
119
+ }
@@ -0,0 +1,2 @@
1
+ require 'mkmf'
2
+ create_makefile 'debug/debug'
@@ -0,0 +1,91 @@
1
+ #include <ruby/ruby.h>
2
+
3
+ VALUE rb_iseqw_new(VALUE v);
4
+ void rb_objspace_each_objects(
5
+ int (*callback)(void *start, void *end, size_t stride, void *data),
6
+ void *data);
7
+ size_t rb_obj_memsize_of(VALUE);
8
+
9
+ // implementation specific.
10
+ enum imemo_type {
11
+ imemo_iseq = 7,
12
+ imemo_mask = 0x07
13
+ };
14
+
15
+ static inline enum imemo_type
16
+ imemo_type(VALUE imemo)
17
+ {
18
+ return (RBASIC(imemo)->flags >> FL_USHIFT) & imemo_mask;
19
+ }
20
+
21
+ static inline int
22
+ rb_obj_is_iseq(VALUE iseq)
23
+ {
24
+ return RB_TYPE_P(iseq, T_IMEMO) && imemo_type(iseq) == imemo_iseq;
25
+ }
26
+
27
+ struct iseq_i_data {
28
+ void (*func)(VALUE v, void *data);
29
+ void *data;
30
+ };
31
+
32
+ int
33
+ iseq_i(void *vstart, void *vend, size_t stride, void *ptr)
34
+ {
35
+ VALUE v;
36
+ struct iseq_i_data *data = (struct iseq_i_data *)ptr;
37
+
38
+ for (v = (VALUE)vstart; v != (VALUE)vend; v += stride) {
39
+ if (RBASIC(v)->flags) {
40
+ switch (BUILTIN_TYPE(v)) {
41
+ case T_IMEMO:
42
+ if (rb_obj_is_iseq(v)) {
43
+ data->func(v, data->data);
44
+ }
45
+ continue;
46
+ default:
47
+ continue;
48
+ }
49
+ }
50
+ }
51
+
52
+ return 0;
53
+ }
54
+
55
+ static void
56
+ each_iseq_i(VALUE v, void *ptr)
57
+ {
58
+ rb_yield(rb_iseqw_new(v));
59
+ }
60
+
61
+ static VALUE
62
+ each_iseq(VALUE self)
63
+ {
64
+ struct iseq_i_data data = {each_iseq_i, NULL};
65
+ rb_objspace_each_objects(iseq_i, &data);
66
+ return Qnil;
67
+ }
68
+
69
+ static void
70
+ count_iseq_i(VALUE v, void *ptr)
71
+ {
72
+ size_t *sizep = (size_t *)ptr;
73
+ *sizep += 1;
74
+ }
75
+
76
+ static VALUE
77
+ count_iseq(VALUE self)
78
+ {
79
+ size_t size = 0;
80
+ struct iseq_i_data data = {count_iseq_i, &size};
81
+ rb_objspace_each_objects(iseq_i, &data);
82
+ return SIZET2NUM(size);
83
+ }
84
+
85
+ void
86
+ Init_iseq_collector(void)
87
+ {
88
+ VALUE rb_mObjSpace = rb_const_get(rb_cObject, rb_intern("ObjectSpace"));
89
+ rb_define_singleton_method(rb_mObjSpace, "each_iseq", each_iseq, 0);
90
+ rb_define_singleton_method(rb_mObjSpace, "count_iseq", count_iseq, 0);
91
+ }
data/lib/debug.rb CHANGED
@@ -1 +1 @@
1
- require_relative 'debug/repl'
1
+ require_relative 'debug/session'
@@ -1,17 +1,14 @@
1
+ require_relative 'color'
2
+
1
3
  module DEBUGGER__
2
- class LineBreakpoint
3
- attr_reader :path, :line, :key
4
+ class Breakpoint
5
+ attr_reader :key
6
+
7
+ def initialize do_enable = true
8
+ @deleted = false
4
9
 
5
- def initialize type, iseq, line, cond = nil, oneshot: false
6
- @iseq = iseq
7
- @path = iseq.path
8
- @line = line
9
- @type = type
10
- @cond = cond
11
- @oneshot = oneshot
12
- @key = [@path, @line].freeze
13
10
  setup
14
- enable
11
+ enable if do_enable
15
12
  end
16
13
 
17
14
  def safe_eval b, expr
@@ -20,46 +17,196 @@ module DEBUGGER__
20
17
  puts "[EVAL ERROR]"
21
18
  puts " expr: #{expr}"
22
19
  puts " err: #{e} (#{e.class})"
20
+ puts "Error caused by #{self}."
23
21
  nil
24
22
  end
25
23
 
26
24
  def setup
25
+ raise "not implemented..."
26
+ end
27
+
28
+ def enable
29
+ @tp.enable
30
+ end
31
+
32
+ def disable
33
+ @tp&.disable
34
+ end
35
+
36
+ def enabled?
37
+ @tp.enabled?
38
+ end
39
+
40
+ def delete
41
+ disable
42
+ @deleted = true
43
+ end
44
+
45
+ def deleted?
46
+ @deleted
47
+ end
48
+
49
+ def suspend
50
+ ThreadClient.current.on_breakpoint @tp, self
51
+ end
52
+
53
+ def to_s
54
+ if @cond
55
+ " if #{@cond}"
56
+ else
57
+ ""
58
+ end
59
+ end
60
+
61
+ def description
62
+ to_s
63
+ end
64
+
65
+ class << self
66
+ include Color
67
+
68
+ def generate_label(name)
69
+ colorize(" BP - #{name} ", [:YELLOW, :BOLD, :REVERSE])
70
+ end
71
+ end
72
+ end
73
+
74
+ class LineBreakpoint < Breakpoint
75
+ LABEL = generate_label("Line")
76
+ PENDING_LABEL = generate_label("Line (pending)")
77
+
78
+ attr_reader :path, :line, :iseq
79
+
80
+ def initialize path, line, cond: nil, oneshot: false, hook_call: true
81
+ @path = path
82
+ @line = line
83
+ @cond = cond
84
+ @oneshot = oneshot
85
+ @hook_call = hook_call
86
+
87
+ @iseq = nil
88
+ @type = nil
89
+
90
+ @key = [@path, @line].freeze
91
+
92
+ super()
93
+ try_activate
94
+ end
95
+
96
+ def setup
97
+ return unless @type
98
+
27
99
  if !@cond
28
100
  @tp = TracePoint.new(@type) do |tp|
29
- tp.disable if @oneshot
30
- ThreadClient.current.on_breakpoint tp
101
+ delete if @oneshot
102
+ suspend
31
103
  end
32
104
  else
33
105
  @tp = TracePoint.new(@type) do |tp|
34
106
  next unless safe_eval tp.binding, @cond
35
- tp.disable if @oneshot
36
- ThreadClient.current.on_breakpoint tp
107
+ delete if @oneshot
108
+ suspend
37
109
  end
38
110
  end
39
111
  end
40
112
 
41
113
  def enable
114
+ return unless @iseq
115
+
42
116
  if @type == :line
43
117
  @tp.enable(target: @iseq, target_line: @line)
44
118
  else
45
119
  @tp.enable(target: @iseq)
46
120
  end
121
+
47
122
  rescue ArgumentError
48
123
  puts @iseq.disasm # for debug
49
124
  raise
50
125
  end
51
126
 
52
- def disable
53
- @tp.disable
127
+ def activate iseq, event, line
128
+ @iseq = iseq
129
+ @type = event
130
+ @line = line
131
+ @path = iseq.absolute_path
132
+
133
+ @key = [@path, @line].freeze
134
+ SESSION.rehash_bps
135
+
136
+ setup
137
+ enable
54
138
  end
55
139
 
56
- def to_s
57
- "line bp #{@iseq.absolute_path}:#{@line} (#{@type})" +
58
- if @cond
59
- "if #{@cond}"
60
- else
61
- ""
140
+ def activate_exact iseq, events, line
141
+ case
142
+ when events.include?(:RUBY_EVENT_CALL)
143
+ # "def foo" line set bp on the beginning of method foo
144
+ activate(iseq, :call, line)
145
+ when events.include?(:RUBY_EVENT_LINE)
146
+ activate(iseq, :line, line)
147
+ when events.include?(:RUBY_EVENT_RETURN)
148
+ activate(iseq, :return, line)
149
+ when events.include?(:RUBY_EVENT_B_RETURN)
150
+ activate(iseq, :b_return, line)
151
+ when events.include?(:RUBY_EVENT_END)
152
+ activate(iseq, :end, line)
153
+ else
154
+ # not actiavated
155
+ end
156
+ end
157
+
158
+ NearestISeq = Struct.new(:iseq, :line, :events)
159
+
160
+ def try_activate
161
+ nearest = nil # NearestISeq
162
+
163
+ ObjectSpace.each_iseq{|iseq|
164
+ if (iseq.absolute_path || iseq.path) == self.path &&
165
+ iseq.first_lineno <= self.line &&
166
+ iseq.type != :ensure # ensure iseq is copied (duplicated)
167
+
168
+ iseq.traceable_lines_norec(line_events = {})
169
+ lines = line_events.keys.sort
170
+
171
+ if !lines.empty? && lines.last >= line
172
+ nline = lines.bsearch{|l| line <= l}
173
+ events = line_events[nline]
174
+
175
+ next if events == [:RUBY_EVENT_B_CALL]
176
+
177
+ if @hook_call &&
178
+ events.include?(:RUBY_EVENT_CALL) &&
179
+ self.line == iseq.first_lineno
180
+ nline = iseq.first_lineno
181
+ end
182
+
183
+ if !nearest || ((line - nline).abs < (line - nearest.line).abs)
184
+ nearest = NearestISeq.new(iseq, nline, events)
185
+ else
186
+ if @hook_call && nearest.iseq.first_lineno <= iseq.first_lineno
187
+ if (nearest.line > line && !nearest.events.include?(:RUBY_EVENT_CALL)) ||
188
+ (events.include?(:RUBY_EVENT_CALL))
189
+ nearest = NearestISeq.new(iseq, nline, events)
190
+ end
191
+ end
192
+ end
193
+ end
62
194
  end
195
+ }
196
+
197
+ if nearest
198
+ activate_exact nearest.iseq, nearest.events, nearest.line
199
+ end
200
+ end
201
+
202
+ def to_s
203
+ oneshot = @oneshot ? " (oneshot)" : ""
204
+
205
+ if @iseq
206
+ "#{LABEL} #{@path}:#{@line} (#{@type})#{oneshot}" + super
207
+ else
208
+ "#{PENDING_LABEL} #{@path}:#{@line}#{oneshot}" + super
209
+ end
63
210
  end
64
211
 
65
212
  def inspect
@@ -67,31 +214,212 @@ module DEBUGGER__
67
214
  end
68
215
  end
69
216
 
70
- class CatchBreakpoint
71
- attr_reader :key
217
+ class CatchBreakpoint < Breakpoint
218
+ LABEL = generate_label("Catch")
219
+ attr_reader :last_exc
72
220
 
73
221
  def initialize pat
74
- @pat = pat
222
+ @pat = pat.freeze
223
+ @key = [:catch, @pat].freeze
224
+ @last_exc = nil
225
+
226
+ super()
227
+ end
228
+
229
+ def setup
75
230
  @tp = TracePoint.new(:raise){|tp|
76
231
  exc = tp.raised_exception
232
+ should_suspend = false
77
233
  exc.class.ancestors.each{|cls|
78
- if pat === cls.name
79
- puts "catch #{exc.class.inspect} by #{@pat.inspect}"
80
- ThreadClient.current.on_suspend :catch
234
+ if @pat === cls.name
235
+ should_suspend = true
236
+ @last_exc = exc
237
+ break
81
238
  end
82
239
  }
240
+ suspend if should_suspend
83
241
  }
84
- @tp.enable
242
+ end
85
243
 
86
- @key = pat.freeze
244
+ def to_s
245
+ "#{LABEL} #{@pat.inspect}"
87
246
  end
88
247
 
89
- def disable
90
- @tp.disable
248
+ def description
249
+ "#{@last_exc.inspect} is raised."
250
+ end
251
+ end
252
+
253
+ class CheckBreakpoint < Breakpoint
254
+ LABEL = generate_label("Check")
255
+
256
+ def initialize expr
257
+ @expr = expr.freeze
258
+ @key = [:check, @expr].freeze
259
+
260
+ super()
261
+ end
262
+
263
+ def setup
264
+ @tp = TracePoint.new(:line){|tp|
265
+ next if tp.path.start_with? __dir__
266
+ next if tp.path.start_with? '<internal:'
267
+ # Skip when `JSON.generate` is called during tests
268
+ next if tp.defined_class.to_s == '#<Class:JSON>' and ENV['RUBY_DEBUG_TEST_MODE']
269
+
270
+ if safe_eval tp.binding, @expr
271
+ suspend
272
+ end
273
+ }
274
+ end
275
+
276
+ def to_s
277
+ "#{LABEL} #{@expr}"
278
+ end
279
+ end
280
+
281
+ class WatchIVarBreakpoint < Breakpoint
282
+ LABEL = generate_label("Watch")
283
+
284
+ def initialize ivar, object, current
285
+ @ivar = ivar.to_sym
286
+ @object = object
287
+ @key = [:watch, @ivar].freeze
288
+
289
+ @current = current
290
+ super()
291
+ end
292
+
293
+ def watch_eval
294
+ result = @object.instance_variable_get(@ivar)
295
+ if result != @current
296
+ begin
297
+ @prev = @current
298
+ @current = result
299
+ suspend
300
+ ensure
301
+ remove_instance_variable(:@prev)
302
+ end
303
+ end
304
+ rescue Exception
305
+ false
306
+ end
307
+
308
+ def setup
309
+ @tp = TracePoint.new(:line, :return, :b_return){|tp|
310
+ next if tp.path.start_with? __dir__
311
+ next if tp.path.start_with? '<internal:'
312
+
313
+ watch_eval
314
+ }
91
315
  end
92
316
 
93
317
  def to_s
94
- "catch bp #{@pat.inspect}"
318
+ value_str =
319
+ if defined?(@prev)
320
+ "#{@prev} -> #{@current}"
321
+ else
322
+ "#{@current}"
323
+ end
324
+ "#{LABEL} #{@object} #{@ivar} = #{value_str}"
325
+ end
326
+ end
327
+
328
+ class MethodBreakpoint < Breakpoint
329
+ LABEL = generate_label("Method")
330
+ PENDING_LABEL = generate_label("Method (pending)")
331
+
332
+ attr_reader :sig_method_name, :method
333
+
334
+ def initialize b, klass_name, op, method_name, cond
335
+ @sig_klass_name = klass_name
336
+ @sig_op = op
337
+ @sig_method_name = method_name
338
+ @klass_eval_binding = b
339
+
340
+ @klass = nil
341
+ @method = nil
342
+ @cond = cond
343
+ @key = "#{klass_name}#{op}#{method_name}".freeze
344
+
345
+ super(false)
346
+ end
347
+
348
+ def setup
349
+ if @cond
350
+ @tp = TracePoint.new(:call){|tp|
351
+ next unless safe_eval tp.binding, @cond
352
+ suspend
353
+ }
354
+ else
355
+ @tp = TracePoint.new(:call){|tp|
356
+ suspend
357
+ }
358
+ end
359
+ end
360
+
361
+ def eval_class_name
362
+ return @klass if @klass
363
+ @klass = @klass_eval_binding.eval(@sig_klass_name)
364
+ @klass_eval_binding = nil
365
+ @klass
366
+ end
367
+
368
+ def search_method
369
+ case @sig_op
370
+ when '.'
371
+ @method = @klass.method(@sig_method_name)
372
+ when '#'
373
+ @method = @klass.instance_method(@sig_method_name)
374
+ else
375
+ raise "Unknown op: #{@sig_op}"
376
+ end
377
+ end
378
+
379
+ def enable
380
+ try_enable
381
+ end
382
+
383
+ def try_enable quiet: false
384
+ eval_class_name
385
+ search_method
386
+
387
+ begin
388
+ retried = false
389
+ @tp.enable(target: @method)
390
+
391
+ rescue ArgumentError
392
+ raise if retried
393
+ retried = true
394
+ sig_method_name = @sig_method_name
395
+
396
+ # maybe C method
397
+ @klass.module_eval do
398
+ orig_name = sig_method_name + '__orig__'
399
+ alias_method orig_name, sig_method_name
400
+ define_method(sig_method_name) do |*args|
401
+ send(orig_name, *args)
402
+ end
403
+ end
404
+
405
+ # re-collect the method object after the above patch
406
+ search_method
407
+ retry
408
+ end
409
+ rescue Exception
410
+ raise unless quiet
411
+ end
412
+
413
+ def sig
414
+ @key
415
+ end
416
+
417
+ def to_s
418
+ if @method
419
+ "#{LABEL} #{sig}"
420
+ else
421
+ "#{PENDING_LABEL} #{sig}"
422
+ end + super
95
423
  end
96
424
  end
97
425
  end