debug 1.0.0.alpha1 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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