debug 1.0.0.beta7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'console'
3
+ require_relative 'session'
4
4
  return unless defined?(DEBUGGER__)
5
5
  DEBUGGER__.start
@@ -7,6 +7,17 @@ require_relative 'frame_info'
7
7
  require_relative 'color'
8
8
 
9
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
+
10
21
  class ThreadClient
11
22
  def self.current
12
23
  Thread.current[:DEBUGGER__ThreadClient] || begin
@@ -16,8 +27,9 @@ module DEBUGGER__
16
27
  end
17
28
 
18
29
  include Color
30
+ include SkipPathHelper
19
31
 
20
- attr_reader :location, :thread, :mode, :id
32
+ attr_reader :location, :thread, :id, :recorder
21
33
 
22
34
  def assemble_arguments(args)
23
35
  args.map do |arg|
@@ -54,14 +66,14 @@ module DEBUGGER__
54
66
  result = "#{call_identifier_str} at #{location_str}"
55
67
 
56
68
  if return_str = frame.return_str
57
- return_str = colorize(frame.return_str, [:MAGENTA, :BOLD])
58
- result += " #=> #{return_str}"
69
+ result += " #=> #{colorize_magenta(frame.return_str)}"
59
70
  end
60
71
 
61
72
  result
62
73
  end
63
74
 
64
75
  def initialize id, q_evt, q_cmd, thr = Thread.current
76
+ @is_management = false
65
77
  @id = id
66
78
  @thread = thr
67
79
  @target_frames = nil
@@ -71,11 +83,52 @@ module DEBUGGER__
71
83
  @output = []
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)
75
90
 
76
91
  ::DEBUGGER__.info("Thread \##{@id} is created.")
77
92
  end
78
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
130
+ end
131
+
79
132
  def name
80
133
  "##{@id} #{@thread.name || @thread.backtrace.last}"
81
134
  end
@@ -85,17 +138,33 @@ module DEBUGGER__
85
138
  end
86
139
 
87
140
  def inspect
88
- "#<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
89
155
  end
90
156
 
91
157
  def puts str = ''
158
+ if @recorder&.replaying?
159
+ prefix = colorize_dim("[replay] ")
160
+ end
92
161
  case str
93
162
  when nil
94
163
  @output << "\n"
95
164
  when Array
96
165
  str.each{|s| puts s}
97
166
  else
98
- @output << str.chomp + "\n"
167
+ @output << "#{prefix}#{str.chomp}\n"
99
168
  end
100
169
  end
101
170
 
@@ -116,40 +185,61 @@ module DEBUGGER__
116
185
 
117
186
  ## events
118
187
 
119
- def on_trap sig
120
- if self.mode == :wait_next_action
121
- # raise Interrupt
122
- else
123
- on_suspend :trap, sig: sig
124
- end
125
- end
188
+ def wait_reply event_arg
189
+ return if management?
126
190
 
127
- def on_pause
128
- on_suspend :pause
191
+ set_mode :waiting
192
+
193
+ event!(*event_arg)
194
+ wait_next_action
129
195
  end
130
196
 
131
197
  def on_thread_begin th
132
- event! :thread_begin, th
133
- wait_next_action
198
+ wait_reply [:thread_begin, th]
134
199
  end
135
200
 
136
201
  def on_load iseq, eval_src
137
- event! :load, iseq, eval_src
138
- wait_next_action
202
+ wait_reply [:load, iseq, eval_src]
139
203
  end
140
204
 
141
205
  def on_init name
142
- event! :init, name
143
- wait_next_action
206
+ wait_reply [:init, name]
207
+ end
208
+
209
+ def on_trace trace_id, msg
210
+ wait_reply [:trace, trace_id, msg]
144
211
  end
145
212
 
146
213
  def on_breakpoint tp, bp
147
- on_suspend tp.event, tp, bp: bp
214
+ suspend tp.event, tp, bp: bp
148
215
  end
149
216
 
150
- def on_suspend event, tp = nil, bp: nil, sig: nil
217
+ def on_trap sig
218
+ if waiting?
219
+ # raise Interrupt
220
+ else
221
+ suspend :trap, sig: sig
222
+ end
223
+ end
224
+
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
+
151
232
  @current_frame_index = 0
152
- @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
153
243
 
154
244
  cf = @target_frames.first
155
245
  if cf
@@ -167,8 +257,10 @@ module DEBUGGER__
167
257
  end
168
258
 
169
259
  if event != :pause
170
- show_src max_lines: (::DEBUGGER__::CONFIG[:show_src_lines] || 10)
171
- show_frames ::DEBUGGER__::CONFIG[:show_frames] || 2
260
+ show_src max_lines: (CONFIG[:show_src_lines] || 10)
261
+ show_frames CONFIG[:show_frames] || 2
262
+
263
+ set_mode :waiting
172
264
 
173
265
  if bp
174
266
  event! :suspend, :breakpoint, bp.key
@@ -177,11 +269,18 @@ module DEBUGGER__
177
269
  else
178
270
  event! :suspend, event
179
271
  end
272
+ else
273
+ set_mode :waiting
180
274
  end
181
275
 
182
276
  wait_next_action
183
277
  end
184
278
 
279
+ def replay_suspend
280
+ # @recorder.current_position
281
+ suspend :replay, replay_frames: @recorder.current_frame
282
+ end
283
+
185
284
  ## control all
186
285
 
187
286
  begin
@@ -191,41 +290,77 @@ module DEBUGGER__
191
290
  SUPPORT_TARGET_THREAD = false
192
291
  end
193
292
 
194
- def step_tp
293
+ def step_tp iter
195
294
  @step_tp.disable if @step_tp
196
295
 
197
296
  thread = Thread.current
198
297
 
199
298
  if SUPPORT_TARGET_THREAD
200
299
  @step_tp = TracePoint.new(:line, :b_return, :return){|tp|
201
- next if SESSION.break? tp.path, tp.lineno
300
+ next if SESSION.break_at? tp.path, tp.lineno
202
301
  next if !yield
203
302
  next if tp.path.start_with?(__dir__)
204
- 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
205
308
 
206
309
  tp.disable
207
- on_suspend tp.event, tp
310
+ suspend tp.event, tp
208
311
  }
209
312
  @step_tp.enable(target_thread: thread)
210
313
  else
211
314
  @step_tp = TracePoint.new(:line, :b_return, :return){|tp|
212
315
  next if thread != Thread.current
213
- next if SESSION.break? tp.path, tp.lineno
316
+ next if SESSION.break_at? tp.path, tp.lineno
214
317
  next if !yield
215
- 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
216
324
 
217
325
  tp.disable
218
- on_suspend tp.event, tp
326
+ suspend tp.event, tp
219
327
  }
220
328
  @step_tp.enable
221
329
  end
222
330
  end
223
331
 
224
- def current_frame
225
- if @target_frames
226
- @target_frames[@current_frame_index]
227
- else
228
- 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
229
364
  end
230
365
  end
231
366
 
@@ -240,11 +375,9 @@ module DEBUGGER__
240
375
  frame_line = frame.location.lineno - 1
241
376
 
242
377
  lines = file_lines.map.with_index do |e, i|
243
- if i == frame_line
244
- "=> #{'%4d' % (i+1)}| #{e}"
245
- else
246
- " #{'%4d' % (i+1)}| #{e}"
247
- end
378
+ cur = i == frame_line ? '=>' : ' '
379
+ line = colorize_dim('%4d|' % (i+1))
380
+ "#{cur}#{line} #{e}"
248
381
  end
249
382
 
250
383
  unless start_line
@@ -282,113 +415,138 @@ module DEBUGGER__
282
415
  exit!
283
416
  end
284
417
 
285
- def show_by_editor path = nil
286
- unless path
287
- if @target_frames && frame = @target_frames[@current_frame_index]
288
- path = frame.path
289
- else
290
- return # can't get path
291
- end
292
- end
293
-
294
- if File.exist?(path)
295
- if editor = (ENV['RUBY_DEBUG_EDITOR'] || ENV['EDITOR'])
296
- puts "command: #{editor}"
297
- puts " path: #{path}"
298
- system(editor, path)
299
- else
300
- puts "can not find editor setting: ENV['RUBY_DEBUG_EDITOR'] or ENV['EDITOR']"
301
- end
418
+ def current_frame
419
+ if @target_frames
420
+ @target_frames[@current_frame_index]
302
421
  else
303
- puts "Can not find file: #{path}"
422
+ nil
304
423
  end
305
424
  end
306
425
 
307
- def puts_variable_info label, obj
308
- info = "#{colorize_cyan(label)} => #{colored_inspect(obj)}".lines
309
- w = SESSION.width
310
- max_inspect_lines = CONFIG[:show_inspect_lines] || 10
426
+ ## cmd: show
311
427
 
312
- if (max_inspect_lines > 0 && (info.size > max_inspect_lines)) || info.any?{|l| l.size > w}
313
- info = "#{colorize_cyan(label)} => #{colored_inspect(obj, no_color: true)}".lines
314
- if max_inspect_lines > 0 && info.size > max_inspect_lines
315
- info = info.first(max_inspect_lines - 2) +
316
- ["...(#{info.size - (max_inspect_lines - 1)} lines)\n" + info.last]
317
- end
318
- info.map!{|l|
319
- l.length > w ? l[0..(w-4)] + '...' : l
320
- }
321
- end
322
-
323
- puts info
324
- end
325
-
326
- def show_locals
428
+ def show_locals pat
327
429
  if s = current_frame&.self
328
- puts_variable_info '%self', s
430
+ puts_variable_info '%self', s, pat
329
431
  end
330
432
  if current_frame&.has_return_value
331
- puts_variable_info '%return', current_frame.return_value
433
+ puts_variable_info '%return', current_frame.return_value, pat
332
434
  end
333
435
  if current_frame&.has_raised_exception
334
- puts_variable_info "%raised", current_frame.raised_exception
436
+ puts_variable_info "%raised", current_frame.raised_exception, pat
335
437
  end
336
- if b = current_frame&.binding
337
- b.local_variables.each{|loc|
338
- value = b.local_variable_get(loc)
339
- puts_variable_info loc, value
438
+
439
+ if vars = current_frame&.local_variables
440
+ vars.each{|var, val|
441
+ puts_variable_info var, val, pat
340
442
  }
341
443
  end
342
444
  end
343
445
 
344
- def show_ivars
446
+ def show_ivars pat
345
447
  if s = current_frame&.self
346
- s.instance_variables.each{|iv|
448
+ s.instance_variables.sort.each{|iv|
347
449
  value = s.instance_variable_get(iv)
348
- puts_variable_info iv, value
450
+ puts_variable_info iv, value, pat
349
451
  }
350
452
  end
351
453
  end
352
454
 
353
- def instance_eval_for_cmethod frame_self, src
354
- frame_self.instance_eval(src)
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
464
+
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
355
483
  end
356
484
 
357
- def frame_eval src, re_raise: false
358
- begin
359
- @success_last_eval = false
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
360
489
 
361
- b = current_frame.binding
362
- result = if b
363
- f, _l = b.source_location
364
- b.eval(src, "(rdbg)/#{f}")
365
- else
366
- frame_self = current_frame.self
367
- instance_eval_for_cmethod(frame_self, src)
368
- end
369
- @success_last_eval = true
370
- result
490
+ value = eval(name.to_s)
491
+ puts_variable_info name, value, pat
492
+ }
493
+ end
494
+
495
+ def puts_variable_info label, obj, pat
496
+ return if pat && pat !~ label
371
497
 
498
+ begin
499
+ inspected = obj.inspect
372
500
  rescue Exception => e
373
- return yield(e) if block_given?
501
+ inspected = e.inspect
502
+ end
503
+ mono_info = "#{label} = #{inspected}"
374
504
 
375
- puts "eval error: #{e}"
505
+ w = SESSION::width
376
506
 
377
- e.backtrace_locations.each do |loc|
378
- break if loc.path == __FILE__
379
- puts " #{loc}"
380
- end
381
- 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}"
382
513
  end
514
+
515
+ puts info
383
516
  end
384
517
 
385
- def frame_str(i, frame: @target_frames[i])
386
- cur_str = (@current_frame_index == i ? '=>' : ' ')
387
- prefix = "#{cur_str}##{i}"
388
- frame_string = @frame_formatter.call(frame)
389
- "#{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
390
546
  end
391
547
 
548
+ ### cmd: show frames
549
+
392
550
  def show_frames max = nil, pattern = nil
393
551
  if @target_frames && (max ||= @target_frames.size) > 0
394
552
  frames = []
@@ -420,29 +578,60 @@ module DEBUGGER__
420
578
  puts frame_str(i)
421
579
  end
422
580
 
423
- 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
424
591
  begin
425
- result = frame_eval(expr, re_raise: true)
592
+ obj = frame_eval(expr, re_raise: true)
426
593
  rescue Exception
427
594
  # ignore
428
595
  else
429
- klass = ObjectSpace.internal_class_of(result)
430
- exists = []
431
- klass.ancestors.each{|k|
432
- puts "= #{k}"
433
- if (ms = (k.instance_methods(false) - exists)).size > 0
434
- puts ms.sort.join("\t")
435
- exists |= ms
436
- end
437
- }
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
438
606
  end
439
607
  end
440
608
 
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
+
441
630
  def make_breakpoint args
442
631
  case args.first
443
632
  when :method
444
633
  klass_name, op, method_name, cond, cmd = args[1..]
445
- bp = MethodBreakpoint.new(current_frame.binding, klass_name, op, method_name, cond, command: cmd)
634
+ bp = MethodBreakpoint.new(current_frame.binding, klass_name, op, method_name, cond: cond, command: cmd)
446
635
  begin
447
636
  bp.enable
448
637
  rescue Exception => e
@@ -459,28 +648,58 @@ module DEBUGGER__
459
648
  end
460
649
  end
461
650
 
462
- def set_mode mode
463
- @mode = mode
651
+ class SuspendReplay < Exception
464
652
  end
465
653
 
466
654
  def wait_next_action
467
- set_mode :wait_next_action
655
+ wait_next_action_
656
+ rescue SuspendReplay
657
+ replay_suspend
658
+ end
468
659
 
469
- SESSION.check_forked
660
+ def wait_next_action_
661
+ # assertions
662
+ raise "@mode is #{@mode}" unless @mode == :waiting
663
+
664
+ unless SESSION.active?
665
+ pp caller
666
+ set_mode :running
667
+ return
668
+ end
669
+ # SESSION.check_forked
470
670
 
471
- while cmds = @q_cmd.pop
472
- # 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
473
680
 
474
681
  cmd, *args = *cmds
475
682
 
476
683
  case cmd
477
684
  when :continue
478
685
  break
686
+
479
687
  when :step
480
688
  step_type = args[0]
689
+ iter = args[1]
690
+
481
691
  case step_type
482
692
  when :in
483
- 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
+
484
703
  when :next
485
704
  frame = @target_frames.first
486
705
  path = frame.location.absolute_path || "!eval:#{frame.path}"
@@ -496,7 +715,7 @@ module DEBUGGER__
496
715
 
497
716
  depth = @target_frames.first.frame_depth
498
717
 
499
- step_tp{
718
+ step_tp iter do
500
719
  loc = caller_locations(2, 1).first
501
720
  loc_path = loc.absolute_path || "!eval:#{loc.path}"
502
721
 
@@ -507,17 +726,39 @@ module DEBUGGER__
507
726
  (next_line && loc_path == path &&
508
727
  (loc_lineno = loc.lineno) > line &&
509
728
  loc_lineno <= next_line)
510
- }
729
+ end
730
+ break
731
+
511
732
  when :finish
512
733
  depth = @target_frames.first.frame_depth
513
- step_tp{
734
+ step_tp iter do
514
735
  # 3 is debugger's frame count
515
736
  DEBUGGER__.frame_depth - 3 < depth
516
- }
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
+
517
758
  else
518
- raise
759
+ raise "unknown: #{type}"
519
760
  end
520
- break
761
+
521
762
  when :eval
522
763
  eval_type, eval_src = *args
523
764
 
@@ -526,11 +767,16 @@ module DEBUGGER__
526
767
  case eval_type
527
768
  when :p
528
769
  result = frame_eval(eval_src)
529
- puts "=> " + result.inspect
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
530
774
  when :pp
531
775
  result = frame_eval(eval_src)
532
- puts "=> "
533
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
534
780
  when :call
535
781
  result = frame_eval(eval_src)
536
782
  when :display, :try_display
@@ -580,6 +826,7 @@ module DEBUGGER__
580
826
  raise "unsupported frame operation: #{arg.inspect}"
581
827
  end
582
828
  event! :result, nil
829
+
583
830
  when :show
584
831
  type = args.shift
585
832
 
@@ -594,20 +841,37 @@ module DEBUGGER__
594
841
  when :edit
595
842
  show_by_editor(args.first)
596
843
 
597
- when :local
598
- show_frame
599
- show_locals
600
- 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
601
857
 
602
- when :object_info
603
- expr = args.shift
604
- show_object_info expr
858
+ when :consts
859
+ pat = args.shift
860
+ show_consts pat
861
+
862
+ when :globals
863
+ pat = args.shift
864
+ show_globals pat
865
+
866
+ when :outline
867
+ show_outline args.first || 'self'
605
868
 
606
869
  else
607
870
  raise "unknown show param: " + [type, *args].inspect
608
871
  end
609
872
 
610
873
  event! :result, nil
874
+
611
875
  when :breakpoint
612
876
  case args[0]
613
877
  when :method
@@ -630,6 +894,55 @@ module DEBUGGER__
630
894
  event! :result, nil
631
895
  end
632
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
945
+
633
946
  when :dap
634
947
  process_dap args
635
948
  else
@@ -637,26 +950,171 @@ module DEBUGGER__
637
950
  end
638
951
  end
639
952
 
640
- rescue SystemExit
953
+ rescue SuspendReplay, SystemExit
641
954
  raise
642
955
  rescue Exception => e
643
956
  pp ["DEBUGGER Exception: #{__FILE__}:#{__LINE__}", e, e.backtrace]
644
957
  raise
645
- ensure
646
- set_mode nil
647
958
  end
648
959
 
649
- def to_s
650
- 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
651
992
 
652
- if loc
653
- str = "(#{@thread.name || @thread.status})@#{loc}"
654
- else
655
- 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
656
998
  end
657
999
 
658
- str += " (not under control)" unless self.mode
659
- 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
660
1117
  end
1118
+ private_constant :Output
661
1119
  end
662
1120
  end