debug 1.6.3 → 1.7.0

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.
data/lib/debug/session.rb CHANGED
@@ -85,11 +85,13 @@ class RubyVM::InstructionSequence
85
85
  end
86
86
 
87
87
  module DEBUGGER__
88
- PresetCommand = Struct.new(:commands, :source, :auto_continue)
88
+ PresetCommands = Struct.new(:commands, :source, :auto_continue)
89
+ SessionCommand = Struct.new(:block, :repeat, :unsafe, :cancel_auto_continue, :postmortem)
90
+
89
91
  class PostmortemError < RuntimeError; end
90
92
 
91
93
  class Session
92
- attr_reader :intercepted_sigint_cmd, :process_group
94
+ attr_reader :intercepted_sigint_cmd, :process_group, :subsession_id
93
95
 
94
96
  include Color
95
97
 
@@ -116,6 +118,7 @@ module DEBUGGER__
116
118
  @intercepted_sigint_cmd = 'DEFAULT'
117
119
  @process_group = ProcessGroup.new
118
120
  @subsession_stack = []
121
+ @subsession_id = 0
119
122
 
120
123
  @frame_map = {} # for DAP: {id => [threadId, frame_depth]} and CDP: {id => frame_depth}
121
124
  @var_map = {1 => [:globals], } # {id => ...} for DAP
@@ -125,21 +128,41 @@ module DEBUGGER__
125
128
  @obj_map = {} # { object_id => ... } for CDP
126
129
 
127
130
  @tp_thread_begin = nil
131
+ @commands = {}
132
+ @unsafe_context = false
133
+
134
+ has_keep_script_lines = RubyVM.respond_to? :keep_script_lines
135
+
128
136
  @tp_load_script = TracePoint.new(:script_compiled){|tp|
129
- ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
137
+ if !has_keep_script_lines || bps_pending_until_load?
138
+ eval_script = tp.eval_script unless has_keep_script_lines
139
+ ThreadClient.current.on_load tp.instruction_sequence, eval_script
140
+ end
130
141
  }
131
142
  @tp_load_script.enable
132
143
 
133
144
  @thread_stopper = thread_stopper
134
145
  self.postmortem = CONFIG[:postmortem]
146
+
147
+ register_default_command
135
148
  end
136
149
 
137
150
  def active?
138
151
  !@q_evt.closed?
139
152
  end
140
153
 
141
- def break_at? file, line
142
- @bps.has_key? [file, line]
154
+ def remote?
155
+ @ui.remote?
156
+ end
157
+
158
+ def stop_stepping? file, line, subsession_id = nil
159
+ if @bps.has_key? [file, line]
160
+ true
161
+ elsif subsession_id && @subsession_id != subsession_id
162
+ true
163
+ else
164
+ false
165
+ end
143
166
  end
144
167
 
145
168
  def activate ui = nil, on_fork: false
@@ -193,6 +216,14 @@ module DEBUGGER__
193
216
  def reset_ui ui
194
217
  @ui.deactivate
195
218
  @ui = ui
219
+
220
+ # activate new ui
221
+ @tp_thread_begin.disable
222
+ @ui.activate self
223
+ if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
224
+ thc.mark_as_management
225
+ end
226
+ @tp_thread_begin.enable
196
227
  end
197
228
 
198
229
  def pop_event
@@ -248,7 +279,7 @@ module DEBUGGER__
248
279
 
249
280
  when :suspend
250
281
  enter_subsession if ev_args.first != :replay
251
- output.each{|str| @ui.puts str}
282
+ output.each{|str| @ui.puts str} unless @ui.ignore_output_on_suspend?
252
283
 
253
284
  case ev_args.first
254
285
  when :breakpoint
@@ -323,7 +354,7 @@ module DEBUGGER__
323
354
  if @preset_command && !@preset_command.commands.empty?
324
355
  @preset_command.commands += cs
325
356
  else
326
- @preset_command = PresetCommand.new(cs, name, continue)
357
+ @preset_command = PresetCommands.new(cs, name, continue)
327
358
  end
328
359
 
329
360
  ThreadClient.current.on_init name if kick
@@ -396,97 +427,121 @@ module DEBUGGER__
396
427
  end
397
428
  end
398
429
 
399
- def process_command line
400
- if line.empty?
401
- if @repl_prev_line
402
- line = @repl_prev_line
403
- else
404
- return :retry
405
- end
406
- else
407
- @repl_prev_line = line
408
- end
409
-
410
- /([^\s]+)(?:\s+(.+))?/ =~ line
411
- cmd, arg = $1, $2
430
+ private def register_command *names,
431
+ repeat: false, unsafe: true, cancel_auto_continue: false, postmortem: true,
432
+ &b
433
+ cmd = SessionCommand.new(b, repeat, unsafe, cancel_auto_continue, postmortem)
412
434
 
413
- # p cmd: [cmd, *arg]
435
+ names.each{|name|
436
+ @commands[name] = cmd
437
+ }
438
+ end
414
439
 
415
- case cmd
440
+ def register_default_command
416
441
  ### Control flow
417
442
 
418
443
  # * `s[tep]`
419
444
  # * Step in. Resume the program until next breakable point.
420
445
  # * `s[tep] <n>`
421
446
  # * Step in, resume the program at `<n>`th breakable point.
422
- when 's', 'step'
423
- cancel_auto_continue
424
- check_postmortem
447
+ register_command 's', 'step',
448
+ repeat: true,
449
+ cancel_auto_continue: true,
450
+ postmortem: false do |arg|
425
451
  step_command :in, arg
452
+ end
426
453
 
427
454
  # * `n[ext]`
428
455
  # * Step over. Resume the program until next line.
429
456
  # * `n[ext] <n>`
430
457
  # * Step over, same as `step <n>`.
431
- when 'n', 'next'
432
- cancel_auto_continue
433
- check_postmortem
458
+ register_command 'n', 'next',
459
+ repeat: true,
460
+ cancel_auto_continue: true,
461
+ postmortem: false do |arg|
434
462
  step_command :next, arg
463
+ end
435
464
 
436
465
  # * `fin[ish]`
437
466
  # * Finish this frame. Resume the program until the current frame is finished.
438
467
  # * `fin[ish] <n>`
439
468
  # * Finish `<n>`th frames.
440
- when 'fin', 'finish'
441
- cancel_auto_continue
442
- check_postmortem
443
-
469
+ register_command 'fin', 'finish',
470
+ repeat: true,
471
+ cancel_auto_continue: true,
472
+ postmortem: false do |arg|
444
473
  if arg&.to_i == 0
445
474
  raise 'finish command with 0 does not make sense.'
446
475
  end
447
476
 
448
477
  step_command :finish, arg
478
+ end
449
479
 
450
- # * `c[ontinue]`
480
+ # * `u[ntil]`
481
+ # * Similar to `next` command, but only stop later lines or the end of the current frame.
482
+ # * Similar to gdb's `advance` command.
483
+ # * `u[ntil] <[file:]line>
484
+ # * Run til the program reaches given location or the end of the current frame.
485
+ # * `u[ntil] <name>
486
+ # * Run til the program invokes a method `<name>`. `<name>` can be a regexp with `/name/`.
487
+ register_command 'u', 'until',
488
+ repeat: true,
489
+ cancel_auto_continue: true,
490
+ postmortem: false do |arg|
491
+
492
+ step_command :until, arg
493
+ end
494
+
495
+ # * `c` or `cont` or `continue`
451
496
  # * Resume the program.
452
- when 'c', 'continue'
453
- cancel_auto_continue
497
+ register_command 'c', 'cont', 'continue',
498
+ repeat: true,
499
+ cancel_auto_continue: true do |arg|
454
500
  leave_subsession :continue
501
+ end
455
502
 
456
503
  # * `q[uit]` or `Ctrl-D`
457
504
  # * Finish debugger (with the debuggee process on non-remote debugging).
458
- when 'q', 'quit'
505
+ register_command 'q', 'quit' do |arg|
459
506
  if ask 'Really quit?'
460
- @ui.quit arg.to_i
507
+ @ui.quit arg.to_i do
508
+ request_tc :quit
509
+ end
461
510
  leave_subsession :continue
462
511
  else
463
- return :retry
512
+ next :retry
464
513
  end
514
+ end
465
515
 
466
516
  # * `q[uit]!`
467
517
  # * Same as q[uit] but without the confirmation prompt.
468
- when 'q!', 'quit!'
469
- @ui.quit arg.to_i
470
- leave_subsession nil
518
+ register_command 'q!', 'quit!', unsafe: false do |arg|
519
+ @ui.quit arg.to_i do
520
+ request_tc :quit
521
+ end
522
+ leave_subsession :continue
523
+ end
471
524
 
472
525
  # * `kill`
473
526
  # * Stop the debuggee process with `Kernel#exit!`.
474
- when 'kill'
527
+ register_command 'kill' do |arg|
475
528
  if ask 'Really kill?'
476
529
  exit! (arg || 1).to_i
477
530
  else
478
- return :retry
531
+ next :retry
479
532
  end
533
+ end
480
534
 
481
535
  # * `kill!`
482
536
  # * Same as kill but without the confirmation prompt.
483
- when 'kill!'
537
+ register_command 'kill!', unsafe: false do |arg|
484
538
  exit! (arg || 1).to_i
539
+ end
485
540
 
486
541
  # * `sigint`
487
542
  # * Execute SIGINT handler registered by the debuggee.
488
543
  # * Note that this command should be used just after stop by `SIGINT`.
489
- when 'sigint'
544
+ register_command 'sigint' do
490
545
  begin
491
546
  case cmd = @intercepted_sigint_cmd
492
547
  when nil, 'IGNORE', :IGNORE, 'DEFAULT', :DEFAULT
@@ -502,8 +557,9 @@ module DEBUGGER__
502
557
  rescue Exception => e
503
558
  @ui.puts "Exception: #{e}"
504
559
  @ui.puts e.backtrace.map{|line| " #{e}"}
505
- return :retry
560
+ next :retry
506
561
  end
562
+ end
507
563
 
508
564
  ### Breakpoint
509
565
 
@@ -528,22 +584,21 @@ module DEBUGGER__
528
584
  # * `b[reak] if: <expr>`
529
585
  # * break if: `<expr>` is true at any lines.
530
586
  # * Note that this feature is super slow.
531
- when 'b', 'break'
532
- check_postmortem
533
-
587
+ register_command 'b', 'break', postmortem: false, unsafe: false do |arg|
534
588
  if arg == nil
535
589
  show_bps
536
- return :retry
590
+ next :retry
537
591
  else
538
592
  case bp = repl_add_breakpoint(arg)
539
593
  when :noretry
540
594
  when nil
541
- return :retry
595
+ next :retry
542
596
  else
543
597
  show_bps bp
544
- return :retry
598
+ next :retry
545
599
  end
546
600
  end
601
+ end
547
602
 
548
603
  # * `catch <Error>`
549
604
  # * Set breakpoint on raising `<Error>`.
@@ -555,16 +610,16 @@ module DEBUGGER__
555
610
  # * stops and run `<command>`, and continue.
556
611
  # * `catch ... path: <path>`
557
612
  # * stops if the exception is raised from a `<path>`. `<path>` can be a regexp with `/regexp/`.
558
- when 'catch'
559
- check_postmortem
560
-
613
+ register_command 'catch', postmortem: false, unsafe: false do |arg|
561
614
  if arg
562
615
  bp = repl_add_catch_breakpoint arg
563
616
  show_bps bp if bp
564
617
  else
565
618
  show_bps
566
619
  end
567
- return :retry
620
+
621
+ :retry
622
+ end
568
623
 
569
624
  # * `watch @ivar`
570
625
  # * Stop the execution when the result of current scope's `@ivar` is changed.
@@ -577,24 +632,20 @@ module DEBUGGER__
577
632
  # * stops and run `<command>`, and continue.
578
633
  # * `watch ... path: <path>`
579
634
  # * stops if the path matches `<path>`. `<path>` can be a regexp with `/regexp/`.
580
- when 'wat', 'watch'
581
- check_postmortem
582
-
635
+ register_command 'wat', 'watch', postmortem: false, unsafe: false do |arg|
583
636
  if arg && arg.match?(/\A@\w+/)
584
637
  repl_add_watch_breakpoint(arg)
585
638
  else
586
639
  show_bps
587
- return :retry
640
+ :retry
588
641
  end
642
+ end
589
643
 
590
644
  # * `del[ete]`
591
645
  # * delete all breakpoints.
592
646
  # * `del[ete] <bpnum>`
593
647
  # * delete specified breakpoint.
594
- when 'del', 'delete'
595
- check_postmortem
596
-
597
- bp =
648
+ register_command 'del', 'delete', postmortem: false, unsafe: false do |arg|
598
649
  case arg
599
650
  when nil
600
651
  show_bps
@@ -602,12 +653,13 @@ module DEBUGGER__
602
653
  delete_bp
603
654
  end
604
655
  when /\d+/
605
- delete_bp arg.to_i
656
+ bp = delete_bp arg.to_i
606
657
  else
607
658
  nil
608
659
  end
609
660
  @ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp
610
- return :retry
661
+ :retry
662
+ end
611
663
 
612
664
  ### Information
613
665
 
@@ -619,7 +671,7 @@ module DEBUGGER__
619
671
  # * Only shows frames with method name or location info that matches `/regexp/`.
620
672
  # * `bt <num> /regexp/` or `backtrace <num> /regexp/`
621
673
  # * Only shows first `<num>` frames with method name or location info that matches `/regexp/`.
622
- when 'bt', 'backtrace'
674
+ register_command 'bt', 'backtrace', unsafe: false do |arg|
623
675
  case arg
624
676
  when /\A(\d+)\z/
625
677
  request_tc [:show, :backtrace, arg.to_i, nil]
@@ -632,6 +684,7 @@ module DEBUGGER__
632
684
  else
633
685
  request_tc [:show, :backtrace, nil, nil]
634
686
  end
687
+ end
635
688
 
636
689
  # * `l[ist]`
637
690
  # * Show current frame's source code.
@@ -640,7 +693,7 @@ module DEBUGGER__
640
693
  # * Show predecessor lines as opposed to the `list` command.
641
694
  # * `l[ist] <start>` or `l[ist] <start>-<end>`
642
695
  # * Show current frame's source code from the line <start> to <end> if given.
643
- when 'l', 'list'
696
+ register_command 'l', 'list', repeat: true, unsafe: false do |arg|
644
697
  case arg ? arg.strip : nil
645
698
  when /\A(\d+)\z/
646
699
  request_tc [:show, :list, {start_line: arg.to_i - 1}]
@@ -652,45 +705,63 @@ module DEBUGGER__
652
705
  request_tc [:show, :list]
653
706
  else
654
707
  @ui.puts "Can not handle list argument: #{arg}"
655
- return :retry
708
+ :retry
656
709
  end
710
+ end
711
+
712
+ # * `whereami`
713
+ # * Show the current frame with source code.
714
+ register_command 'whereami', unsafe: false do
715
+ request_tc [:show, :whereami]
716
+ end
657
717
 
658
718
  # * `edit`
659
719
  # * Open the current file on the editor (use `EDITOR` environment variable).
660
720
  # * Note that edited file will not be reloaded.
661
721
  # * `edit <file>`
662
722
  # * Open <file> on the editor.
663
- when 'edit'
723
+ register_command 'edit' do |arg|
664
724
  if @ui.remote?
665
725
  @ui.puts "not supported on the remote console."
666
- return :retry
726
+ next :retry
667
727
  end
668
728
 
669
729
  begin
670
730
  arg = resolve_path(arg) if arg
671
731
  rescue Errno::ENOENT
672
732
  @ui.puts "not found: #{arg}"
673
- return :retry
733
+ next :retry
674
734
  end
675
735
 
676
736
  request_tc [:show, :edit, arg]
737
+ end
738
+
739
+ info_subcommands = nil
740
+ info_subcommands_abbrev = nil
677
741
 
678
742
  # * `i[nfo]`
679
- # * Show information about current frame (local/instance variables and defined constants).
680
- # * `i[nfo] l[ocal[s]]`
743
+ # * Show information about current frame (local/instance variables and defined constants).
744
+ # * `i[nfo]` <subcommand>
745
+ # * `info` has the following sub-commands.
746
+ # * Sub-commands can be specified with few letters which is unambiguous, like `l` for 'locals'.
747
+ # * `i[nfo] l or locals or local_variables`
681
748
  # * Show information about the current frame (local variables)
682
- # * It includes `self` as `%self` and a return value as `%return`.
683
- # * `i[nfo] i[var[s]]` or `i[nfo] instance`
749
+ # * It includes `self` as `%self` and a return value as `_return`.
750
+ # * `i[nfo] i or ivars or instance_variables`
684
751
  # * Show information about instance variables about `self`.
685
- # * `i[nfo] c[onst[s]]` or `i[nfo] constant[s]`
752
+ # * `info ivars <expr>` shows the instance variables of the result of `<expr>`.
753
+ # * `i[nfo] c or consts or constants`
686
754
  # * Show information about accessible constants except toplevel constants.
687
- # * `i[nfo] g[lobal[s]]`
755
+ # * `info consts <expr>` shows the constants of a class/module of the result of `<expr>`
756
+ # * `i[nfo] g or globals or global_variables`
688
757
  # * Show information about global variables
758
+ # * `i[nfo] th or threads`
759
+ # * Show all threads (same as `th[read]`).
760
+ # * `i[nfo] b or breakpoints or w or watchpoints`
761
+ # * Show all breakpoints and watchpoints.
689
762
  # * `i[nfo] ... /regexp/`
690
763
  # * Filter the output with `/regexp/`.
691
- # * `i[nfo] th[read[s]]`
692
- # * Show all threads (same as `th[read]`).
693
- when 'i', 'info'
764
+ register_command 'i', 'info', unsafe: false do |arg|
694
765
  if /\/(.+)\/\z/ =~ arg
695
766
  pat = Regexp.compile($1)
696
767
  sub = $~.pre_match.strip
@@ -698,51 +769,85 @@ module DEBUGGER__
698
769
  sub = arg
699
770
  end
700
771
 
772
+ if /\A(.+?)\b(.+)/ =~ sub
773
+ sub = $1
774
+ opt = $2.strip
775
+ opt = nil if opt.empty?
776
+ end
777
+
778
+ if sub && !info_subcommands
779
+ info_subcommands = {
780
+ locals: %w[ locals local_variables ],
781
+ ivars: %w[ ivars instance_variables ],
782
+ consts: %w[ consts constants ],
783
+ globals:%w[ globals global_variables ],
784
+ threads:%w[ threads ],
785
+ breaks: %w[ breakpoints ],
786
+ watchs: %w[ watchpoints ],
787
+ }
788
+
789
+ require_relative 'abbrev_command'
790
+ info_subcommands_abbrev = AbbrevCommand.new(info_subcommands)
791
+ end
792
+
793
+ if sub
794
+ sub = info_subcommands_abbrev.search sub, :unknown do |candidates|
795
+ # note: unreached now
796
+ @ui.puts "Ambiguous command '#{sub}': #{candidates.join(' ')}"
797
+ end
798
+ end
799
+
701
800
  case sub
702
801
  when nil
703
802
  request_tc [:show, :default, pat] # something useful
704
- when 'l', /^locals?/
803
+ when :locals
705
804
  request_tc [:show, :locals, pat]
706
- when 'i', /^ivars?/i, /^instance[_ ]variables?/i
707
- request_tc [:show, :ivars, pat]
708
- when 'c', /^consts?/i, /^constants?/i
709
- request_tc [:show, :consts, pat]
710
- when 'g', /^globals?/i, /^global[_ ]variables?/i
805
+ when :ivars
806
+ request_tc [:show, :ivars, pat, opt]
807
+ when :consts
808
+ request_tc [:show, :consts, pat, opt]
809
+ when :globals
711
810
  request_tc [:show, :globals, pat]
712
- when 'th', /threads?/
811
+ when :threads
713
812
  thread_list
714
- return :retry
813
+ :retry
814
+ when :breaks, :watchs
815
+ show_bps
816
+ :retry
715
817
  else
716
818
  @ui.puts "unrecognized argument for info command: #{arg}"
717
819
  show_help 'info'
718
- return :retry
820
+ :retry
719
821
  end
822
+ end
720
823
 
721
824
  # * `o[utline]` or `ls`
722
825
  # * Show you available methods, constants, local variables, and instance variables in the current scope.
723
826
  # * `o[utline] <expr>` or `ls <expr>`
724
827
  # * Show you available methods and instance variables of the given object.
725
828
  # * If the object is a class/module, it also lists its constants.
726
- when 'outline', 'o', 'ls'
829
+ register_command 'outline', 'o', 'ls', unsafe: false do |arg|
727
830
  request_tc [:show, :outline, arg]
831
+ end
728
832
 
729
833
  # * `display`
730
834
  # * Show display setting.
731
835
  # * `display <expr>`
732
836
  # * Show the result of `<expr>` at every suspended timing.
733
- when 'display'
837
+ register_command 'display', postmortem: false do |arg|
734
838
  if arg && !arg.empty?
735
839
  @displays << arg
736
840
  request_tc [:eval, :try_display, @displays]
737
841
  else
738
842
  request_tc [:eval, :display, @displays]
739
843
  end
844
+ end
740
845
 
741
846
  # * `undisplay`
742
847
  # * Remove all display settings.
743
848
  # * `undisplay <displaynum>`
744
849
  # * Remove a specified display setting.
745
- when 'undisplay'
850
+ register_command 'undisplay', postmortem: false, unsafe: false do |arg|
746
851
  case arg
747
852
  when /(\d+)/
748
853
  if @displays[n = $1.to_i]
@@ -753,8 +858,9 @@ module DEBUGGER__
753
858
  if ask "clear all?", 'N'
754
859
  @displays.clear
755
860
  end
756
- return :retry
861
+ :retry
757
862
  end
863
+ end
758
864
 
759
865
  ### Frame control
760
866
 
@@ -762,53 +868,57 @@ module DEBUGGER__
762
868
  # * Show the current frame.
763
869
  # * `f[rame] <framenum>`
764
870
  # * Specify a current frame. Evaluation are run on specified frame.
765
- when 'frame', 'f'
871
+ register_command 'frame', 'f', unsafe: false do |arg|
766
872
  request_tc [:frame, :set, arg]
873
+ end
767
874
 
768
875
  # * `up`
769
876
  # * Specify the upper frame.
770
- when 'up'
877
+ register_command 'up', repeat: true, unsafe: false do |arg|
771
878
  request_tc [:frame, :up]
879
+ end
772
880
 
773
881
  # * `down`
774
882
  # * Specify the lower frame.
775
- when 'down'
883
+ register_command 'down', repeat: true, unsafe: false do |arg|
776
884
  request_tc [:frame, :down]
885
+ end
777
886
 
778
887
  ### Evaluate
779
888
 
780
889
  # * `p <expr>`
781
890
  # * Evaluate like `p <expr>` on the current frame.
782
- when 'p'
891
+ register_command 'p' do |arg|
783
892
  request_tc [:eval, :p, arg.to_s]
893
+ end
784
894
 
785
895
  # * `pp <expr>`
786
896
  # * Evaluate like `pp <expr>` on the current frame.
787
- when 'pp'
897
+ register_command 'pp' do |arg|
788
898
  request_tc [:eval, :pp, arg.to_s]
899
+ end
789
900
 
790
901
  # * `eval <expr>`
791
902
  # * Evaluate `<expr>` on the current frame.
792
- when 'eval', 'call'
903
+ register_command 'eval', 'call' do |arg|
793
904
  if arg == nil || arg.empty?
794
905
  show_help 'eval'
795
906
  @ui.puts "\nTo evaluate the variable `#{cmd}`, use `pp #{cmd}` instead."
796
- return :retry
907
+ :retry
797
908
  else
798
909
  request_tc [:eval, :call, arg]
799
910
  end
911
+ end
800
912
 
801
913
  # * `irb`
802
914
  # * Invoke `irb` on the current frame.
803
- when 'irb'
915
+ register_command 'irb' do |arg|
804
916
  if @ui.remote?
805
917
  @ui.puts "not supported on the remote console."
806
- return :retry
918
+ :retry
807
919
  end
808
920
  request_tc [:eval, :irb]
809
-
810
- # don't repeat irb command
811
- @repl_prev_line = nil
921
+ end
812
922
 
813
923
  ### Trace
814
924
  # * `trace`
@@ -829,7 +939,7 @@ module DEBUGGER__
829
939
  # * Disable tracer specified by `<num>` (use `trace` command to check the numbers).
830
940
  # * `trace off [line|call|pass]`
831
941
  # * Disable all tracers. If `<type>` is provided, disable specified type tracers.
832
- when 'trace'
942
+ register_command 'trace', postmortem: false, unsafe: false do |arg|
833
943
  if (re = /\s+into:\s*(.+)/) =~ arg
834
944
  into = $1
835
945
  arg.sub!(re, '')
@@ -847,19 +957,19 @@ module DEBUGGER__
847
957
  @ui.puts "* \##{i} #{t}"
848
958
  }
849
959
  @ui.puts
850
- return :retry
960
+ :retry
851
961
 
852
962
  when /\Aline\z/
853
963
  add_tracer LineTracer.new(@ui, pattern: pattern, into: into)
854
- return :retry
964
+ :retry
855
965
 
856
966
  when /\Acall\z/
857
967
  add_tracer CallTracer.new(@ui, pattern: pattern, into: into)
858
- return :retry
968
+ :retry
859
969
 
860
970
  when /\Aexception\z/
861
971
  add_tracer ExceptionTracer.new(@ui, pattern: pattern, into: into)
862
- return :retry
972
+ :retry
863
973
 
864
974
  when /\Aobject\s+(.+)/
865
975
  request_tc [:trace, :object, $1.strip, {pattern: pattern, into: into}]
@@ -871,7 +981,7 @@ module DEBUGGER__
871
981
  else
872
982
  @ui.puts "Unmatched: #{$1}"
873
983
  end
874
- return :retry
984
+ :retry
875
985
 
876
986
  when /\Aoff(\s+(line|call|exception|object))?\z/
877
987
  @tracers.values.each{|t|
@@ -880,12 +990,13 @@ module DEBUGGER__
880
990
  @ui.puts "Disable #{t.to_s}"
881
991
  end
882
992
  }
883
- return :retry
993
+ :retry
884
994
 
885
995
  else
886
996
  @ui.puts "Unknown trace option: #{arg.inspect}"
887
- return :retry
997
+ :retry
888
998
  end
999
+ end
889
1000
 
890
1001
  # Record
891
1002
  # * `record`
@@ -897,14 +1008,15 @@ module DEBUGGER__
897
1008
  # * `s[tep]` does stepping forward with the last log.
898
1009
  # * `step reset`
899
1010
  # * Stop replay .
900
- when 'record'
1011
+ register_command 'record', postmortem: false, unsafe: false do |arg|
901
1012
  case arg
902
1013
  when nil, 'on', 'off'
903
1014
  request_tc [:record, arg&.to_sym]
904
1015
  else
905
1016
  @ui.puts "unknown command: #{arg}"
906
- return :retry
1017
+ :retry
907
1018
  end
1019
+ end
908
1020
 
909
1021
  ### Thread control
910
1022
 
@@ -912,7 +1024,7 @@ module DEBUGGER__
912
1024
  # * Show all threads.
913
1025
  # * `th[read] <thnum>`
914
1026
  # * Switch thread specified by `<thnum>`.
915
- when 'th', 'thread'
1027
+ register_command 'th', 'thread', unsafe: false do |arg|
916
1028
  case arg
917
1029
  when nil, 'list', 'l'
918
1030
  thread_list
@@ -921,7 +1033,8 @@ module DEBUGGER__
921
1033
  else
922
1034
  @ui.puts "unknown thread command: #{arg}"
923
1035
  end
924
- return :retry
1036
+ :retry
1037
+ end
925
1038
 
926
1039
  ### Configuration
927
1040
  # * `config`
@@ -934,13 +1047,14 @@ module DEBUGGER__
934
1047
  # * Append `<val>` to `<name>` if it is an array.
935
1048
  # * `config unset <name>`
936
1049
  # * Set <name> to default.
937
- when 'config'
1050
+ register_command 'config', unsafe: false do |arg|
938
1051
  config_command arg
939
- return :retry
1052
+ :retry
1053
+ end
940
1054
 
941
1055
  # * `source <file>`
942
1056
  # * Evaluate lines in `<file>` as debug commands.
943
- when 'source'
1057
+ register_command 'source' do |arg|
944
1058
  if arg
945
1059
  begin
946
1060
  cmds = File.readlines(path = File.expand_path(arg))
@@ -951,7 +1065,8 @@ module DEBUGGER__
951
1065
  else
952
1066
  show_help 'source'
953
1067
  end
954
- return :retry
1068
+ :retry
1069
+ end
955
1070
 
956
1071
  # * `open`
957
1072
  # * open debuggee port on UNIX domain socket and wait for attaching.
@@ -962,26 +1077,28 @@ module DEBUGGER__
962
1077
  # * open debuggee port for VSCode and launch VSCode if available.
963
1078
  # * `open chrome`
964
1079
  # * open debuggee port for Chrome and wait for attaching.
965
- when 'open'
1080
+ register_command 'open' do |arg|
966
1081
  case arg&.downcase
967
1082
  when '', nil
968
- repl_open
969
- when 'vscode'
970
- repl_open_vscode
971
- when /\A(.+):(\d+)\z/
972
- repl_open_tcp $1, $2.to_i
1083
+ ::DEBUGGER__.open nonstop: true
973
1084
  when /\A(\d+)z/
974
- repl_open_tcp nil, $1.to_i
1085
+ ::DEBUGGER__.open_tcp host: nil, port: $1.to_i, nonstop: true
1086
+ when /\A(.+):(\d+)\z/
1087
+ ::DEBUGGER__.open_tcp host: $1, port: $2.to_i, nonstop: true
975
1088
  when 'tcp'
976
- repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0)
1089
+ ::DEBUGGER__.open_tcp host: CONFIG[:host], port: (CONFIG[:port] || 0), nonstop: true
1090
+ when 'vscode'
1091
+ CONFIG[:open] = 'vscode'
1092
+ ::DEBUGGER__.open nonstop: true
977
1093
  when 'chrome', 'cdp'
978
- CONFIG[:open_frontend] = 'chrome'
979
- repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0)
1094
+ CONFIG[:open] = 'chrome'
1095
+ ::DEBUGGER__.open_tcp host: CONFIG[:host], port: (CONFIG[:port] || 0), nonstop: true
980
1096
  else
981
1097
  raise "Unknown arg: #{arg}"
982
1098
  end
983
1099
 
984
- return :retry
1100
+ :retry
1101
+ end
985
1102
 
986
1103
  ### Help
987
1104
 
@@ -989,26 +1106,38 @@ module DEBUGGER__
989
1106
  # * Show help for all commands.
990
1107
  # * `h[elp] <command>`
991
1108
  # * Show help for the given command.
992
- when 'h', 'help', '?'
1109
+ register_command 'h', 'help', '?', unsafe: false do |arg|
993
1110
  show_help arg
994
- return :retry
1111
+ :retry
1112
+ end
1113
+ end
995
1114
 
996
- ### END
1115
+ def process_command line
1116
+ if line.empty?
1117
+ if @repl_prev_line
1118
+ line = @repl_prev_line
1119
+ else
1120
+ return :retry
1121
+ end
1122
+ else
1123
+ @repl_prev_line = line
1124
+ end
1125
+
1126
+ /([^\s]+)(?:\s+(.+))?/ =~ line
1127
+ cmd_name, cmd_arg = $1, $2
1128
+
1129
+ if cmd = @commands[cmd_name]
1130
+ check_postmortem if !cmd.postmortem
1131
+ check_unsafe if cmd.unsafe
1132
+ cancel_auto_continue if cmd.cancel_auto_continue
1133
+ @repl_prev_line = nil if !cmd.repeat
1134
+
1135
+ cmd.block.call(cmd_arg)
997
1136
  else
998
- request_tc [:eval, :pp, line]
999
- =begin
1000
1137
  @repl_prev_line = nil
1001
- @ui.puts "unknown command: #{line}"
1002
- begin
1003
- require 'did_you_mean'
1004
- spell_checker = DidYouMean::SpellChecker.new(dictionary: DEBUGGER__.commands)
1005
- correction = spell_checker.correct(line.split(/\s/).first || '')
1006
- @ui.puts "Did you mean? #{correction.join(' or ')}" unless correction.empty?
1007
- rescue LoadError
1008
- # Don't use D
1009
- end
1010
- return :retry
1011
- =end
1138
+ check_unsafe
1139
+
1140
+ request_tc [:eval, :pp, line]
1012
1141
  end
1013
1142
 
1014
1143
  rescue Interrupt
@@ -1024,31 +1153,12 @@ module DEBUGGER__
1024
1153
  return :retry
1025
1154
  end
1026
1155
 
1027
- def repl_open_setup
1028
- @tp_thread_begin.disable
1029
- @ui.activate self
1030
- if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
1031
- thc.mark_as_management
1156
+ def step_command type, arg
1157
+ if type == :until
1158
+ leave_subsession [:step, type, arg]
1159
+ return
1032
1160
  end
1033
- @tp_thread_begin.enable
1034
- end
1035
-
1036
- def repl_open_tcp host, port, **kw
1037
- DEBUGGER__.open_tcp host: host, port: port, nonstop: true, **kw
1038
- repl_open_setup
1039
- end
1040
1161
 
1041
- def repl_open
1042
- DEBUGGER__.open nonstop: true
1043
- repl_open_setup
1044
- end
1045
-
1046
- def repl_open_vscode
1047
- CONFIG[:open_frontend] = 'vscode'
1048
- repl_open
1049
- end
1050
-
1051
- def step_command type, arg
1052
1162
  case arg
1053
1163
  when nil, /\A\d+\z/
1054
1164
  if type == :in && @tc.recorder&.replaying?
@@ -1056,12 +1166,14 @@ module DEBUGGER__
1056
1166
  else
1057
1167
  leave_subsession [:step, type, arg&.to_i]
1058
1168
  end
1059
- when /\Aback\z/, /\Areset\z/
1169
+ when /\A(back)\z/, /\A(back)\s+(\d+)\z/, /\A(reset)\z/
1060
1170
  if type != :in
1061
1171
  @ui.puts "only `step #{arg}` is supported."
1062
1172
  :retry
1063
1173
  else
1064
- request_tc [:step, arg.to_sym]
1174
+ type = $1.to_sym
1175
+ iter = $2&.to_i
1176
+ request_tc [:step, type, iter]
1065
1177
  end
1066
1178
  else
1067
1179
  @ui.puts "Unknown option: #{arg}"
@@ -1209,6 +1321,10 @@ module DEBUGGER__
1209
1321
 
1210
1322
  # breakpoint management
1211
1323
 
1324
+ def bps_pending_until_load?
1325
+ @bps.any?{|key, bp| bp.pending_until_load?}
1326
+ end
1327
+
1212
1328
  def iterate_bps
1213
1329
  deleted_bps = []
1214
1330
  i = 0
@@ -1255,8 +1371,6 @@ module DEBUGGER__
1255
1371
 
1256
1372
  def add_bp bp
1257
1373
  # don't repeat commands that add breakpoints
1258
- @repl_prev_line = nil
1259
-
1260
1374
  if @bps.has_key? bp.key
1261
1375
  if bp.duplicable?
1262
1376
  bp
@@ -1288,7 +1402,7 @@ module DEBUGGER__
1288
1402
 
1289
1403
  BREAK_KEYWORDS = %w(if: do: pre: path:).freeze
1290
1404
 
1291
- def parse_break arg
1405
+ private def parse_break type, arg
1292
1406
  mode = :sig
1293
1407
  expr = Hash.new{|h, k| h[k] = []}
1294
1408
  arg.split(' ').each{|w|
@@ -1305,13 +1419,18 @@ module DEBUGGER__
1305
1419
  expr[:path] = Regexp.compile($1)
1306
1420
  end
1307
1421
 
1422
+ if expr[:do] || expr[:pre]
1423
+ check_unsafe
1424
+ expr[:cmd] = [type, expr[:pre], expr[:do]]
1425
+ end
1426
+
1308
1427
  expr
1309
1428
  end
1310
1429
 
1311
1430
  def repl_add_breakpoint arg
1312
- expr = parse_break arg.strip
1431
+ expr = parse_break 'break', arg.strip
1313
1432
  cond = expr[:if]
1314
- cmd = ['break', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1433
+ cmd = expr[:cmd]
1315
1434
  path = expr[:path]
1316
1435
 
1317
1436
  case expr[:sig]
@@ -1332,9 +1451,9 @@ module DEBUGGER__
1332
1451
  end
1333
1452
 
1334
1453
  def repl_add_catch_breakpoint arg
1335
- expr = parse_break arg.strip
1454
+ expr = parse_break 'catch', arg.strip
1336
1455
  cond = expr[:if]
1337
- cmd = ['catch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1456
+ cmd = expr[:cmd]
1338
1457
  path = expr[:path]
1339
1458
 
1340
1459
  bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd, path: path)
@@ -1342,9 +1461,9 @@ module DEBUGGER__
1342
1461
  end
1343
1462
 
1344
1463
  def repl_add_watch_breakpoint arg
1345
- expr = parse_break arg.strip
1464
+ expr = parse_break 'watch', arg.strip
1346
1465
  cond = expr[:if]
1347
- cmd = ['watch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1466
+ cmd = expr[:cmd]
1348
1467
  path = Regexp.compile(expr[:path]) if expr[:path]
1349
1468
 
1350
1469
  request_tc [:breakpoint, :watch, expr[:sig], cond, cmd, path]
@@ -1405,8 +1524,6 @@ module DEBUGGER__
1405
1524
  # tracers
1406
1525
 
1407
1526
  def add_tracer tracer
1408
- # don't repeat commands that add tracers
1409
- @repl_prev_line = nil
1410
1527
  if @tracers.has_key? tracer.key
1411
1528
  tracer.disable
1412
1529
  @ui.puts "Duplicated tracer: #{tracer}"
@@ -1564,10 +1681,11 @@ module DEBUGGER__
1564
1681
  end
1565
1682
 
1566
1683
  private def enter_subsession
1684
+ @subsession_id += 1
1567
1685
  if !@subsession_stack.empty?
1568
- DEBUGGER__.info "Enter subsession (nested #{@subsession_stack.size})"
1686
+ DEBUGGER__.debug{ "Enter subsession (nested #{@subsession_stack.size})" }
1569
1687
  else
1570
- DEBUGGER__.info "Enter subsession"
1688
+ DEBUGGER__.debug{ "Enter subsession" }
1571
1689
  stop_all_threads
1572
1690
  @process_group.lock
1573
1691
  end
@@ -1580,11 +1698,11 @@ module DEBUGGER__
1580
1698
  @subsession_stack.pop
1581
1699
 
1582
1700
  if @subsession_stack.empty?
1583
- DEBUGGER__.info "Leave subsession"
1701
+ DEBUGGER__.debug{ "Leave subsession" }
1584
1702
  @process_group.unlock
1585
1703
  restart_all_threads
1586
1704
  else
1587
- DEBUGGER__.info "Leave subsession (nested #{@subsession_stack.size})"
1705
+ DEBUGGER__.debug{ "Leave subsession (nested #{@subsession_stack.size})" }
1588
1706
  end
1589
1707
 
1590
1708
  request_tc type if type
@@ -1723,6 +1841,12 @@ module DEBUGGER__
1723
1841
  end
1724
1842
  end
1725
1843
 
1844
+ def check_unsafe
1845
+ if @unsafe_context
1846
+ raise RuntimeError, "#{@repl_prev_line.dump} is not allowed on unsafe context."
1847
+ end
1848
+ end
1849
+
1726
1850
  def enter_postmortem_session exc
1727
1851
  return unless exc.instance_variable_defined? :@__debugger_postmortem_frames
1728
1852
 
@@ -1802,6 +1926,17 @@ module DEBUGGER__
1802
1926
  end
1803
1927
  end
1804
1928
 
1929
+ def set_no_sigint_hook old, new
1930
+ return unless old != new
1931
+ return unless @ui.respond_to? :activate_sigint
1932
+
1933
+ if old # no -> yes
1934
+ @ui.activate_sigint
1935
+ else
1936
+ @ui.deactivate_sigint
1937
+ end
1938
+ end
1939
+
1805
1940
  def save_int_trap cmd
1806
1941
  prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, cmd
1807
1942
  prev
@@ -1906,7 +2041,7 @@ module DEBUGGER__
1906
2041
  end
1907
2042
 
1908
2043
  def locked?
1909
- # DEBUGGER__.info "locked? #{@lock_level}"
2044
+ # DEBUGGER__.debug{ "locked? #{@lock_level}" }
1910
2045
  @lock_level > 0
1911
2046
  end
1912
2047
 
@@ -1991,6 +2126,10 @@ module DEBUGGER__
1991
2126
  end
1992
2127
  end
1993
2128
 
2129
+ def ignore_output_on_suspend?
2130
+ false
2131
+ end
2132
+
1994
2133
  def flush
1995
2134
  end
1996
2135
  end
@@ -2027,19 +2166,22 @@ module DEBUGGER__
2027
2166
  def self.start nonstop: false, **kw
2028
2167
  CONFIG.set_config(**kw)
2029
2168
 
2030
- unless defined? SESSION
2031
- require_relative 'local'
2032
- initialize_session{ UI_LocalConsole.new }
2169
+ if CONFIG[:open]
2170
+ open nonstop: nonstop, **kw
2171
+ else
2172
+ unless defined? SESSION
2173
+ require_relative 'local'
2174
+ initialize_session{ UI_LocalConsole.new }
2175
+ end
2176
+ setup_initial_suspend unless nonstop
2033
2177
  end
2034
-
2035
- setup_initial_suspend unless nonstop
2036
2178
  end
2037
2179
 
2038
2180
  def self.open host: nil, port: CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
2039
2181
  CONFIG.set_config(**kw)
2040
2182
  require_relative 'server'
2041
2183
 
2042
- if port || CONFIG[:open_frontend] == 'chrome' || (!::Addrinfo.respond_to?(:unix))
2184
+ if port || CONFIG[:open] == 'chrome' || (!::Addrinfo.respond_to?(:unix))
2043
2185
  open_tcp host: host, port: (port || 0), nonstop: nonstop
2044
2186
  else
2045
2187
  open_unix sock_path: sock_path, sock_dir: sock_dir, nonstop: nonstop
@@ -2101,6 +2243,18 @@ module DEBUGGER__
2101
2243
  end
2102
2244
  end
2103
2245
 
2246
+ # Exiting control
2247
+
2248
+ class << self
2249
+ def skip_all
2250
+ @skip_all = true
2251
+ end
2252
+
2253
+ def skip?
2254
+ @skip_all
2255
+ end
2256
+ end
2257
+
2104
2258
  def self.load_rc
2105
2259
  [[File.expand_path('~/.rdbgrc'), true],
2106
2260
  [File.expand_path('~/.rdbgrc.rb'), true],
@@ -2200,6 +2354,7 @@ module DEBUGGER__
2200
2354
  def self.log level, msg
2201
2355
  if check_loglevel level
2202
2356
  @logfile = STDERR unless defined? @logfile
2357
+ return if @logfile.closed?
2203
2358
 
2204
2359
  if defined? SESSION
2205
2360
  pi = SESSION.process_info
@@ -2282,8 +2437,24 @@ module DEBUGGER__
2282
2437
  end
2283
2438
  end
2284
2439
 
2285
- private def __fork_setup_for_debugger
2286
- fork_mode = CONFIG[:fork_mode]
2440
+ module DaemonInterceptor
2441
+ def daemon
2442
+ return super unless defined?(SESSION) && SESSION.active?
2443
+
2444
+ _, child_hook = __fork_setup_for_debugger(:child)
2445
+
2446
+ unless SESSION.remote?
2447
+ DEBUGGER__.warn "Can't debug the code after Process.daemon locally. Use the remote debugging feature."
2448
+ end
2449
+
2450
+ super.tap do
2451
+ child_hook.call
2452
+ end
2453
+ end
2454
+ end
2455
+
2456
+ private def __fork_setup_for_debugger fork_mode = nil
2457
+ fork_mode ||= CONFIG[:fork_mode]
2287
2458
 
2288
2459
  if fork_mode == :both && CONFIG[:parent_on_fork]
2289
2460
  fork_mode = :parent
@@ -2298,19 +2469,19 @@ module DEBUGGER__
2298
2469
  # Do nothing
2299
2470
  }
2300
2471
  child_hook = -> {
2301
- DEBUGGER__.warn "Detaching after fork from child process #{Process.pid}"
2472
+ DEBUGGER__.info "Detaching after fork from child process #{Process.pid}"
2302
2473
  SESSION.deactivate
2303
2474
  }
2304
2475
  when :child
2305
2476
  SESSION.before_fork false
2306
2477
 
2307
2478
  parent_hook = -> child_pid {
2308
- DEBUGGER__.warn "Detaching after fork from parent process #{Process.pid}"
2479
+ DEBUGGER__.info "Detaching after fork from parent process #{Process.pid}"
2309
2480
  SESSION.after_fork_parent
2310
2481
  SESSION.deactivate
2311
2482
  }
2312
2483
  child_hook = -> {
2313
- DEBUGGER__.warn "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2484
+ DEBUGGER__.info "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2314
2485
  SESSION.activate on_fork: true
2315
2486
  }
2316
2487
  when :both
@@ -2321,7 +2492,7 @@ module DEBUGGER__
2321
2492
  SESSION.after_fork_parent
2322
2493
  }
2323
2494
  child_hook = -> {
2324
- DEBUGGER__.warn "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2495
+ DEBUGGER__.info "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2325
2496
  SESSION.process_group.after_fork child: true
2326
2497
  SESSION.activate on_fork: true
2327
2498
  }
@@ -2348,6 +2519,7 @@ module DEBUGGER__
2348
2519
  module ::Process
2349
2520
  class << self
2350
2521
  prepend ForkInterceptor
2522
+ prepend DaemonInterceptor
2351
2523
  end
2352
2524
  end
2353
2525
 
@@ -2383,6 +2555,7 @@ module DEBUGGER__
2383
2555
  module ::Process
2384
2556
  class << self
2385
2557
  prepend ForkInterceptor
2558
+ prepend DaemonInterceptor
2386
2559
  end
2387
2560
  end
2388
2561
  end
@@ -2399,10 +2572,17 @@ module Kernel
2399
2572
  return if !defined?(::DEBUGGER__::SESSION) || !::DEBUGGER__::SESSION.active?
2400
2573
 
2401
2574
  if pre || (do_expr = binding.local_variable_get(:do))
2402
- cmds = ['binding.break', pre, do_expr]
2575
+ cmds = ['#debugger', pre, do_expr]
2403
2576
  end
2404
2577
 
2405
- loc = caller_locations(up_level, 1).first; ::DEBUGGER__.add_line_breakpoint loc.path, loc.lineno + 1, oneshot: true, command: cmds
2578
+ if ::DEBUGGER__::SESSION.in_subsession?
2579
+ if cmds
2580
+ commands = [*cmds[1], *cmds[2]].map{|c| c.split(';;').join("\n")}
2581
+ ::DEBUGGER__::SESSION.add_preset_commands cmds[0], commands, kick: false, continue: false
2582
+ end
2583
+ else
2584
+ loc = caller_locations(up_level, 1).first; ::DEBUGGER__.add_line_breakpoint loc.path, loc.lineno + 1, oneshot: true, command: cmds
2585
+ end
2406
2586
  self
2407
2587
  end
2408
2588