debug 1.0.0.beta5 → 1.0.0.rc1

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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'color'
2
4
 
3
5
  module DEBUGGER__
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'session'
4
+ return unless defined?(DEBUGGER__)
5
+ DEBUGGER__.start
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'objspace'
2
4
  require 'pp'
3
5
 
@@ -5,6 +7,17 @@ require_relative 'frame_info'
5
7
  require_relative 'color'
6
8
 
7
9
  module DEBUGGER__
10
+ module SkipPathHelper
11
+ def skip_path?(path)
12
+ (skip_paths = CONFIG[:skip_path]) && skip_paths.any?{|skip_path| path.match?(skip_path)}
13
+ end
14
+
15
+ def skip_location?(loc)
16
+ loc_path = loc.absolute_path || "!eval:#{loc.path}"
17
+ skip_path?(loc_path)
18
+ end
19
+ end
20
+
8
21
  class ThreadClient
9
22
  def self.current
10
23
  Thread.current[:DEBUGGER__ThreadClient] || begin
@@ -14,8 +27,9 @@ module DEBUGGER__
14
27
  end
15
28
 
16
29
  include Color
30
+ include SkipPathHelper
17
31
 
18
- attr_reader :location, :thread, :mode, :id
32
+ attr_reader :location, :thread, :id, :recorder
19
33
 
20
34
  def assemble_arguments(args)
21
35
  args.map do |arg|
@@ -52,14 +66,14 @@ module DEBUGGER__
52
66
  result = "#{call_identifier_str} at #{location_str}"
53
67
 
54
68
  if return_str = frame.return_str
55
- return_str = colorize(frame.return_str, [:MAGENTA, :BOLD])
56
- result += " #=> #{return_str}"
69
+ result += " #=> #{colorize_magenta(frame.return_str)}"
57
70
  end
58
71
 
59
72
  result
60
73
  end
61
74
 
62
75
  def initialize id, q_evt, q_cmd, thr = Thread.current
76
+ @is_management = false
63
77
  @id = id
64
78
  @thread = thr
65
79
  @target_frames = nil
@@ -67,11 +81,52 @@ module DEBUGGER__
67
81
  @q_cmd = q_cmd
68
82
  @step_tp = nil
69
83
  @output = []
70
- @src_lines_on_stop = (::DEBUGGER__::CONFIG[:show_src_lines] || 10).to_i
71
- @show_frames_on_stop = (::DEBUGGER__::CONFIG[:show_frames] || 2).to_i
72
84
  @frame_formatter = method(:default_frame_formatter)
73
85
  @var_map = {} # { thread_local_var_id => obj } for DAP
74
- set_mode nil
86
+ @recorder = nil
87
+ @mode = :waiting
88
+ set_mode :running
89
+ thr.instance_variable_set(:@__thread_client_id, id)
90
+
91
+ ::DEBUGGER__.info("Thread \##{@id} is created.")
92
+ end
93
+
94
+ def deactivate
95
+ @step_tp.disable if @step_tp
96
+ end
97
+
98
+ def management?
99
+ @is_management
100
+ end
101
+
102
+ def is_management
103
+ @is_management = true
104
+ end
105
+
106
+ def set_mode mode
107
+ # STDERR.puts "#{@mode} => #{mode} @ #{caller.inspect}"
108
+ #pp caller
109
+
110
+ # mode transition check
111
+ case mode
112
+ when :running
113
+ raise "#{mode} is given, but #{mode}" unless self.waiting?
114
+ when :waiting
115
+ # TODO: there is waiting -> waiting
116
+ # raise "#{mode} is given, but #{mode}" unless self.running?
117
+ else
118
+ raise
119
+ end
120
+
121
+ @mode = mode
122
+ end
123
+
124
+ def running?
125
+ @mode == :running
126
+ end
127
+
128
+ def waiting?
129
+ @mode == :waiting
75
130
  end
76
131
 
77
132
  def name
@@ -83,17 +138,33 @@ module DEBUGGER__
83
138
  end
84
139
 
85
140
  def inspect
86
- "#<DBG:TC #{self.id}:#{self.mode}@#{@thread.backtrace[-1]}>"
141
+ "#<DBG:TC #{self.id}:#{@mode}@#{@thread.backtrace[-1]}>"
142
+ end
143
+
144
+ def to_s
145
+ loc = current_frame&.location
146
+
147
+ if loc
148
+ str = "(#{@thread.name || @thread.status})@#{loc}"
149
+ else
150
+ str = "(#{@thread.name || @thread.status})@#{@thread.to_s}"
151
+ end
152
+
153
+ str += " (not under control)" unless self.waiting?
154
+ str
87
155
  end
88
156
 
89
157
  def puts str = ''
158
+ if @recorder&.replaying?
159
+ prefix = colorize_dim("[replay] ")
160
+ end
90
161
  case str
91
162
  when nil
92
163
  @output << "\n"
93
164
  when Array
94
165
  str.each{|s| puts s}
95
166
  else
96
- @output << str.chomp + "\n"
167
+ @output << "#{prefix}#{str.chomp}\n"
97
168
  end
98
169
  end
99
170
 
@@ -114,35 +185,61 @@ module DEBUGGER__
114
185
 
115
186
  ## events
116
187
 
117
- def on_trap sig
118
- if self.mode == :wait_next_action
119
- # raise Interrupt
120
- else
121
- on_suspend :trap, sig: sig
122
- end
123
- end
188
+ def wait_reply event_arg
189
+ return if management?
124
190
 
125
- def on_pause
126
- on_suspend :pause
191
+ set_mode :waiting
192
+
193
+ event!(*event_arg)
194
+ wait_next_action
127
195
  end
128
196
 
129
197
  def on_thread_begin th
130
- event! :thread_begin, th
131
- wait_next_action
198
+ wait_reply [:thread_begin, th]
132
199
  end
133
200
 
134
201
  def on_load iseq, eval_src
135
- event! :load, iseq, eval_src
136
- wait_next_action
202
+ wait_reply [:load, iseq, eval_src]
203
+ end
204
+
205
+ def on_init name
206
+ wait_reply [:init, name]
207
+ end
208
+
209
+ def on_trace trace_id, msg
210
+ wait_reply [:trace, trace_id, msg]
137
211
  end
138
212
 
139
213
  def on_breakpoint tp, bp
140
- on_suspend tp.event, tp, bp: bp
214
+ suspend tp.event, tp, bp: bp
215
+ end
216
+
217
+ def on_trap sig
218
+ if waiting?
219
+ # raise Interrupt
220
+ else
221
+ suspend :trap, sig: sig
222
+ end
141
223
  end
142
224
 
143
- def on_suspend event, tp = nil, bp: nil, sig: nil
225
+ def on_pause
226
+ suspend :pause
227
+ end
228
+
229
+ def suspend event, tp = nil, bp: nil, sig: nil, postmortem_frames: nil, replay_frames: nil
230
+ return if management?
231
+
144
232
  @current_frame_index = 0
145
- @target_frames = DEBUGGER__.capture_frames __dir__
233
+
234
+ case
235
+ when postmortem_frames
236
+ @target_frames = postmortem_frames
237
+ @postmortem = true
238
+ when replay_frames
239
+ @target_frames = replay_frames
240
+ else
241
+ @target_frames = DEBUGGER__.capture_frames(__dir__)
242
+ end
146
243
 
147
244
  cf = @target_frames.first
148
245
  if cf
@@ -160,8 +257,10 @@ module DEBUGGER__
160
257
  end
161
258
 
162
259
  if event != :pause
163
- show_src max_lines: @src_lines_on_stop
164
- show_frames @show_frames_on_stop
260
+ show_src max_lines: (CONFIG[:show_src_lines] || 10)
261
+ show_frames CONFIG[:show_frames] || 2
262
+
263
+ set_mode :waiting
165
264
 
166
265
  if bp
167
266
  event! :suspend, :breakpoint, bp.key
@@ -170,11 +269,18 @@ module DEBUGGER__
170
269
  else
171
270
  event! :suspend, event
172
271
  end
272
+ else
273
+ set_mode :waiting
173
274
  end
174
275
 
175
276
  wait_next_action
176
277
  end
177
278
 
279
+ def replay_suspend
280
+ # @recorder.current_position
281
+ suspend :replay, replay_frames: @recorder.current_frame
282
+ end
283
+
178
284
  ## control all
179
285
 
180
286
  begin
@@ -184,41 +290,77 @@ module DEBUGGER__
184
290
  SUPPORT_TARGET_THREAD = false
185
291
  end
186
292
 
187
- def step_tp
293
+ def step_tp iter
188
294
  @step_tp.disable if @step_tp
189
295
 
190
296
  thread = Thread.current
191
297
 
192
298
  if SUPPORT_TARGET_THREAD
193
299
  @step_tp = TracePoint.new(:line, :b_return, :return){|tp|
194
- next if SESSION.break? tp.path, tp.lineno
300
+ next if SESSION.break_at? tp.path, tp.lineno
195
301
  next if !yield
196
302
  next if tp.path.start_with?(__dir__)
197
- next unless File.exist?(tp.path) if ::DEBUGGER__::CONFIG[:skip_nosrc]
303
+ next if tp.path.start_with?('<internal:trace_point>')
304
+ next unless File.exist?(tp.path) if CONFIG[:skip_nosrc]
305
+ loc = caller_locations(1, 1).first
306
+ next if skip_location?(loc)
307
+ next if iter && (iter -= 1) > 0
198
308
 
199
309
  tp.disable
200
- on_suspend tp.event, tp
310
+ suspend tp.event, tp
201
311
  }
202
312
  @step_tp.enable(target_thread: thread)
203
313
  else
204
314
  @step_tp = TracePoint.new(:line, :b_return, :return){|tp|
205
315
  next if thread != Thread.current
206
- next if SESSION.break? tp.path, tp.lineno
316
+ next if SESSION.break_at? tp.path, tp.lineno
207
317
  next if !yield
208
- next unless File.exist?(tp.path) if ::DEBUGGER__::CONFIG[:skip_nosrc]
318
+ next if tp.path.start_with?(__dir__)
319
+ next if tp.path.start_with?('<internal:trace_point>')
320
+ next unless File.exist?(tp.path) if CONFIG[:skip_nosrc]
321
+ loc = caller_locations(1, 1).first
322
+ next if skip_location?(loc)
323
+ next if iter && (iter -= 1) > 0
209
324
 
210
325
  tp.disable
211
- on_suspend tp.event, tp
326
+ suspend tp.event, tp
212
327
  }
213
328
  @step_tp.enable
214
329
  end
215
330
  end
216
331
 
217
- def current_frame
218
- if @target_frames
219
- @target_frames[@current_frame_index]
220
- else
221
- nil
332
+ ## cmd helpers
333
+
334
+ # this method is extracted to hide frame_eval's local variables from C method eval's binding
335
+ def instance_eval_for_cmethod frame_self, src
336
+ frame_self.instance_eval(src)
337
+ end
338
+
339
+ def frame_eval src, re_raise: false
340
+ begin
341
+ @success_last_eval = false
342
+
343
+ b = current_frame.eval_binding
344
+ result = if b
345
+ f, _l = b.source_location
346
+ b.eval(src, "(rdbg)/#{f}")
347
+ else
348
+ frame_self = current_frame.self
349
+ instance_eval_for_cmethod(frame_self, src)
350
+ end
351
+ @success_last_eval = true
352
+ result
353
+
354
+ rescue Exception => e
355
+ return yield(e) if block_given?
356
+
357
+ puts "eval error: #{e}"
358
+
359
+ e.backtrace_locations.each do |loc|
360
+ break if loc.path == __FILE__
361
+ puts " #{loc}"
362
+ end
363
+ raise if re_raise
222
364
  end
223
365
  end
224
366
 
@@ -233,11 +375,9 @@ module DEBUGGER__
233
375
  frame_line = frame.location.lineno - 1
234
376
 
235
377
  lines = file_lines.map.with_index do |e, i|
236
- if i == frame_line
237
- "=> #{'%4d' % (i+1)}| #{e}"
238
- else
239
- " #{'%4d' % (i+1)}| #{e}"
240
- end
378
+ cur = i == frame_line ? '=>' : ' '
379
+ line = colorize_dim('%4d|' % (i+1))
380
+ "#{cur}#{line} #{e}"
241
381
  end
242
382
 
243
383
  unless start_line
@@ -275,98 +415,160 @@ module DEBUGGER__
275
415
  exit!
276
416
  end
277
417
 
278
- def show_by_editor path = nil
279
- unless path
280
- if @target_frames && frame = @target_frames[@current_frame_index]
281
- path = frame.path
282
- else
283
- return # can't get path
284
- end
285
- end
286
-
287
- if File.exist?(path)
288
- if editor = (ENV['RUBY_DEBUG_EDITOR'] || ENV['EDITOR'])
289
- puts "command: #{editor}"
290
- puts " path: #{path}"
291
- system(editor, path)
292
- else
293
- puts "can not find editor setting: ENV['RUBY_DEBUG_EDITOR'] or ENV['EDITOR']"
294
- end
418
+ def current_frame
419
+ if @target_frames
420
+ @target_frames[@current_frame_index]
295
421
  else
296
- puts "Can not find file: #{path}"
422
+ nil
297
423
  end
298
424
  end
299
425
 
300
- def show_locals
426
+ ## cmd: show
427
+
428
+ def show_locals pat
301
429
  if s = current_frame&.self
302
- puts " #{colorize_cyan("%self")} => #{colored_inspect(s)}"
430
+ puts_variable_info '%self', s, pat
303
431
  end
304
432
  if current_frame&.has_return_value
305
- puts " #{colorize_cyan("%return")} => #{colored_inspect(current_frame.return_value)}"
433
+ puts_variable_info '%return', current_frame.return_value, pat
306
434
  end
307
435
  if current_frame&.has_raised_exception
308
- puts " #{colorize_cyan("%raised")} => #{colored_inspect(current_frame.raised_exception)}"
436
+ puts_variable_info "%raised", current_frame.raised_exception, pat
309
437
  end
310
- if b = current_frame&.binding
311
- b.local_variables.each{|loc|
312
- value = b.local_variable_get(loc)
313
- puts " #{colorize_cyan(loc)} => #{colored_inspect(value)}"
438
+
439
+ if vars = current_frame&.local_variables
440
+ vars.each{|var, val|
441
+ puts_variable_info var, val, pat
314
442
  }
315
443
  end
316
444
  end
317
445
 
318
- def show_ivars
446
+ def show_ivars pat
319
447
  if s = current_frame&.self
320
- s.instance_variables.each{|iv|
448
+ s.instance_variables.sort.each{|iv|
321
449
  value = s.instance_variable_get(iv)
322
- puts " #{colorize_cyan(iv)} => #{colored_inspect(value)}"
450
+ puts_variable_info iv, value, pat
323
451
  }
324
452
  end
325
453
  end
326
454
 
327
- def frame_eval src, re_raise: false
328
- begin
329
- @success_last_eval = false
455
+ def show_consts pat, only_self: false
456
+ if s = current_frame&.self
457
+ cs = {}
458
+ if s.kind_of? Module
459
+ cs[s] = :self
460
+ else
461
+ s = s.class
462
+ cs[s] = :self unless only_self
463
+ end
330
464
 
331
- b = current_frame.binding
332
- result = if b
333
- f, _l = b.source_location
334
- b.eval(src, "(rdbg)/#{f}")
335
- else
336
- frame_self = current_frame.self
337
- frame_self.instance_eval(src)
338
- end
339
- @success_last_eval = true
340
- result
465
+ unless only_self
466
+ s.ancestors.each{|c| break if c == Object; cs[c] = :ancestors}
467
+ if b = current_frame&.binding
468
+ b.eval('Module.nesting').each{|c| cs[c] = :nesting unless cs.has_key? c}
469
+ end
470
+ end
471
+
472
+ names = {}
473
+
474
+ cs.each{|c, _|
475
+ c.constants(false).sort.each{|name|
476
+ next if names.has_key? name
477
+ names[name] = nil
478
+ value = c.const_get(name)
479
+ puts_variable_info name, value, pat
480
+ }
481
+ }
482
+ end
483
+ end
484
+
485
+ SKIP_GLOBAL_LIST = %i[$= $KCODE $-K $SAFE].freeze
486
+ def show_globals pat
487
+ global_variables.sort.each{|name|
488
+ next if SKIP_GLOBAL_LIST.include? name
489
+
490
+ value = eval(name.to_s)
491
+ puts_variable_info name, value, pat
492
+ }
493
+ end
341
494
 
495
+ def puts_variable_info label, obj, pat
496
+ return if pat && pat !~ label
497
+
498
+ begin
499
+ inspected = obj.inspect
342
500
  rescue Exception => e
343
- return yield(e) if block_given?
501
+ inspected = e.inspect
502
+ end
503
+ mono_info = "#{label} = #{inspected}"
344
504
 
345
- puts "eval error: #{e}"
505
+ w = SESSION::width
346
506
 
347
- e.backtrace_locations.each do |loc|
348
- break if loc.path == __FILE__
349
- puts " #{loc}"
350
- end
351
- raise if re_raise
507
+ if mono_info.length >= w
508
+ info = truncate(mono_info, width: w)
509
+ else
510
+ valstr = colored_inspect(obj, width: 2 ** 30)
511
+ valstr = inspected if valstr.lines.size > 1
512
+ info = "#{colorize_cyan(label)} = #{valstr}"
352
513
  end
514
+
515
+ puts info
353
516
  end
354
517
 
355
- def frame_str(i)
356
- cur_str = (@current_frame_index == i ? '=>' : ' ')
357
- prefix = "#{cur_str}##{i}"
358
- frame = @target_frames[i]
359
- frame_string = @frame_formatter.call(frame)
360
- "#{prefix}\t#{frame_string}"
518
+ def truncate(string, width:)
519
+ str = string[0 .. (width-4)] + '...'
520
+ str += ">" if str.start_with?("#<")
521
+ str
522
+ end
523
+
524
+ ### cmd: show edit
525
+
526
+ def show_by_editor path = nil
527
+ unless path
528
+ if @target_frames && frame = @target_frames[@current_frame_index]
529
+ path = frame.path
530
+ else
531
+ return # can't get path
532
+ end
533
+ end
534
+
535
+ if File.exist?(path)
536
+ if editor = (ENV['RUBY_DEBUG_EDITOR'] || ENV['EDITOR'])
537
+ puts "command: #{editor}"
538
+ puts " path: #{path}"
539
+ system(editor, path)
540
+ else
541
+ puts "can not find editor setting: ENV['RUBY_DEBUG_EDITOR'] or ENV['EDITOR']"
542
+ end
543
+ else
544
+ puts "Can not find file: #{path}"
545
+ end
361
546
  end
362
547
 
363
- def show_frames max = (@target_frames || []).size
364
- if max > 0 && @target_frames
365
- size = @target_frames.size
366
- max += 1 if size == max + 1
548
+ ### cmd: show frames
549
+
550
+ def show_frames max = nil, pattern = nil
551
+ if @target_frames && (max ||= @target_frames.size) > 0
552
+ frames = []
553
+ @target_frames.each_with_index{|f, i|
554
+ next if pattern && !(f.name.match?(pattern) || f.location_str.match?(pattern))
555
+ next if CONFIG[:skip_path] && CONFIG[:skip_path].any?{|pat|
556
+ case pat
557
+ when String
558
+ f.location_str.start_with?(pat)
559
+ when Regexp
560
+ f.location_str.match?(pat)
561
+ end
562
+ }
563
+
564
+ frames << [i, f]
565
+ }
566
+
567
+ size = frames.size
367
568
  max.times{|i|
368
- break if i >= size
369
- puts frame_str(i)
569
+ break unless frames[i]
570
+ index, frame = frames[i]
571
+ puts frame_str(index, frame: frame)
370
572
  }
371
573
  puts " # and #{size - max} frames (use `bt' command for all frames)" if max < size
372
574
  end
@@ -376,75 +578,144 @@ module DEBUGGER__
376
578
  puts frame_str(i)
377
579
  end
378
580
 
379
- def show_object_info expr
581
+ def frame_str(i, frame: @target_frames[i])
582
+ cur_str = (@current_frame_index == i ? '=>' : ' ')
583
+ prefix = "#{cur_str}##{i}"
584
+ frame_string = @frame_formatter.call(frame)
585
+ "#{prefix}\t#{frame_string}"
586
+ end
587
+
588
+ ### cmd: show outline
589
+
590
+ def show_outline expr
380
591
  begin
381
- result = frame_eval(expr, re_raise: true)
592
+ obj = frame_eval(expr, re_raise: true)
382
593
  rescue Exception
383
594
  # ignore
384
595
  else
385
- klass = ObjectSpace.internal_class_of(result)
386
- exists = []
387
- klass.ancestors.each{|k|
388
- puts "= #{k}"
389
- if (ms = (k.instance_methods(false) - exists)).size > 0
390
- puts ms.sort.join("\t")
391
- exists |= ms
392
- end
393
- }
596
+ o = Output.new(@output)
597
+
598
+ locals = current_frame&.local_variables
599
+ klass = (obj.class == Class || obj.class == Module ? obj : obj.class)
600
+
601
+ o.dump("constants", obj.constants) if obj.respond_to?(:constants)
602
+ outline_method(o, klass, obj)
603
+ o.dump("instance variables", obj.instance_variables)
604
+ o.dump("class variables", klass.class_variables)
605
+ o.dump("locals", locals.keys) if locals
394
606
  end
395
607
  end
396
608
 
397
- def add_breakpoint args
609
+ def outline_method(o, klass, obj)
610
+ singleton_class = begin obj.singleton_class; rescue TypeError; nil end
611
+ maps = class_method_map((singleton_class || klass).ancestors)
612
+ maps.each do |mod, methods|
613
+ name = mod == singleton_class ? "#{klass}.methods" : "#{mod}#methods"
614
+ o.dump(name, methods)
615
+ end
616
+ end
617
+
618
+ def class_method_map(classes)
619
+ dumped = Array.new
620
+ classes.reject { |mod| mod >= Object }.map do |mod|
621
+ methods = mod.public_instance_methods(false).select do |m|
622
+ dumped.push(m) unless dumped.include?(m)
623
+ end
624
+ [mod, methods]
625
+ end.reverse
626
+ end
627
+
628
+ ## cmd: breakpoint
629
+
630
+ def make_breakpoint args
398
631
  case args.first
399
632
  when :method
400
- klass_name, op, method_name, cond = args[1..]
401
- bp = MethodBreakpoint.new(current_frame.binding, klass_name, op, method_name, cond)
633
+ klass_name, op, method_name, cond, cmd = args[1..]
634
+ bp = MethodBreakpoint.new(current_frame.binding, klass_name, op, method_name, cond: cond, command: cmd)
402
635
  begin
403
636
  bp.enable
404
637
  rescue Exception => e
405
638
  puts e.message
406
639
  ::DEBUGGER__::METHOD_ADDED_TRACKER.enable
407
640
  end
408
- event! :result, :method_breakpoint, bp
641
+
642
+ bp
643
+ when :watch
644
+ ivar, object, result = args[1..]
645
+ WatchIVarBreakpoint.new(ivar, object, result)
409
646
  else
410
647
  raise "unknown breakpoint: #{args}"
411
648
  end
412
649
  end
413
650
 
414
- def set_mode mode
415
- @mode = mode
651
+ class SuspendReplay < Exception
416
652
  end
417
653
 
418
654
  def wait_next_action
419
- set_mode :wait_next_action
655
+ wait_next_action_
656
+ rescue SuspendReplay
657
+ replay_suspend
658
+ end
659
+
660
+ def wait_next_action_
661
+ # assertions
662
+ raise "@mode is #{@mode}" unless @mode == :waiting
420
663
 
421
- SESSION.check_forked
664
+ unless SESSION.active?
665
+ pp caller
666
+ set_mode :running
667
+ return
668
+ end
669
+ # SESSION.check_forked
422
670
 
423
- while cmds = @q_cmd.pop
424
- # pp [self, cmds: cmds]
671
+ while true
672
+ begin
673
+ set_mode :waiting if @mode != :waiting
674
+ cmds = @q_cmd.pop
675
+ # pp [self, cmds: cmds]
676
+ break unless cmds
677
+ ensure
678
+ set_mode :running
679
+ end
425
680
 
426
681
  cmd, *args = *cmds
427
682
 
428
683
  case cmd
429
684
  when :continue
430
685
  break
686
+
431
687
  when :step
432
688
  step_type = args[0]
689
+ iter = args[1]
690
+
433
691
  case step_type
434
692
  when :in
435
- step_tp{true}
693
+ if @recorder&.replaying?
694
+ @recorder.step_forward
695
+ raise SuspendReplay
696
+ else
697
+ step_tp iter do
698
+ true
699
+ end
700
+ break
701
+ end
702
+
436
703
  when :next
437
704
  frame = @target_frames.first
438
705
  path = frame.location.absolute_path || "!eval:#{frame.path}"
439
706
  line = frame.location.lineno
440
- frame.iseq.traceable_lines_norec(lines = {})
441
- next_line = lines.keys.bsearch{|e| e > line}
442
- if !next_line && (last_line = frame.iseq.last_line) > line
443
- next_line = last_line
707
+
708
+ if frame.iseq
709
+ frame.iseq.traceable_lines_norec(lines = {})
710
+ next_line = lines.keys.bsearch{|e| e > line}
711
+ if !next_line && (last_line = frame.iseq.last_line) > line
712
+ next_line = last_line
713
+ end
444
714
  end
715
+
445
716
  depth = @target_frames.first.frame_depth
446
717
 
447
- step_tp{
718
+ step_tp iter do
448
719
  loc = caller_locations(2, 1).first
449
720
  loc_path = loc.absolute_path || "!eval:#{loc.path}"
450
721
 
@@ -455,34 +726,57 @@ module DEBUGGER__
455
726
  (next_line && loc_path == path &&
456
727
  (loc_lineno = loc.lineno) > line &&
457
728
  loc_lineno <= next_line)
458
- }
729
+ end
730
+ break
731
+
459
732
  when :finish
460
733
  depth = @target_frames.first.frame_depth
461
- step_tp{
734
+ step_tp iter do
462
735
  # 3 is debugger's frame count
463
736
  DEBUGGER__.frame_depth - 3 < depth
464
- }
737
+ end
738
+ break
739
+
740
+ when :back
741
+ if @recorder&.can_step_back?
742
+ unless @recorder.backup_frames
743
+ @recorder.backup_frames = @target_frames
744
+ end
745
+ @recorder.step_back
746
+ raise SuspendReplay
747
+ else
748
+ puts "Can not step back more."
749
+ event! :result, nil
750
+ end
751
+
752
+ when :reset
753
+ if @recorder&.replaying?
754
+ @recorder.step_reset
755
+ raise SuspendReplay
756
+ end
757
+
465
758
  else
466
- raise
759
+ raise "unknown: #{type}"
467
760
  end
468
- break
761
+
469
762
  when :eval
470
763
  eval_type, eval_src = *args
471
764
 
472
- case eval_type
473
- when :display, :try_display
474
- else
475
- result = frame_eval(eval_src)
476
- end
477
765
  result_type = nil
478
766
 
479
767
  case eval_type
480
768
  when :p
481
- puts "=> " + result.inspect
769
+ result = frame_eval(eval_src)
770
+ puts "=> " + color_pp(result, 2 ** 30)
771
+ if alloc_path = ObjectSpace.allocation_sourcefile(result)
772
+ puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}"
773
+ end
482
774
  when :pp
483
- puts "=> "
484
- PP.pp(result, out = ''.dup, SESSION.width)
485
- puts out
775
+ result = frame_eval(eval_src)
776
+ puts color_pp(result, SESSION.width)
777
+ if alloc_path = ObjectSpace.allocation_sourcefile(result)
778
+ puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}"
779
+ end
486
780
  when :call
487
781
  result = frame_eval(eval_src)
488
782
  when :display, :try_display
@@ -497,23 +791,6 @@ module DEBUGGER__
497
791
 
498
792
  result_type = eval_type
499
793
  result = failed_results
500
- when :watch
501
- if @success_last_eval
502
- if eval_src.match?(/@\w+/)
503
- object =
504
- if b = current_frame.binding
505
- b.receiver
506
- else
507
- current_frame.self
508
- end
509
- puts "#{object} #{eval_src} = #{result}"
510
- result = WatchIVarBreakpoint.new(eval_src, object, result)
511
- end
512
-
513
- result_type = :watch
514
- else
515
- result = nil
516
- end
517
794
  else
518
795
  raise "unknown error option: #{args.inspect}"
519
796
  end
@@ -549,12 +826,14 @@ module DEBUGGER__
549
826
  raise "unsupported frame operation: #{arg.inspect}"
550
827
  end
551
828
  event! :result, nil
829
+
552
830
  when :show
553
831
  type = args.shift
554
832
 
555
833
  case type
556
834
  when :backtrace
557
- show_frames
835
+ max_lines, pattern = *args
836
+ show_frames max_lines, pattern
558
837
 
559
838
  when :list
560
839
  show_src(update_line: true, **(args.first || {}))
@@ -562,14 +841,30 @@ module DEBUGGER__
562
841
  when :edit
563
842
  show_by_editor(args.first)
564
843
 
565
- when :local
566
- show_frame
567
- show_locals
568
- show_ivars
844
+ when :default
845
+ pat = args.shift
846
+ show_locals pat
847
+ show_ivars pat
848
+ show_consts pat, only_self: true
849
+
850
+ when :locals
851
+ pat = args.shift
852
+ show_locals pat
853
+
854
+ when :ivars
855
+ pat = args.shift
856
+ show_ivars pat
857
+
858
+ when :consts
859
+ pat = args.shift
860
+ show_consts pat
861
+
862
+ when :globals
863
+ pat = args.shift
864
+ show_globals pat
569
865
 
570
- when :object_info
571
- expr = args.shift
572
- show_object_info expr
866
+ when :outline
867
+ show_outline args.first || 'self'
573
868
 
574
869
  else
575
870
  raise "unknown show param: " + [type, *args].inspect
@@ -578,36 +873,248 @@ module DEBUGGER__
578
873
  event! :result, nil
579
874
 
580
875
  when :breakpoint
581
- add_breakpoint args
876
+ case args[0]
877
+ when :method
878
+ bp = make_breakpoint args
879
+ event! :result, :method_breakpoint, bp
880
+ when :watch
881
+ ivar = args[1]
882
+ result = frame_eval(ivar)
883
+
884
+ if @success_last_eval
885
+ object =
886
+ if b = current_frame.binding
887
+ b.receiver
888
+ else
889
+ current_frame.self
890
+ end
891
+ bp = make_breakpoint [:watch, ivar, object, result]
892
+ event! :result, :watch_breakpoint, bp
893
+ else
894
+ event! :result, nil
895
+ end
896
+ end
897
+
898
+ when :trace
899
+ case args.shift
900
+ when :object
901
+ begin
902
+ obj = frame_eval args.shift, re_raise: true
903
+ opt = args.shift
904
+ obj_inspect = obj.inspect
905
+
906
+ width = 50
907
+
908
+ if obj_inspect.length >= width
909
+ obj_inspect = truncate(obj_inspect, width: width)
910
+ end
911
+
912
+ event! :result, :trace_pass, obj.object_id, obj_inspect, opt
913
+ rescue => e
914
+ puts e.message
915
+ event! :result, nil
916
+ end
917
+ else
918
+ raise "unreachable"
919
+ end
920
+
921
+ when :record
922
+ case args[0]
923
+ when nil
924
+ # ok
925
+ when :on
926
+ # enable recording
927
+ if !@recorder
928
+ @recorder = Recorder.new
929
+ @recorder.enable
930
+ end
931
+ when :off
932
+ if @recorder&.enabled?
933
+ @recorder.disable
934
+ end
935
+ else
936
+ raise "unknown: #{args.inspect}"
937
+ end
938
+
939
+ if @recorder&.enabled?
940
+ puts "Recorder for #{Thread.current}: on (#{@recorder.log.size} records)"
941
+ else
942
+ puts "Recorder for #{Thread.current}: off"
943
+ end
944
+ event! :result, nil
582
945
 
583
946
  when :dap
584
947
  process_dap args
585
-
586
948
  else
587
949
  raise [cmd, *args].inspect
588
950
  end
589
951
  end
590
952
 
591
- rescue SystemExit
953
+ rescue SuspendReplay, SystemExit
592
954
  raise
593
955
  rescue Exception => e
594
- pp [__FILE__, __LINE__, e, e.backtrace]
956
+ pp ["DEBUGGER Exception: #{__FILE__}:#{__LINE__}", e, e.backtrace]
595
957
  raise
596
- ensure
597
- set_mode nil
598
958
  end
599
959
 
600
- def to_s
601
- loc = current_frame&.location
960
+ class Recorder
961
+ attr_reader :log, :index
962
+ attr_accessor :backup_frames
963
+
964
+ include SkipPathHelper
965
+
966
+ def initialize
967
+ @log = []
968
+ @index = 0
969
+ @backup_frames = nil
970
+ thread = Thread.current
971
+
972
+ @tp_recorder ||= TracePoint.new(:line){|tp|
973
+ next unless Thread.current == thread
974
+ next if tp.path.start_with? __dir__
975
+ next if tp.path.start_with? '<internal:'
976
+ loc = caller_locations(1, 1).first
977
+ next if skip_location?(loc)
978
+
979
+ frames = DEBUGGER__.capture_frames(__dir__)
980
+ frames.each{|frame|
981
+ if b = frame.binding
982
+ frame.binding = nil
983
+ frame._local_variables = b.local_variables.map{|name|
984
+ [name, b.local_variable_get(name)]
985
+ }.to_h
986
+ frame._callee = b.eval('__callee__')
987
+ end
988
+ }
989
+ @log << frames
990
+ }
991
+ end
602
992
 
603
- if loc
604
- str = "(#{@thread.name || @thread.status})@#{loc}"
605
- else
606
- str = "(#{@thread.name || @thread.status})@#{@thread.to_s}"
993
+ def enable
994
+ unless @tp_recorder.enabled?
995
+ @log.clear
996
+ @tp_recorder.enable
997
+ end
607
998
  end
608
999
 
609
- str += " (not under control)" unless self.mode
610
- str
1000
+ def disable
1001
+ if @tp_recorder.enabled?
1002
+ @log.clear
1003
+ @tp_recorder.disable
1004
+ end
1005
+ end
1006
+
1007
+ def enabled?
1008
+ @tp_recorder.enabled?
1009
+ end
1010
+
1011
+ def step_back
1012
+ @index += 1
1013
+ end
1014
+
1015
+ def step_forward
1016
+ @index -= 1
1017
+ end
1018
+
1019
+ def step_reset
1020
+ @index = 0
1021
+ @backup_frames = nil
1022
+ end
1023
+
1024
+ def replaying?
1025
+ @index > 0
1026
+ end
1027
+
1028
+ def can_step_back?
1029
+ log.size > @index
1030
+ end
1031
+
1032
+ def log_index
1033
+ @log.size - @index
1034
+ end
1035
+
1036
+ def current_frame
1037
+ if @index == 0
1038
+ f = @backup_frames
1039
+ @backup_frames = nil
1040
+ f
1041
+ else
1042
+ frames = @log[log_index]
1043
+ frames
1044
+ end
1045
+ end
1046
+
1047
+ # for debugging
1048
+ def current_position
1049
+ puts "INDEX: #{@index}"
1050
+ li = log_index
1051
+ @log.each_with_index{|frame, i|
1052
+ loc = frame.first&.location
1053
+ prefix = i == li ? "=> " : ' '
1054
+ puts "#{prefix} #{loc}"
1055
+ }
1056
+ end
1057
+ end
1058
+
1059
+ # copyed from irb
1060
+ class Output
1061
+ include Color
1062
+
1063
+ MARGIN = " "
1064
+
1065
+ def initialize(output)
1066
+ @output = output
1067
+ @line_width = screen_width - MARGIN.length # right padding
1068
+ end
1069
+
1070
+ def dump(name, strs)
1071
+ strs = strs.sort
1072
+ return if strs.empty?
1073
+
1074
+ line = "#{colorize_blue(name)}: "
1075
+
1076
+ # Attempt a single line
1077
+ if fits_on_line?(strs, cols: strs.size, offset: "#{name}: ".length)
1078
+ line += strs.join(MARGIN)
1079
+ @output << line
1080
+ return
1081
+ end
1082
+
1083
+ # Multi-line
1084
+ @output << line
1085
+
1086
+ # Dump with the largest # of columns that fits on a line
1087
+ cols = strs.size
1088
+ until fits_on_line?(strs, cols: cols, offset: MARGIN.length) || cols == 1
1089
+ cols -= 1
1090
+ end
1091
+ widths = col_widths(strs, cols: cols)
1092
+ strs.each_slice(cols) do |ss|
1093
+ @output << ss.map.with_index { |s, i| "#{MARGIN}%-#{widths[i]}s" % s }.join
1094
+ end
1095
+ end
1096
+
1097
+ private
1098
+
1099
+ def fits_on_line?(strs, cols:, offset: 0)
1100
+ width = col_widths(strs, cols: cols).sum + MARGIN.length * (cols - 1)
1101
+ width <= @line_width - offset
1102
+ end
1103
+
1104
+ def col_widths(strs, cols:)
1105
+ cols.times.map do |col|
1106
+ (col...strs.size).step(cols).map do |i|
1107
+ strs[i].length
1108
+ end.max
1109
+ end
1110
+ end
1111
+
1112
+ def screen_width
1113
+ SESSION.width
1114
+ rescue Errno::EINVAL # in `winsize': Invalid argument - <STDIN>
1115
+ 80
1116
+ end
611
1117
  end
1118
+ private_constant :Output
612
1119
  end
613
1120
  end