debug 1.0.0.alpha1 → 1.0.0.beta1

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/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,5 @@ 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"]
29
-
30
- spec.add_runtime_dependency 'debug_inspector'
31
- spec.add_runtime_dependency 'iseq_collector'
26
+ spec.extensions = ['ext/debug/extconf.rb']
32
27
  end
data/exe/rdbg CHANGED
@@ -1,3 +1,34 @@
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
+ exec("#{RbConfig.ruby}", "-I#{File.expand_path(File.dirname(__dir__))}/lib", "-r" + start_mode, *ARGV)
19
+
20
+ when :attach
21
+ require_relative "../lib/debug/client"
22
+
23
+ begin
24
+ if ARGV.empty? && config[:port]
25
+ connect [config[:host], config[:port]].compact
26
+ else
27
+ connect ARGV
28
+ end
29
+ rescue DEBUGGER__::CommandLineOptionError
30
+ puts opt.help
31
+ end
32
+ else
33
+ raise # assert
34
+ end
data/ext/debug/debug.c ADDED
@@ -0,0 +1,118 @@
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);
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 iseq = rb_debug_inspector_frame_iseq_get(dc, i);
53
+
54
+ if (!NIL_P(iseq)) {
55
+ VALUE path = iseq_realpath(iseq);
56
+ if (!NIL_P(path) && str_start_with(path, skip_path_prefix)) continue;
57
+ }
58
+
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));
66
+ rb_ary_push(ary, e);
67
+ }
68
+
69
+ return ary;
70
+ }
71
+
72
+ static VALUE
73
+ capture_frames(VALUE self, VALUE skip_path_prefix)
74
+ {
75
+ return rb_debug_inspector_open(di_body, (void *)skip_path_prefix);
76
+ }
77
+
78
+ static VALUE
79
+ frame_depth(VALUE self)
80
+ {
81
+ // TODO: more efficient API
82
+ VALUE bt = rb_make_backtrace();
83
+ return INT2FIX(RARRAY_LEN(bt));
84
+ }
85
+
86
+ static void
87
+ method_added_tracker(VALUE tpval, void *ptr)
88
+ {
89
+ rb_trace_arg_t *arg = rb_tracearg_from_tracepoint(tpval);
90
+ VALUE mid = rb_tracearg_callee_id(arg);
91
+
92
+ if (RB_UNLIKELY(mid == ID2SYM(rb_intern("method_added")) ||
93
+ mid == ID2SYM(rb_intern("singleton_method_method_added")))) {
94
+ VALUE args[] = {
95
+ tpval,
96
+ };
97
+ rb_funcallv(rb_mDebugger, rb_intern("method_added"), 1, args);
98
+ }
99
+ }
100
+
101
+ static VALUE
102
+ create_method_added_tracker(VALUE self)
103
+ {
104
+ return rb_tracepoint_new(0, RUBY_EVENT_CALL, method_added_tracker, NULL);
105
+ }
106
+
107
+ void Init_iseq_collector(void);
108
+
109
+ void
110
+ Init_debug(void)
111
+ {
112
+ rb_mDebugger = rb_const_get(rb_cObject, rb_intern("DEBUGGER__"));
113
+ rb_cFrameInfo = rb_const_get(rb_mDebugger, rb_intern("FrameInfo"));
114
+ rb_define_singleton_method(rb_mDebugger, "capture_frames", capture_frames, 1);
115
+ rb_define_singleton_method(rb_mDebugger, "frame_depth", frame_depth, 0);
116
+ rb_define_singleton_method(rb_mDebugger, "create_method_added_tracker", create_method_added_tracker, 0);
117
+ Init_iseq_collector();
118
+ }
@@ -0,0 +1,2 @@
1
+ require 'mkmf'
2
+ create_makefile '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/run'
@@ -1,17 +1,12 @@
1
1
  module DEBUGGER__
2
- class LineBreakpoint
3
- attr_reader :path, :line, :key
2
+ class Breakpoint
3
+ attr_reader :key
4
+
5
+ def initialize do_enable = true
6
+ @deleted = false
4
7
 
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
8
  setup
14
- enable
9
+ enable if do_enable
15
10
  end
16
11
 
17
12
  def safe_eval b, expr
@@ -20,46 +15,178 @@ module DEBUGGER__
20
15
  puts "[EVAL ERROR]"
21
16
  puts " expr: #{expr}"
22
17
  puts " err: #{e} (#{e.class})"
18
+ puts "Error caused by #{self}."
23
19
  nil
24
20
  end
25
21
 
26
22
  def setup
23
+ raise "not implemented..."
24
+ end
25
+
26
+ def enable
27
+ @tp.enable
28
+ end
29
+
30
+ def disable
31
+ @tp.disable
32
+ end
33
+
34
+ def enabled?
35
+ @tp.enabled?
36
+ end
37
+
38
+ def delete
39
+ disable
40
+ @deleted = true
41
+ end
42
+
43
+ def deleted?
44
+ @deleted
45
+ end
46
+
47
+ def suspend
48
+ ThreadClient.current.on_breakpoint @tp, self
49
+ end
50
+
51
+ def to_s
52
+ if @cond
53
+ " if #{@cond}"
54
+ else
55
+ ""
56
+ end
57
+ end
58
+ end
59
+
60
+ class LineBreakpoint < Breakpoint
61
+ attr_reader :path, :line, :iseq
62
+
63
+ def initialize path, line, cond: nil, oneshot: false, hook_call: true
64
+ @path = path
65
+ @line = line
66
+ @cond = cond
67
+ @oneshot = oneshot
68
+ @hook_call = hook_call
69
+
70
+ @iseq = nil
71
+ @type = nil
72
+
73
+ @key = [@path, @line].freeze
74
+
75
+ super()
76
+ try_activate
77
+ end
78
+
79
+ def setup
80
+ return unless @type
81
+
27
82
  if !@cond
28
83
  @tp = TracePoint.new(@type) do |tp|
29
- tp.disable if @oneshot
30
- ThreadClient.current.on_breakpoint tp
84
+ delete if @oneshot
85
+ suspend
31
86
  end
32
87
  else
33
88
  @tp = TracePoint.new(@type) do |tp|
34
89
  next unless safe_eval tp.binding, @cond
35
- tp.disable if @oneshot
36
- ThreadClient.current.on_breakpoint tp
90
+ delete if @oneshot
91
+ suspend
37
92
  end
38
93
  end
39
94
  end
40
95
 
41
96
  def enable
97
+ return unless @iseq
98
+
42
99
  if @type == :line
43
100
  @tp.enable(target: @iseq, target_line: @line)
44
101
  else
45
102
  @tp.enable(target: @iseq)
46
103
  end
104
+
47
105
  rescue ArgumentError
48
106
  puts @iseq.disasm # for debug
49
107
  raise
50
108
  end
51
109
 
52
- def disable
53
- @tp.disable
110
+ def activate iseq, event, line
111
+ @iseq = iseq
112
+ @type = event
113
+ @line = line
114
+ @path = iseq.absolute_path
115
+
116
+ @key = [@path, @line].freeze
117
+ SESSION.rehash_bps
118
+
119
+ setup
120
+ enable
54
121
  end
55
122
 
56
- def to_s
57
- "line bp #{@iseq.absolute_path}:#{@line} (#{@type})" +
58
- if @cond
59
- "if #{@cond}"
60
- else
61
- ""
123
+ def activate_exact iseq, events, line
124
+ case
125
+ when events.include?(:RUBY_EVENT_CALL)
126
+ # "def foo" line set bp on the beggining of method foo
127
+ activate(iseq, :call, line)
128
+ when events.include?(:RUBY_EVENT_LINE)
129
+ activate(iseq, :line, line)
130
+ when events.include?(:RUBY_EVENT_RETURN)
131
+ activate(iseq, :return, line)
132
+ when events.include?(:RUBY_EVENT_B_RETURN)
133
+ activate(iseq, :b_return, line)
134
+ when events.include?(:RUBY_EVENT_END)
135
+ activate(iseq, :end, line)
136
+ else
137
+ # not actiavated
138
+ end
139
+ end
140
+
141
+ NearestISeq = Struct.new(:iseq, :line, :events)
142
+
143
+ def try_activate
144
+ nearest = nil # NearestISeq
145
+
146
+ ObjectSpace.each_iseq{|iseq|
147
+ if (iseq.absolute_path || iseq.path) == self.path && iseq.first_lineno <= self.line
148
+ iseq.traceable_lines_norec(line_events = {})
149
+ lines = line_events.keys.sort
150
+
151
+ if !lines.empty? && lines.last >= line
152
+ nline = lines.bsearch{|l| line <= l}
153
+ events = line_events[nline]
154
+
155
+ next if events == [:RUBY_EVENT_B_CALL]
156
+
157
+ if @hook_call &&
158
+ events.include?(:RUBY_EVENT_CALL) &&
159
+ self.line == iseq.first_lineno
160
+ nline = iseq.first_lineno
161
+ end
162
+
163
+ if !nearest || ((line - nline).abs < (line - nearest.line).abs)
164
+ nearest = NearestISeq.new(iseq, nline, events)
165
+ else
166
+ if @hook_call && nearest.iseq.first_lineno <= iseq.first_lineno
167
+ if (nearest.line > line && !nearest.events.include?(:RUBY_EVENT_CALL)) ||
168
+ (events.include?(:RUBY_EVENT_CALL))
169
+ nearest = NearestISeq.new(iseq, nline, events)
170
+ end
171
+ end
172
+ end
173
+ end
62
174
  end
175
+ }
176
+
177
+ if nearest
178
+ activate_exact nearest.iseq, nearest.events, nearest.line
179
+ end
180
+ end
181
+
182
+ def to_s
183
+ oneshot = @oneshot ? " (oneshot)" : ""
184
+
185
+ if @iseq
186
+ "line bp #{@path}:#{@line} (#{@type})#{oneshot}" + super
187
+ else
188
+ "line bp (pending) #{@path}:#{@line}#{oneshot}" + super
189
+ end
63
190
  end
64
191
 
65
192
  def inspect
@@ -67,31 +194,178 @@ module DEBUGGER__
67
194
  end
68
195
  end
69
196
 
70
- class CatchBreakpoint
71
- attr_reader :key
72
-
197
+ class CatchBreakpoint < Breakpoint
73
198
  def initialize pat
74
- @pat = pat
199
+ @pat = pat.freeze
200
+ @key = [:catch, @pat].freeze
201
+
202
+ super()
203
+ end
204
+
205
+ def setup
75
206
  @tp = TracePoint.new(:raise){|tp|
76
207
  exc = tp.raised_exception
77
208
  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
81
- end
209
+ suspend if pat === cls.name
82
210
  }
83
211
  }
84
- @tp.enable
212
+ end
85
213
 
86
- @key = pat.freeze
214
+ def to_s
215
+ "catch bp #{@pat.inspect}"
87
216
  end
217
+ end
88
218
 
89
- def disable
90
- @tp.disable
219
+ class CheckBreakpoint < Breakpoint
220
+ def initialize expr
221
+ @expr = expr.freeze
222
+ @key = [:check, @expr].freeze
223
+
224
+ super()
225
+ end
226
+
227
+ def setup
228
+ @tp = TracePoint.new(:line){|tp|
229
+ next if tp.path.start_with? __dir__
230
+ next if tp.path.start_with? '<internal:'
231
+
232
+ if safe_eval tp.binding, @expr
233
+ suspend
234
+ end
235
+ }
91
236
  end
92
237
 
93
238
  def to_s
94
- "catch bp #{@pat.inspect}"
239
+ "check bp: #{@expr}"
240
+ end
241
+ end
242
+
243
+ class WatchExprBreakpoint < Breakpoint
244
+ def initialize expr, current
245
+ @expr = expr.freeze
246
+ @key = [:watch, @expr].freeze
247
+
248
+ @current = current
249
+ super()
250
+ end
251
+
252
+ def watch_eval b
253
+ result = b.eval(@expr)
254
+ if result != @current
255
+ begin
256
+ @prev = @current
257
+ @current = result
258
+ suspend
259
+ ensure
260
+ remove_instance_variable(:@prev)
261
+ end
262
+ end
263
+ rescue Exception => e
264
+ false
265
+ end
266
+
267
+ def setup
268
+ @tp = TracePoint.new(:line, :return, :b_return){|tp|
269
+ next if tp.path.start_with? __dir__
270
+ next if tp.path.start_with? '<internal:'
271
+
272
+ watch_eval(tp.binding)
273
+ }
274
+ end
275
+
276
+ def to_s
277
+ if defined? @prev
278
+ "watch bp: #{@expr} = #{@prev} -> #{@current}"
279
+ else
280
+ "watch bp: #{@expr} = #{@current}"
281
+ end
282
+ end
283
+ end
284
+
285
+ class MethodBreakpoint < Breakpoint
286
+ attr_reader :sig_method_name, :method
287
+
288
+ def initialize b, klass_name, op, method_name, cond
289
+ @sig_klass_name = klass_name
290
+ @sig_op = op
291
+ @sig_method_name = method_name
292
+ @klass_eval_binding = b
293
+
294
+ @klass = nil
295
+ @method = nil
296
+ @cond = cond
297
+ @key = "#{klass_name}#{op}#{method_name}".freeze
298
+
299
+ super(false)
300
+ end
301
+
302
+ def setup
303
+ if @cond
304
+ @tp = TracePoint.new(:call){|tp|
305
+ next unless safe_eval tp.binding, @cond
306
+ suspend
307
+ }
308
+ else
309
+ @tp = TracePoint.new(:call){|tp|
310
+ suspend
311
+ }
312
+ end
313
+ end
314
+
315
+ def eval_class_name
316
+ return @klass if @klass
317
+ @klass = @klass_eval_binding.eval(@sig_klass_name)
318
+ @klass_eval_binding = nil
319
+ @klass
320
+ end
321
+
322
+ def search_method
323
+ case @sig_op
324
+ when '.'
325
+ @method = @klass.method(@sig_method_name)
326
+ when '#'
327
+ @method = @klass.instance_method(@sig_method_name)
328
+ else
329
+ raise "Unknown op: #{@sig_op}"
330
+ end
331
+ end
332
+
333
+ def enable quiet: false
334
+ eval_class_name
335
+ search_method
336
+
337
+ begin
338
+ retried = false
339
+ @tp.enable(target: @method)
340
+
341
+ rescue ArgumentError => e
342
+ raise if retried
343
+ retried = true
344
+
345
+ # maybe C method
346
+ @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|
350
+ send(orig_name, *args)
351
+ end
352
+ end
353
+ retry
354
+ end
355
+ rescue Exception
356
+ raise unless quiet
357
+ end
358
+
359
+ def sig
360
+ @key
361
+ end
362
+
363
+ def to_s
364
+ if @method
365
+ "method bp: #{sig}"
366
+ else
367
+ "method bp (pending): #{sig}"
368
+ end + super
95
369
  end
96
370
  end
97
371
  end