debug 1.0.0.beta6 → 1.0.0.rc2

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,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
@@ -69,11 +81,52 @@ module DEBUGGER__
69
81
  @q_cmd = q_cmd
70
82
  @step_tp = nil
71
83
  @output = []
72
- @src_lines_on_stop = (::DEBUGGER__::CONFIG[:show_src_lines] || 10).to_i
73
- @show_frames_on_stop = (::DEBUGGER__::CONFIG[:show_frames] || 2).to_i
74
84
  @frame_formatter = method(:default_frame_formatter)
75
85
  @var_map = {} # { thread_local_var_id => obj } for DAP
76
- 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
77
130
  end
78
131
 
79
132
  def name
@@ -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
215
+ end
216
+
217
+ def on_trap sig
218
+ if waiting?
219
+ # raise Interrupt
220
+ else
221
+ suspend :trap, sig: sig
222
+ end
148
223
  end
149
224
 
150
- 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
+
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: @src_lines_on_stop
171
- 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
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,98 +415,160 @@ 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 show_locals
426
+ ## cmd: show
427
+
428
+ def show_locals pat
308
429
  if s = current_frame&.self
309
- puts " #{colorize_cyan("%self")} => #{colored_inspect(s)}"
430
+ puts_variable_info '%self', s, pat
310
431
  end
311
432
  if current_frame&.has_return_value
312
- puts " #{colorize_cyan("%return")} => #{colored_inspect(current_frame.return_value)}"
433
+ puts_variable_info '%return', current_frame.return_value, pat
313
434
  end
314
435
  if current_frame&.has_raised_exception
315
- puts " #{colorize_cyan("%raised")} => #{colored_inspect(current_frame.raised_exception)}"
436
+ puts_variable_info "%raised", current_frame.raised_exception, pat
316
437
  end
317
- if b = current_frame&.binding
318
- b.local_variables.each{|loc|
319
- value = b.local_variable_get(loc)
320
- 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
321
442
  }
322
443
  end
323
444
  end
324
445
 
325
- def show_ivars
446
+ def show_ivars pat
326
447
  if s = current_frame&.self
327
- s.instance_variables.each{|iv|
448
+ s.instance_variables.sort.each{|iv|
328
449
  value = s.instance_variable_get(iv)
329
- puts " #{colorize_cyan(iv)} => #{colored_inspect(value)}"
450
+ puts_variable_info iv, value, pat
330
451
  }
331
452
  end
332
453
  end
333
454
 
334
- def frame_eval src, re_raise: false
335
- begin
336
- @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
337
464
 
338
- b = current_frame.binding
339
- result = if b
340
- f, _l = b.source_location
341
- b.eval(src, "(rdbg)/#{f}")
342
- else
343
- frame_self = current_frame.self
344
- frame_self.instance_eval(src)
345
- end
346
- @success_last_eval = true
347
- 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 = {}
348
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
494
+
495
+ def puts_variable_info label, obj, pat
496
+ return if pat && pat !~ label
497
+
498
+ begin
499
+ inspected = obj.inspect
349
500
  rescue Exception => e
350
- return yield(e) if block_given?
501
+ inspected = e.inspect
502
+ end
503
+ mono_info = "#{label} = #{inspected}"
351
504
 
352
- puts "eval error: #{e}"
505
+ w = SESSION::width
353
506
 
354
- e.backtrace_locations.each do |loc|
355
- break if loc.path == __FILE__
356
- puts " #{loc}"
357
- end
358
- 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}"
359
513
  end
514
+
515
+ puts info
360
516
  end
361
517
 
362
- def frame_str(i)
363
- cur_str = (@current_frame_index == i ? '=>' : ' ')
364
- prefix = "#{cur_str}##{i}"
365
- frame = @target_frames[i]
366
- frame_string = @frame_formatter.call(frame)
367
- "#{prefix}\t#{frame_string}"
518
+ def truncate(string, width:)
519
+ str = string[0 .. (width-4)] + '...'
520
+ str += ">" if str.start_with?("#<")
521
+ str
368
522
  end
369
523
 
370
- def show_frames max = (@target_frames || []).size
371
- if max > 0 && @target_frames
372
- size = @target_frames.size
373
- max += 1 if size == max + 1
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
546
+ end
547
+
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
374
568
  max.times{|i|
375
- break if i >= size
376
- puts frame_str(i)
569
+ break unless frames[i]
570
+ index, frame = frames[i]
571
+ puts frame_str(index, frame: frame)
377
572
  }
378
573
  puts " # and #{size - max} frames (use `bt' command for all frames)" if max < size
379
574
  end
@@ -383,29 +578,60 @@ module DEBUGGER__
383
578
  puts frame_str(i)
384
579
  end
385
580
 
386
- 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
387
591
  begin
388
- result = frame_eval(expr, re_raise: true)
592
+ obj = frame_eval(expr, re_raise: true)
389
593
  rescue Exception
390
594
  # ignore
391
595
  else
392
- klass = ObjectSpace.internal_class_of(result)
393
- exists = []
394
- klass.ancestors.each{|k|
395
- puts "= #{k}"
396
- if (ms = (k.instance_methods(false) - exists)).size > 0
397
- puts ms.sort.join("\t")
398
- exists |= ms
399
- end
400
- }
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
401
606
  end
402
607
  end
403
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
+
404
630
  def make_breakpoint args
405
631
  case args.first
406
632
  when :method
407
- klass_name, op, method_name, cond = args[1..]
408
- 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)
409
635
  begin
410
636
  bp.enable
411
637
  rescue Exception => e
@@ -422,28 +648,58 @@ module DEBUGGER__
422
648
  end
423
649
  end
424
650
 
425
- def set_mode mode
426
- @mode = mode
651
+ class SuspendReplay < Exception
427
652
  end
428
653
 
429
654
  def wait_next_action
430
- 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
431
663
 
432
- SESSION.check_forked
664
+ unless SESSION.active?
665
+ pp caller
666
+ set_mode :running
667
+ return
668
+ end
669
+ # SESSION.check_forked
433
670
 
434
- while cmds = @q_cmd.pop
435
- # 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
436
680
 
437
681
  cmd, *args = *cmds
438
682
 
439
683
  case cmd
440
684
  when :continue
441
685
  break
686
+
442
687
  when :step
443
688
  step_type = args[0]
689
+ iter = args[1]
690
+
444
691
  case step_type
445
692
  when :in
446
- 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
+
447
703
  when :next
448
704
  frame = @target_frames.first
449
705
  path = frame.location.absolute_path || "!eval:#{frame.path}"
@@ -459,7 +715,7 @@ module DEBUGGER__
459
715
 
460
716
  depth = @target_frames.first.frame_depth
461
717
 
462
- step_tp{
718
+ step_tp iter do
463
719
  loc = caller_locations(2, 1).first
464
720
  loc_path = loc.absolute_path || "!eval:#{loc.path}"
465
721
 
@@ -470,17 +726,39 @@ module DEBUGGER__
470
726
  (next_line && loc_path == path &&
471
727
  (loc_lineno = loc.lineno) > line &&
472
728
  loc_lineno <= next_line)
473
- }
729
+ end
730
+ break
731
+
474
732
  when :finish
475
733
  depth = @target_frames.first.frame_depth
476
- step_tp{
734
+ step_tp iter do
477
735
  # 3 is debugger's frame count
478
736
  DEBUGGER__.frame_depth - 3 < depth
479
- }
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
+
480
758
  else
481
- raise
759
+ raise "unknown: #{type}"
482
760
  end
483
- break
761
+
484
762
  when :eval
485
763
  eval_type, eval_src = *args
486
764
 
@@ -489,12 +767,16 @@ module DEBUGGER__
489
767
  case eval_type
490
768
  when :p
491
769
  result = frame_eval(eval_src)
492
- 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
493
774
  when :pp
494
775
  result = frame_eval(eval_src)
495
- puts "=> "
496
- PP.pp(result, out = ''.dup, SESSION.width)
497
- puts out
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
498
780
  when :call
499
781
  result = frame_eval(eval_src)
500
782
  when :display, :try_display
@@ -544,12 +826,14 @@ module DEBUGGER__
544
826
  raise "unsupported frame operation: #{arg.inspect}"
545
827
  end
546
828
  event! :result, nil
829
+
547
830
  when :show
548
831
  type = args.shift
549
832
 
550
833
  case type
551
834
  when :backtrace
552
- show_frames
835
+ max_lines, pattern = *args
836
+ show_frames max_lines, pattern
553
837
 
554
838
  when :list
555
839
  show_src(update_line: true, **(args.first || {}))
@@ -557,20 +841,37 @@ module DEBUGGER__
557
841
  when :edit
558
842
  show_by_editor(args.first)
559
843
 
560
- when :local
561
- show_frame
562
- show_locals
563
- 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
564
853
 
565
- when :object_info
566
- expr = args.shift
567
- show_object_info expr
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
865
+
866
+ when :outline
867
+ show_outline args.first || 'self'
568
868
 
569
869
  else
570
870
  raise "unknown show param: " + [type, *args].inspect
571
871
  end
572
872
 
573
873
  event! :result, nil
874
+
574
875
  when :breakpoint
575
876
  case args[0]
576
877
  when :method
@@ -593,6 +894,55 @@ module DEBUGGER__
593
894
  event! :result, nil
594
895
  end
595
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
+
596
946
  when :dap
597
947
  process_dap args
598
948
  else
@@ -600,26 +950,171 @@ module DEBUGGER__
600
950
  end
601
951
  end
602
952
 
603
- rescue SystemExit
953
+ rescue SuspendReplay, SystemExit
604
954
  raise
605
955
  rescue Exception => e
606
- pp [__FILE__, __LINE__, e, e.backtrace]
956
+ pp ["DEBUGGER Exception: #{__FILE__}:#{__LINE__}", e, e.backtrace]
607
957
  raise
608
- ensure
609
- set_mode nil
610
958
  end
611
959
 
612
- def to_s
613
- 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
614
992
 
615
- if loc
616
- str = "(#{@thread.name || @thread.status})@#{loc}"
617
- else
618
- 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
619
998
  end
620
999
 
621
- str += " (not under control)" unless self.mode
622
- 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
623
1117
  end
1118
+ private_constant :Output
624
1119
  end
625
1120
  end