debug 1.4.0 → 1.9.2

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +210 -6
  3. data/Gemfile +2 -0
  4. data/LICENSE.txt +0 -0
  5. data/README.md +161 -85
  6. data/Rakefile +33 -10
  7. data/TODO.md +8 -8
  8. data/debug.gemspec +9 -7
  9. data/exe/rdbg +23 -4
  10. data/ext/debug/debug.c +111 -21
  11. data/ext/debug/extconf.rb +23 -0
  12. data/ext/debug/iseq_collector.c +2 -0
  13. data/lib/debug/abbrev_command.rb +77 -0
  14. data/lib/debug/breakpoint.rb +102 -74
  15. data/lib/debug/client.rb +46 -12
  16. data/lib/debug/color.rb +0 -0
  17. data/lib/debug/config.rb +129 -36
  18. data/lib/debug/console.rb +46 -40
  19. data/lib/debug/dap_custom/traceInspector.rb +336 -0
  20. data/lib/debug/frame_info.rb +40 -25
  21. data/lib/debug/irb_integration.rb +37 -0
  22. data/lib/debug/local.rb +17 -11
  23. data/lib/debug/open.rb +0 -0
  24. data/lib/debug/open_nonstop.rb +0 -0
  25. data/lib/debug/prelude.rb +3 -2
  26. data/lib/debug/server.rb +126 -56
  27. data/lib/debug/server_cdp.rb +673 -248
  28. data/lib/debug/server_dap.rb +497 -261
  29. data/lib/debug/session.rb +899 -441
  30. data/lib/debug/source_repository.rb +122 -49
  31. data/lib/debug/start.rb +1 -1
  32. data/lib/debug/thread_client.rb +460 -155
  33. data/lib/debug/tracer.rb +10 -16
  34. data/lib/debug/version.rb +1 -1
  35. data/lib/debug.rb +7 -2
  36. data/misc/README.md.erb +106 -56
  37. metadata +14 -24
  38. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
  39. data/.github/ISSUE_TEMPLATE/custom.md +0 -10
  40. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
  41. data/.github/pull_request_template.md +0 -9
  42. data/.github/workflows/ruby.yml +0 -34
  43. data/.gitignore +0 -12
  44. data/bin/console +0 -14
  45. data/bin/gentest +0 -30
  46. data/bin/setup +0 -8
  47. data/lib/debug/bp.vim +0 -68
@@ -5,32 +5,65 @@ require 'pp'
5
5
 
6
6
  require_relative 'color'
7
7
 
8
+ class ::Thread
9
+ attr_accessor :debug_thread_client
10
+ end
11
+
8
12
  module DEBUGGER__
13
+ M_INSTANCE_VARIABLES = method(:instance_variables).unbind
14
+ M_INSTANCE_VARIABLE_GET = method(:instance_variable_get).unbind
15
+ M_CLASS = method(:class).unbind
16
+ M_SINGLETON_CLASS = method(:singleton_class).unbind
17
+ M_KIND_OF_P = method(:kind_of?).unbind
18
+ M_RESPOND_TO_P = method(:respond_to?).unbind
19
+ M_METHOD = method(:method).unbind
20
+ M_OBJECT_ID = method(:object_id).unbind
21
+ M_NAME = method(:name).unbind
22
+
9
23
  module SkipPathHelper
10
24
  def skip_path?(path)
25
+ !path ||
26
+ DEBUGGER__.skip? ||
27
+ ThreadClient.current.management? ||
28
+ skip_internal_path?(path) ||
29
+ skip_config_skip_path?(path)
30
+ end
31
+
32
+ def skip_config_skip_path?(path)
11
33
  (skip_paths = CONFIG[:skip_path]) && skip_paths.any?{|skip_path| path.match?(skip_path)}
12
34
  end
13
35
 
36
+ def skip_internal_path?(path)
37
+ path.start_with?(__dir__) || path.delete_prefix('!eval:').start_with?('<internal:')
38
+ end
39
+
14
40
  def skip_location?(loc)
15
41
  loc_path = loc.absolute_path || "!eval:#{loc.path}"
16
42
  skip_path?(loc_path)
17
43
  end
18
44
  end
19
45
 
46
+ module GlobalVariablesHelper
47
+ SKIP_GLOBAL_LIST = %i[$= $KCODE $-K $SAFE].freeze
48
+ def safe_global_variables
49
+ global_variables.reject{|name| SKIP_GLOBAL_LIST.include? name }
50
+ end
51
+ end
52
+
20
53
  class ThreadClient
21
54
  def self.current
22
- if thc = Thread.current[:DEBUGGER__ThreadClient]
23
- thc
24
- else
25
- thc = SESSION.get_thread_client
26
- Thread.current[:DEBUGGER__ThreadClient] = thc
27
- end
55
+ Thread.current.debug_thread_client ||= SESSION.get_thread_client
28
56
  end
29
57
 
30
58
  include Color
31
59
  include SkipPathHelper
60
+ include GlobalVariablesHelper
61
+
62
+ attr_reader :thread, :id, :recorder, :check_bp_fulfillment_map
32
63
 
33
- attr_reader :location, :thread, :id, :recorder
64
+ def location
65
+ current_frame&.location
66
+ end
34
67
 
35
68
  def assemble_arguments(args)
36
69
  args.map do |arg|
@@ -42,7 +75,8 @@ module DEBUGGER__
42
75
  call_identifier_str =
43
76
  case frame.frame_type
44
77
  when :block
45
- level, block_loc, args = frame.block_identifier
78
+ level, block_loc = frame.block_identifier
79
+ args = frame.parameters_info
46
80
 
47
81
  if !args.empty?
48
82
  args_str = " {|#{assemble_arguments(args)}|}"
@@ -50,7 +84,8 @@ module DEBUGGER__
50
84
 
51
85
  "#{colorize_blue("block")}#{args_str} in #{colorize_blue(block_loc + level)}"
52
86
  when :method
53
- ci, args = frame.method_identifier
87
+ ci = frame.method_identifier
88
+ args = frame.parameters_info
54
89
 
55
90
  if !args.empty?
56
91
  args_str = "(#{assemble_arguments(args)})"
@@ -87,6 +122,9 @@ module DEBUGGER__
87
122
  @obj_map = {} # { object_id => obj } for CDP
88
123
  @recorder = nil
89
124
  @mode = :waiting
125
+ @current_frame_index = 0
126
+ # every thread should maintain its own CheckBreakpoint fulfillment state
127
+ @check_bp_fulfillment_map = {} # { check_bp => boolean }
90
128
  set_mode :running
91
129
  thr.instance_variable_set(:@__thread_client_id, id)
92
130
 
@@ -106,6 +144,7 @@ module DEBUGGER__
106
144
  end
107
145
 
108
146
  def set_mode mode
147
+ debug_mode(@mode, mode)
109
148
  # STDERR.puts "#{@mode} => #{mode} @ #{caller.inspect}"
110
149
  # pp caller
111
150
 
@@ -149,14 +188,7 @@ module DEBUGGER__
149
188
  end
150
189
 
151
190
  def to_s
152
- loc = current_frame&.location
153
-
154
- if loc
155
- str = "(#{@thread.name || @thread.status})@#{loc}"
156
- else
157
- str = "(#{@thread.name || @thread.status})@#{@thread.to_s}"
158
- end
159
-
191
+ str = "(#{@thread.name || @thread.status})@#{current_frame&.location || @thread.to_s}"
160
192
  str += " (not under control)" unless self.waiting?
161
193
  str
162
194
  end
@@ -176,6 +208,7 @@ module DEBUGGER__
176
208
  end
177
209
 
178
210
  def << req
211
+ debug_cmd(req)
179
212
  @q_cmd << req
180
213
  end
181
214
 
@@ -186,6 +219,7 @@ module DEBUGGER__
186
219
  end
187
220
 
188
221
  def event! ev, *args
222
+ debug_event(ev, args)
189
223
  @q_evt << [self, @output, ev, generate_info, *args]
190
224
  @output = []
191
225
  end
@@ -231,6 +265,7 @@ module DEBUGGER__
231
265
 
232
266
  def suspend event, tp = nil, bp: nil, sig: nil, postmortem_frames: nil, replay_frames: nil, postmortem_exc: nil
233
267
  return if management?
268
+ debug_suspend(event)
234
269
 
235
270
  @current_frame_index = 0
236
271
 
@@ -246,7 +281,6 @@ module DEBUGGER__
246
281
 
247
282
  cf = @target_frames.first
248
283
  if cf
249
- @location = cf.location
250
284
  case event
251
285
  when :return, :b_return, :c_return
252
286
  cf.has_return_value = true
@@ -265,8 +299,10 @@ module DEBUGGER__
265
299
  end
266
300
 
267
301
  if event != :pause
268
- show_src
269
- show_frames CONFIG[:show_frames] || 2
302
+ unless bp&.skip_src
303
+ show_src
304
+ show_frames CONFIG[:show_frames]
305
+ end
270
306
 
271
307
  set_mode :waiting
272
308
 
@@ -302,11 +338,15 @@ module DEBUGGER__
302
338
  @step_tp.disable if @step_tp
303
339
 
304
340
  thread = Thread.current
341
+ subsession_id = SESSION.subsession_id
305
342
 
306
343
  if SUPPORT_TARGET_THREAD
307
344
  @step_tp = TracePoint.new(*events){|tp|
308
- next if SESSION.break_at? tp.path, tp.lineno
309
- next if !yield(tp.event)
345
+ if SESSION.stop_stepping? tp.path, tp.lineno, subsession_id
346
+ tp.disable
347
+ next
348
+ end
349
+ next if !yield(tp)
310
350
  next if tp.path.start_with?(__dir__)
311
351
  next if tp.path.start_with?('<internal:trace_point>')
312
352
  next unless File.exist?(tp.path) if CONFIG[:skip_nosrc]
@@ -321,8 +361,11 @@ module DEBUGGER__
321
361
  else
322
362
  @step_tp = TracePoint.new(*events){|tp|
323
363
  next if thread != Thread.current
324
- next if SESSION.break_at? tp.path, tp.lineno
325
- next if !yield(tp.event)
364
+ if SESSION.stop_stepping? tp.path, tp.lineno, subsession_id
365
+ tp.disable
366
+ next
367
+ end
368
+ next if !yield(tp)
326
369
  next if tp.path.start_with?(__dir__)
327
370
  next if tp.path.start_with?('<internal:trace_point>')
328
371
  next unless File.exist?(tp.path) if CONFIG[:skip_nosrc]
@@ -339,9 +382,49 @@ module DEBUGGER__
339
382
 
340
383
  ## cmd helpers
341
384
 
342
- # this method is extracted to hide frame_eval's local variables from C method eval's binding
343
- def instance_eval_for_cmethod frame_self, src
344
- frame_self.instance_eval(src)
385
+ if TracePoint.respond_to? :allow_reentry
386
+ def tp_allow_reentry
387
+ TracePoint.allow_reentry do
388
+ yield
389
+ end
390
+ rescue RuntimeError => e
391
+ # on the postmortem mode, it is not stopped in TracePoint
392
+ if e.message == 'No need to allow reentrance.'
393
+ yield
394
+ else
395
+ raise
396
+ end
397
+ end
398
+ else
399
+ def tp_allow_reentry
400
+ yield
401
+ end
402
+ end
403
+
404
+ def frame_eval_core src, b, binding_location: false
405
+ saved_target_frames = @target_frames
406
+ saved_current_frame_index = @current_frame_index
407
+
408
+ if b
409
+ file, lineno = b.source_location
410
+
411
+ tp_allow_reentry do
412
+ if binding_location
413
+ b.eval(src, file, lineno)
414
+ else
415
+ b.eval(src, "(rdbg)/#{file}")
416
+ end
417
+ end
418
+ else
419
+ frame_self = current_frame.self
420
+
421
+ tp_allow_reentry do
422
+ frame_self.instance_eval(src)
423
+ end
424
+ end
425
+ ensure
426
+ @target_frames = saved_target_frames
427
+ @current_frame_index = saved_current_frame_index
345
428
  end
346
429
 
347
430
  SPECIAL_LOCAL_VARS = [
@@ -349,25 +432,22 @@ module DEBUGGER__
349
432
  [:return_value, "_return"],
350
433
  ]
351
434
 
352
- def frame_eval src, re_raise: false
435
+ def frame_eval src, re_raise: false, binding_location: false
353
436
  @success_last_eval = false
354
437
 
355
- b = current_frame.eval_binding
438
+ b = current_frame&.eval_binding || TOPLEVEL_BINDING
356
439
 
357
440
  special_local_variables current_frame do |name, var|
358
441
  b.local_variable_set(name, var) if /\%/ !~ name
359
442
  end
360
443
 
361
- result = if b
362
- f, _l = b.source_location
363
- b.eval(src, "(rdbg)/#{f}")
364
- else
365
- frame_self = current_frame.self
366
- instance_eval_for_cmethod(frame_self, src)
367
- end
444
+ result = frame_eval_core(src, b, binding_location: binding_location)
445
+
368
446
  @success_last_eval = true
369
447
  result
370
448
 
449
+ rescue SystemExit
450
+ raise
371
451
  rescue Exception => e
372
452
  return yield(e) if block_given?
373
453
 
@@ -380,50 +460,46 @@ module DEBUGGER__
380
460
  raise if re_raise
381
461
  end
382
462
 
383
- def show_src(frame_index: @current_frame_index,
384
- update_line: false,
385
- max_lines: CONFIG[:show_src_lines] || 10,
386
- start_line: nil,
387
- end_line: nil,
388
- dir: +1)
389
- if @target_frames && frame = @target_frames[frame_index]
390
- if file_lines = frame.file_lines
391
- frame_line = frame.location.lineno - 1
463
+ def get_src(frame,
464
+ max_lines:,
465
+ start_line: nil,
466
+ end_line: nil,
467
+ dir: +1)
468
+ if file_lines = frame.file_lines
469
+ frame_line = frame.location.lineno - 1
392
470
 
471
+ if CONFIG[:no_lineno]
472
+ lines = file_lines
473
+ else
393
474
  lines = file_lines.map.with_index do |e, i|
394
475
  cur = i == frame_line ? '=>' : ' '
395
476
  line = colorize_dim('%4d|' % (i+1))
396
477
  "#{cur}#{line} #{e}"
397
478
  end
479
+ end
398
480
 
399
- unless start_line
400
- if frame.show_line
401
- if dir > 0
402
- start_line = frame.show_line
403
- else
404
- end_line = frame.show_line - max_lines
405
- start_line = [end_line - max_lines, 0].max
406
- end
481
+ unless start_line
482
+ if frame.show_line
483
+ if dir > 0
484
+ start_line = frame.show_line
407
485
  else
408
- start_line = [frame_line - max_lines/2, 0].max
486
+ end_line = frame.show_line - max_lines
487
+ start_line = [end_line - max_lines, 0].max
409
488
  end
489
+ else
490
+ start_line = [frame_line - max_lines/2, 0].max
410
491
  end
492
+ end
411
493
 
412
- unless end_line
413
- end_line = [start_line + max_lines, lines.size].min
414
- end
415
-
416
- if update_line
417
- frame.show_line = end_line
418
- end
494
+ unless end_line
495
+ end_line = [start_line + max_lines, lines.size].min
496
+ end
419
497
 
420
- if start_line != end_line && max_lines
421
- puts "[#{start_line+1}, #{end_line}] in #{frame.pretty_path}" if !update_line && max_lines != 1
422
- puts lines[start_line ... end_line]
423
- end
424
- else # no file lines
425
- puts "# No sourcefile available for #{frame.path}"
498
+ if start_line != end_line && max_lines
499
+ [start_line, end_line, lines]
426
500
  end
501
+ else # no file lines
502
+ nil
427
503
  end
428
504
  rescue Exception => e
429
505
  p e
@@ -431,14 +507,63 @@ module DEBUGGER__
431
507
  exit!
432
508
  end
433
509
 
510
+ def show_src(frame_index: @current_frame_index, update_line: false, ignore_show_line: false, max_lines: CONFIG[:show_src_lines], **options)
511
+ if frame = get_frame(frame_index)
512
+ begin
513
+ if ignore_show_line
514
+ prev_show_line = frame.show_line
515
+ frame.show_line = nil
516
+ end
517
+
518
+ start_line, end_line, lines = *get_src(frame, max_lines: max_lines, **options)
519
+
520
+ if start_line
521
+ if update_line
522
+ frame.show_line = end_line
523
+ end
524
+
525
+ puts "[#{start_line+1}, #{end_line}] in #{frame.pretty_path}" if !update_line && max_lines != 1
526
+ puts lines[start_line...end_line]
527
+ else
528
+ puts "# No sourcefile available for #{frame.path}"
529
+ end
530
+ ensure
531
+ frame.show_line = prev_show_line if prev_show_line
532
+ end
533
+ end
534
+ end
535
+
434
536
  def current_frame
537
+ get_frame(@current_frame_index)
538
+ end
539
+
540
+ def get_frame(index)
435
541
  if @target_frames
436
- @target_frames[@current_frame_index]
542
+ @target_frames[index]
437
543
  else
438
544
  nil
439
545
  end
440
546
  end
441
547
 
548
+ def collect_locals(frame)
549
+ locals = []
550
+
551
+ if s = frame&.self
552
+ locals << ["%self", s]
553
+ end
554
+ special_local_variables frame do |name, val|
555
+ locals << [name, val]
556
+ end
557
+
558
+ if vars = frame&.local_variables
559
+ vars.each{|var, val|
560
+ locals << [var, val]
561
+ }
562
+ end
563
+
564
+ locals
565
+ end
566
+
442
567
  ## cmd: show
443
568
 
444
569
  def special_local_variables frame
@@ -450,62 +575,86 @@ module DEBUGGER__
450
575
  end
451
576
 
452
577
  def show_locals pat
453
- if s = current_frame&.self
454
- puts_variable_info '%self', s, pat
455
- end
456
- special_local_variables current_frame do |name, val|
457
- puts_variable_info name, val, pat
578
+ collect_locals(current_frame).each do |var, val|
579
+ puts_variable_info(var, val, pat)
458
580
  end
581
+ end
459
582
 
460
- if vars = current_frame&.local_variables
461
- vars.each{|var, val|
462
- puts_variable_info var, val, pat
463
- }
583
+ def show_ivars pat, expr = nil
584
+ if expr && !expr.empty?
585
+ _self = frame_eval(expr);
586
+ elsif _self = current_frame&.self
587
+ else
588
+ _self = nil
464
589
  end
465
- end
466
590
 
467
- def show_ivars pat
468
- if s = current_frame&.self
469
- s.instance_variables.sort.each{|iv|
470
- value = s.instance_variable_get(iv)
591
+ if _self
592
+ M_INSTANCE_VARIABLES.bind_call(_self).sort.each{|iv|
593
+ value = M_INSTANCE_VARIABLE_GET.bind_call(_self, iv)
471
594
  puts_variable_info iv, value, pat
472
595
  }
473
596
  end
474
597
  end
475
598
 
476
- def show_consts pat, only_self: false
477
- if s = current_frame&.self
599
+ def iter_consts c, names = {}
600
+ c.constants(false).sort.each{|name|
601
+ next if names.has_key? name
602
+ names[name] = nil
603
+ begin
604
+ value = c.const_get(name)
605
+ rescue Exception => e
606
+ value = e
607
+ end
608
+ yield name, value
609
+ }
610
+ end
611
+
612
+ def get_consts expr = nil, only_self: false, &block
613
+ if expr && !expr.empty?
614
+ begin
615
+ _self = frame_eval(expr, re_raise: true)
616
+ rescue Exception
617
+ # ignore
618
+ else
619
+ if M_KIND_OF_P.bind_call(_self, Module)
620
+ iter_consts _self, &block
621
+ return
622
+ else
623
+ puts "#{_self.inspect} (by #{expr}) is not a Module."
624
+ end
625
+ end
626
+ elsif _self = current_frame&.self
478
627
  cs = {}
479
- if s.kind_of? Module
480
- cs[s] = :self
628
+ if M_KIND_OF_P.bind_call(_self, Module)
629
+ cs[_self] = :self
481
630
  else
482
- s = s.class
483
- cs[s] = :self unless only_self
631
+ _self = M_CLASS.bind_call(_self)
632
+ cs[_self] = :self unless only_self
484
633
  end
485
634
 
486
635
  unless only_self
487
- s.ancestors.each{|c| break if c == Object; cs[c] = :ancestors}
636
+ _self.ancestors.each{|c| break if c == Object; cs[c] = :ancestors}
488
637
  if b = current_frame&.binding
489
- b.eval('Module.nesting').each{|c| cs[c] = :nesting unless cs.has_key? c}
638
+ b.eval('::Module.nesting').each{|c| cs[c] = :nesting unless cs.has_key? c}
490
639
  end
491
640
  end
492
641
 
493
642
  names = {}
494
643
 
495
644
  cs.each{|c, _|
496
- c.constants(false).sort.each{|name|
497
- next if names.has_key? name
498
- names[name] = nil
499
- value = c.const_get(name)
500
- puts_variable_info name, value, pat
501
- }
645
+ iter_consts c, names, &block
502
646
  }
503
647
  end
504
648
  end
505
649
 
506
- SKIP_GLOBAL_LIST = %i[$= $KCODE $-K $SAFE].freeze
650
+ def show_consts pat, expr = nil, only_self: false
651
+ get_consts expr, only_self: only_self do |name, value|
652
+ puts_variable_info name, value, pat
653
+ end
654
+ end
655
+
507
656
  def show_globals pat
508
- global_variables.sort.each{|name|
657
+ safe_global_variables.sort.each{|name|
509
658
  next if SKIP_GLOBAL_LIST.include? name
510
659
 
511
660
  value = eval(name.to_s)
@@ -517,7 +666,7 @@ module DEBUGGER__
517
666
  return if pat && pat !~ label
518
667
 
519
668
  begin
520
- inspected = obj.inspect
669
+ inspected = DEBUGGER__.safe_inspect(obj)
521
670
  rescue Exception => e
522
671
  inspected = e.inspect
523
672
  end
@@ -526,28 +675,32 @@ module DEBUGGER__
526
675
  w = SESSION::width
527
676
 
528
677
  if mono_info.length >= w
529
- info = truncate(mono_info, width: w)
678
+ maximum_value_width = w - "#{label} = ".length
679
+ valstr = truncate(inspected, width: maximum_value_width)
530
680
  else
531
681
  valstr = colored_inspect(obj, width: 2 ** 30)
532
682
  valstr = inspected if valstr.lines.size > 1
533
- info = "#{colorize_cyan(label)} = #{valstr}"
534
683
  end
535
684
 
685
+ info = "#{colorize_cyan(label)} = #{valstr}"
686
+
536
687
  puts info
537
688
  end
538
689
 
539
690
  def truncate(string, width:)
540
- str = string[0 .. (width-4)] + '...'
541
- str += ">" if str.start_with?("#<")
542
- str
691
+ if string.start_with?("#<")
692
+ string[0 .. (width-5)] + '...>'
693
+ else
694
+ string[0 .. (width-4)] + '...'
695
+ end
543
696
  end
544
697
 
545
698
  ### cmd: show edit
546
699
 
547
700
  def show_by_editor path = nil
548
701
  unless path
549
- if @target_frames && frame = @target_frames[@current_frame_index]
550
- path = frame.path
702
+ if current_frame
703
+ path = current_frame.path
551
704
  else
552
705
  return # can't get path
553
706
  end
@@ -557,7 +710,8 @@ module DEBUGGER__
557
710
  if editor = (ENV['RUBY_DEBUG_EDITOR'] || ENV['EDITOR'])
558
711
  puts "command: #{editor}"
559
712
  puts " path: #{path}"
560
- system(editor, path)
713
+ require 'shellwords'
714
+ system(*Shellwords.split(editor), path)
561
715
  else
562
716
  puts "can not find editor setting: ENV['RUBY_DEBUG_EDITOR'] or ENV['EDITOR']"
563
717
  end
@@ -572,15 +726,11 @@ module DEBUGGER__
572
726
  if @target_frames && (max ||= @target_frames.size) > 0
573
727
  frames = []
574
728
  @target_frames.each_with_index{|f, i|
575
- next if pattern && !(f.name.match?(pattern) || f.location_str.match?(pattern))
576
- next if CONFIG[:skip_path] && CONFIG[:skip_path].any?{|pat|
577
- case pat
578
- when String
579
- f.location_str.start_with?(pat)
580
- when Regexp
581
- f.location_str.match?(pat)
582
- end
583
- }
729
+ # we need to use FrameInfo#matchable_location because #location_str is for display
730
+ # and it may change based on configs (e.g. use_short_path)
731
+ next if pattern && !(f.name.match?(pattern) || f.matchable_location.match?(pattern))
732
+ # avoid using skip_path? because we still want to display internal frames
733
+ next if skip_config_skip_path?(f.matchable_location)
584
734
 
585
735
  frames << [i, f]
586
736
  }
@@ -617,18 +767,25 @@ module DEBUGGER__
617
767
  o = Output.new(@output)
618
768
 
619
769
  locals = current_frame&.local_variables
620
- klass = (obj.class == Class || obj.class == Module ? obj : obj.class)
621
770
 
622
- o.dump("constants", obj.constants) if obj.respond_to?(:constants)
771
+ klass = M_CLASS.bind_call(obj)
772
+ klass = obj if Class == klass || Module == klass
773
+
774
+ o.dump("constants", obj.constants) if M_RESPOND_TO_P.bind_call(obj, :constants)
623
775
  outline_method(o, klass, obj)
624
- o.dump("instance variables", obj.instance_variables)
776
+ o.dump("instance variables", M_INSTANCE_VARIABLES.bind_call(obj))
625
777
  o.dump("class variables", klass.class_variables)
626
778
  o.dump("locals", locals.keys) if locals
627
779
  end
628
780
  end
629
781
 
630
782
  def outline_method(o, klass, obj)
631
- singleton_class = begin obj.singleton_class; rescue TypeError; nil end
783
+ begin
784
+ singleton_class = M_SINGLETON_CLASS.bind_call(obj)
785
+ rescue TypeError
786
+ singleton_class = nil
787
+ end
788
+
632
789
  maps = class_method_map((singleton_class || klass).ancestors)
633
790
  maps.each do |mod, methods|
634
791
  name = mod == singleton_class ? "#{klass}.methods" : "#{mod}#methods"
@@ -648,16 +805,48 @@ module DEBUGGER__
648
805
 
649
806
  ## cmd: breakpoint
650
807
 
808
+ # TODO: support non-ASCII Constant name
809
+ def constant_name? name
810
+ case name
811
+ when /\A::\b/
812
+ constant_name? $~.post_match
813
+ when /\A[A-Z]\w*/
814
+ post = $~.post_match
815
+ if post.empty?
816
+ true
817
+ else
818
+ constant_name? post
819
+ end
820
+ else
821
+ false
822
+ end
823
+ end
824
+
651
825
  def make_breakpoint args
652
826
  case args.first
653
827
  when :method
654
828
  klass_name, op, method_name, cond, cmd, path = args[1..]
655
- bp = MethodBreakpoint.new(current_frame.eval_binding, klass_name, op, method_name, cond: cond, command: cmd, path: path)
829
+ bp = MethodBreakpoint.new(current_frame&.eval_binding || TOPLEVEL_BINDING, klass_name, op, method_name, cond: cond, command: cmd, path: path)
656
830
  begin
657
831
  bp.enable
832
+ rescue NameError => e
833
+ if bp.klass
834
+ puts "Unknown method name: \"#{e.name}\""
835
+ else
836
+ # klass_name can not be evaluated
837
+ if constant_name? klass_name
838
+ puts "Unknown constant name: \"#{e.name}\""
839
+ else
840
+ # only Class name is allowed
841
+ puts "Not a constant name: \"#{klass_name}\""
842
+ bp = nil
843
+ end
844
+ end
845
+
846
+ Session.activate_method_added_trackers if bp
658
847
  rescue Exception => e
659
- puts e.message
660
- ::DEBUGGER__::METHOD_ADDED_TRACKER.enable
848
+ puts e.inspect
849
+ bp = nil
661
850
  end
662
851
 
663
852
  bp
@@ -672,8 +861,18 @@ module DEBUGGER__
672
861
  class SuspendReplay < Exception
673
862
  end
674
863
 
864
+ if ::Fiber.respond_to?(:blocking)
865
+ private def fiber_blocking
866
+ ::Fiber.blocking{yield}
867
+ end
868
+ else
869
+ private def fiber_blocking
870
+ yield
871
+ end
872
+ end
873
+
675
874
  def wait_next_action
676
- wait_next_action_
875
+ fiber_blocking{wait_next_action_}
677
876
  rescue SuspendReplay
678
877
  replay_suspend
679
878
  end
@@ -693,6 +892,7 @@ module DEBUGGER__
693
892
  set_mode :waiting if !waiting?
694
893
  cmds = @q_cmd.pop
695
894
  # pp [self, cmds: cmds]
895
+
696
896
  break unless cmds
697
897
  ensure
698
898
  set_mode :running
@@ -710,8 +910,9 @@ module DEBUGGER__
710
910
 
711
911
  case step_type
712
912
  when :in
913
+ iter = iter || 1
713
914
  if @recorder&.replaying?
714
- @recorder.step_forward
915
+ @recorder.step_forward iter
715
916
  raise SuspendReplay
716
917
  else
717
918
  step_tp iter do
@@ -724,6 +925,7 @@ module DEBUGGER__
724
925
  frame = @target_frames.first
725
926
  path = frame.location.absolute_path || "!eval:#{frame.path}"
726
927
  line = frame.location.lineno
928
+ label = frame.location.base_label
727
929
 
728
930
  if frame.iseq
729
931
  frame.iseq.traceable_lines_norec(lines = {})
@@ -735,35 +937,89 @@ module DEBUGGER__
735
937
 
736
938
  depth = @target_frames.first.frame_depth
737
939
 
738
- step_tp iter do
940
+ step_tp iter do |tp|
739
941
  loc = caller_locations(2, 1).first
740
942
  loc_path = loc.absolute_path || "!eval:#{loc.path}"
943
+ loc_label = loc.base_label
944
+ loc_depth = DEBUGGER__.frame_depth - 3
741
945
 
742
- # same stack depth
743
- (DEBUGGER__.frame_depth - 3 <= depth) ||
744
-
745
- # different frame
746
- (next_line && loc_path == path &&
747
- (loc_lineno = loc.lineno) > line &&
748
- loc_lineno <= next_line)
946
+ case
947
+ when loc_depth == depth && loc_label == label
948
+ true
949
+ when loc_depth < depth
950
+ # lower stack depth
951
+ true
952
+ when (next_line &&
953
+ loc_path == path &&
954
+ (loc_lineno = loc.lineno) > line &&
955
+ loc_lineno <= next_line)
956
+ # different frame (maybe block) but the line is before next_line
957
+ true
958
+ end
749
959
  end
750
960
  break
751
961
 
752
962
  when :finish
753
963
  finish_frames = (iter || 1) - 1
754
- goal_depth = @target_frames.first.frame_depth - finish_frames
964
+ frame = @target_frames.first
965
+ goal_depth = frame.frame_depth - finish_frames - (frame.has_return_value ? 1 : 0)
755
966
 
756
967
  step_tp nil, [:return, :b_return] do
757
968
  DEBUGGER__.frame_depth - 3 <= goal_depth ? true : false
758
969
  end
759
970
  break
760
971
 
972
+ when :until
973
+ location = iter&.strip
974
+ frame = @target_frames.first
975
+ depth = frame.frame_depth - (frame.has_return_value ? 1 : 0)
976
+ target_location_label = frame.location.base_label
977
+
978
+ case location
979
+ when nil, /\A(?:(.+):)?(\d+)\z/
980
+ no_loc = !location
981
+ file = $1 || frame.location.path
982
+ line = ($2 || frame.location.lineno + 1).to_i
983
+
984
+ step_tp nil, [:line, :return] do |tp|
985
+ if tp.event == :line
986
+ next false if no_loc && depth < DEBUGGER__.frame_depth - 3
987
+ next false unless tp.path.end_with?(file)
988
+ next false unless tp.lineno >= line
989
+ true
990
+ else
991
+ true if depth >= DEBUGGER__.frame_depth - 3 &&
992
+ caller_locations(2, 1).first.base_label == target_location_label
993
+ # TODO: imcomplete condition
994
+ end
995
+ end
996
+ else
997
+ pat = location
998
+ if /\A\/(.+)\/\z/ =~ pat
999
+ pat = Regexp.new($1)
1000
+ end
1001
+
1002
+ step_tp nil, [:call, :c_call, :return] do |tp|
1003
+ case tp.event
1004
+ when :call, :c_call
1005
+ true if pat === tp.callee_id.to_s
1006
+ else # :return, :b_return
1007
+ true if depth >= DEBUGGER__.frame_depth - 3 &&
1008
+ caller_locations(2, 1).first.base_label == target_location_label
1009
+ # TODO: imcomplete condition
1010
+ end
1011
+ end
1012
+ end
1013
+
1014
+ break
1015
+
761
1016
  when :back
1017
+ iter = iter || 1
762
1018
  if @recorder&.can_step_back?
763
1019
  unless @recorder.backup_frames
764
1020
  @recorder.backup_frames = @target_frames
765
1021
  end
766
- @recorder.step_back
1022
+ @recorder.step_back iter
767
1023
  raise SuspendReplay
768
1024
  else
769
1025
  puts "Can not step back more."
@@ -800,13 +1056,6 @@ module DEBUGGER__
800
1056
  end
801
1057
  when :call
802
1058
  result = frame_eval(eval_src)
803
- when :irb
804
- begin
805
- result = frame_eval('binding.irb')
806
- ensure
807
- # workaround: https://github.com/ruby/debug/issues/308
808
- Reline.prompt_proc = nil if defined? Reline
809
- end
810
1059
  when :display, :try_display
811
1060
  failed_results = []
812
1061
  eval_src.each_with_index{|src, i|
@@ -853,6 +1102,7 @@ module DEBUGGER__
853
1102
  else
854
1103
  raise "unsupported frame operation: #{arg.inspect}"
855
1104
  end
1105
+
856
1106
  event! :result, nil
857
1107
 
858
1108
  when :show
@@ -866,6 +1116,10 @@ module DEBUGGER__
866
1116
  when :list
867
1117
  show_src(update_line: true, **(args.first || {}))
868
1118
 
1119
+ when :whereami
1120
+ show_src ignore_show_line: true
1121
+ show_frames CONFIG[:show_frames]
1122
+
869
1123
  when :edit
870
1124
  show_by_editor(args.first)
871
1125
 
@@ -881,11 +1135,13 @@ module DEBUGGER__
881
1135
 
882
1136
  when :ivars
883
1137
  pat = args.shift
884
- show_ivars pat
1138
+ expr = args.shift
1139
+ show_ivars pat, expr
885
1140
 
886
1141
  when :consts
887
1142
  pat = args.shift
888
- show_consts pat
1143
+ expr = args.shift
1144
+ show_consts pat, expr
889
1145
 
890
1146
  when :globals
891
1147
  pat = args.shift
@@ -929,7 +1185,7 @@ module DEBUGGER__
929
1185
  begin
930
1186
  obj = frame_eval args.shift, re_raise: true
931
1187
  opt = args.shift
932
- obj_inspect = obj.inspect
1188
+ obj_inspect = DEBUGGER__.safe_inspect(obj)
933
1189
 
934
1190
  width = 50
935
1191
 
@@ -937,7 +1193,7 @@ module DEBUGGER__
937
1193
  obj_inspect = truncate(obj_inspect, width: width)
938
1194
  end
939
1195
 
940
- event! :result, :trace_pass, obj.object_id, obj_inspect, opt
1196
+ event! :result, :trace_pass, M_OBJECT_ID.bind_call(obj), obj_inspect, opt
941
1197
  rescue => e
942
1198
  puts e.message
943
1199
  event! :result, nil
@@ -954,8 +1210,8 @@ module DEBUGGER__
954
1210
  # enable recording
955
1211
  if !@recorder
956
1212
  @recorder = Recorder.new
957
- @recorder.enable
958
1213
  end
1214
+ @recorder.enable
959
1215
  when :off
960
1216
  if @recorder&.enabled?
961
1217
  @recorder.disable
@@ -971,6 +1227,8 @@ module DEBUGGER__
971
1227
  end
972
1228
  event! :result, nil
973
1229
 
1230
+ when :quit
1231
+ sleep # wait for SystemExit
974
1232
  when :dap
975
1233
  process_dap args
976
1234
  when :cdp
@@ -983,8 +1241,45 @@ module DEBUGGER__
983
1241
  rescue SuspendReplay, SystemExit, Interrupt
984
1242
  raise
985
1243
  rescue Exception => e
986
- pp ["DEBUGGER Exception: #{__FILE__}:#{__LINE__}", e, e.backtrace]
1244
+ STDERR.puts e.cause.inspect
1245
+ STDERR.puts e.inspect
1246
+ Thread.list.each{|th|
1247
+ STDERR.puts "@@@ #{th}"
1248
+ th.backtrace.each{|b|
1249
+ STDERR.puts " > #{b}"
1250
+ }
1251
+ }
1252
+ p ["DEBUGGER Exception: #{__FILE__}:#{__LINE__}", e, e.backtrace]
987
1253
  raise
1254
+ ensure
1255
+ @returning = false
1256
+ end
1257
+
1258
+ def debug_event(ev, args)
1259
+ DEBUGGER__.debug{
1260
+ args = args.map { |arg| DEBUGGER__.safe_inspect(arg) }
1261
+ "#{inspect} sends Event { type: #{ev.inspect}, args: #{args} } to Session"
1262
+ }
1263
+ end
1264
+
1265
+ def debug_mode(old_mode, new_mode)
1266
+ DEBUGGER__.debug{
1267
+ "#{inspect} changes mode (#{old_mode} -> #{new_mode})"
1268
+ }
1269
+ end
1270
+
1271
+ def debug_cmd(cmds)
1272
+ DEBUGGER__.debug{
1273
+ cmd, *args = *cmds
1274
+ args = args.map { |arg| DEBUGGER__.safe_inspect(arg) }
1275
+ "#{inspect} receives Cmd { type: #{cmd.inspect}, args: #{args} } from Session"
1276
+ }
1277
+ end
1278
+
1279
+ def debug_suspend(event)
1280
+ DEBUGGER__.debug{
1281
+ "#{inspect} is suspended for #{event.inspect}"
1282
+ }
988
1283
  end
989
1284
 
990
1285
  class Recorder
@@ -1001,8 +1296,8 @@ module DEBUGGER__
1001
1296
 
1002
1297
  @tp_recorder ||= TracePoint.new(:line){|tp|
1003
1298
  next unless Thread.current == thread
1004
- next if tp.path.start_with? __dir__
1005
- next if tp.path.start_with? '<internal:'
1299
+ # can't be replaced by skip_location
1300
+ next if skip_internal_path?(tp.path)
1006
1301
  loc = caller_locations(1, 1).first
1007
1302
  next if skip_location?(loc)
1008
1303
 
@@ -1016,10 +1311,14 @@ module DEBUGGER__
1016
1311
  frame._callee = b.eval('__callee__')
1017
1312
  end
1018
1313
  }
1019
- @log << frames
1314
+ append(frames)
1020
1315
  }
1021
1316
  end
1022
1317
 
1318
+ def append frames
1319
+ @log << frames
1320
+ end
1321
+
1023
1322
  def enable
1024
1323
  unless @tp_recorder.enabled?
1025
1324
  @log.clear
@@ -1038,12 +1337,18 @@ module DEBUGGER__
1038
1337
  @tp_recorder.enabled?
1039
1338
  end
1040
1339
 
1041
- def step_back
1042
- @index += 1
1340
+ def step_back iter
1341
+ @index += iter
1342
+ if @index > @log.size
1343
+ @index = @log.size
1344
+ end
1043
1345
  end
1044
1346
 
1045
- def step_forward
1046
- @index -= 1
1347
+ def step_forward iter
1348
+ @index -= iter
1349
+ if @index < 0
1350
+ @index = 0
1351
+ end
1047
1352
  end
1048
1353
 
1049
1354
  def step_reset