debug 1.0.0.beta5 → 1.0.0.rc1

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