debug 1.0.0.beta6 → 1.0.0.rc2

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
@@ -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