debug 1.0.0.beta8 → 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.
@@ -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
659
+
660
+ def wait_next_action_
661
+ # assertions
662
+ raise "@mode is #{@mode}" unless @mode == :waiting
468
663
 
469
- SESSION.check_forked
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