debug 1.6.1 → 1.9.1

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
@@ -22,8 +22,9 @@ end
22
22
  # restore RUBYOPT
23
23
  if (added_opt = ENV['RUBY_DEBUG_ADDED_RUBYOPT']) &&
24
24
  (rubyopt = ENV['RUBYOPT']) &&
25
- rubyopt.start_with?(added_opt)
26
- ENV['RUBYOPT'] = rubyopt.delete_prefix(rubyopt)
25
+ rubyopt.end_with?(added_opt)
26
+
27
+ ENV['RUBYOPT'] = rubyopt.delete_suffix(added_opt)
27
28
  ENV['RUBY_DEBUG_ADDED_RUBYOPT'] = nil
28
29
  end
29
30
 
@@ -81,14 +82,16 @@ class RubyVM::InstructionSequence
81
82
  def first_line
82
83
  self.to_a[4][:code_location][0]
83
84
  end unless method_defined?(:first_line)
84
- end
85
+ end if defined?(RubyVM::InstructionSequence)
85
86
 
86
87
  module DEBUGGER__
87
- 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
+
88
91
  class PostmortemError < RuntimeError; end
89
92
 
90
93
  class Session
91
- attr_reader :intercepted_sigint_cmd, :process_group
94
+ attr_reader :intercepted_sigint_cmd, :process_group, :subsession_id
92
95
 
93
96
  include Color
94
97
 
@@ -115,6 +118,7 @@ module DEBUGGER__
115
118
  @intercepted_sigint_cmd = 'DEFAULT'
116
119
  @process_group = ProcessGroup.new
117
120
  @subsession_stack = []
121
+ @subsession_id = 0
118
122
 
119
123
  @frame_map = {} # for DAP: {id => [threadId, frame_depth]} and CDP: {id => frame_depth}
120
124
  @var_map = {1 => [:globals], } # {id => ...} for DAP
@@ -124,33 +128,58 @@ module DEBUGGER__
124
128
  @obj_map = {} # { object_id => ... } for CDP
125
129
 
126
130
  @tp_thread_begin = nil
131
+ @tp_thread_end = nil
132
+
133
+ @commands = {}
134
+ @unsafe_context = false
135
+
136
+ @has_keep_script_lines = defined?(RubyVM.keep_script_lines)
137
+
127
138
  @tp_load_script = TracePoint.new(:script_compiled){|tp|
128
- ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
139
+ eval_script = tp.eval_script unless @has_keep_script_lines
140
+ ThreadClient.current.on_load tp.instruction_sequence, eval_script
129
141
  }
130
142
  @tp_load_script.enable
131
143
 
132
144
  @thread_stopper = thread_stopper
133
145
  self.postmortem = CONFIG[:postmortem]
146
+
147
+ register_default_command
134
148
  end
135
149
 
136
150
  def active?
137
151
  !@q_evt.closed?
138
152
  end
139
153
 
140
- def break_at? file, line
141
- @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
142
166
  end
143
167
 
144
168
  def activate ui = nil, on_fork: false
145
169
  @ui = ui if ui
146
170
 
147
171
  @tp_thread_begin&.disable
172
+ @tp_thread_end&.disable
148
173
  @tp_thread_begin = nil
149
-
174
+ @tp_thread_end = nil
150
175
  @ui.activate self, on_fork: on_fork
151
176
 
152
177
  q = Queue.new
178
+ first_q = Queue.new
153
179
  @session_server = Thread.new do
180
+ # make sure `@session_server` is assigned
181
+ first_q.pop; first_q = nil
182
+
154
183
  Thread.current.name = 'DEBUGGER__::SESSION@server'
155
184
  Thread.current.abort_on_exception = true
156
185
 
@@ -168,10 +197,21 @@ module DEBUGGER__
168
197
  end
169
198
  @tp_thread_begin.enable
170
199
 
200
+ @tp_thread_end = TracePoint.new(:thread_end) do |tp|
201
+ @th_clients.delete(Thread.current)
202
+ end
203
+ @tp_thread_end.enable
204
+
205
+ if CONFIG[:irb_console] && !CONFIG[:open]
206
+ require_relative "irb_integration"
207
+ thc.activate_irb_integration
208
+ end
209
+
171
210
  # session start
172
211
  q << true
173
212
  session_server_main
174
213
  end
214
+ first_q << :ok
175
215
 
176
216
  q.pop
177
217
  end
@@ -181,6 +221,7 @@ module DEBUGGER__
181
221
  @thread_stopper.disable
182
222
  @tp_load_script.disable
183
223
  @tp_thread_begin.disable
224
+ @tp_thread_end.disable
184
225
  @bps.each_value{|bp| bp.disable}
185
226
  @th_clients.each_value{|thc| thc.close}
186
227
  @tracers.values.each{|t| t.disable}
@@ -192,6 +233,16 @@ module DEBUGGER__
192
233
  def reset_ui ui
193
234
  @ui.deactivate
194
235
  @ui = ui
236
+
237
+ # activate new ui
238
+ @tp_thread_begin.disable
239
+ @tp_thread_end.disable
240
+ @ui.activate self
241
+ if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
242
+ thc.mark_as_management
243
+ end
244
+ @tp_thread_begin.enable
245
+ @tp_thread_end.enable
195
246
  end
196
247
 
197
248
  def pop_event
@@ -210,20 +261,35 @@ module DEBUGGER__
210
261
  @tc << req
211
262
  end
212
263
 
264
+ def request_tc_with_restarted_threads(req)
265
+ restart_all_threads
266
+ request_tc(req)
267
+ end
268
+
269
+ def request_eval type, src
270
+ request_tc_with_restarted_threads [:eval, type, src]
271
+ end
272
+
213
273
  def process_event evt
214
274
  # variable `@internal_info` is only used for test
215
- @tc, output, ev, @internal_info, *ev_args = evt
275
+ tc, output, ev, @internal_info, *ev_args = evt
216
276
 
217
277
  output.each{|str| @ui.puts str} if ev != :suspend
218
278
 
219
- case ev
220
-
221
- when :thread_begin # special event, tc is nil
279
+ # special event, tc is nil
280
+ # and we don't want to set @tc to the newly created thread's ThreadClient
281
+ if ev == :thread_begin
222
282
  th = ev_args.shift
223
283
  q = ev_args.shift
224
284
  on_thread_begin th
225
285
  q << true
226
286
 
287
+ return
288
+ end
289
+
290
+ @tc = tc
291
+
292
+ case ev
227
293
  when :init
228
294
  enter_subsession
229
295
  wait_command_loop
@@ -241,7 +307,7 @@ module DEBUGGER__
241
307
 
242
308
  when :suspend
243
309
  enter_subsession if ev_args.first != :replay
244
- output.each{|str| @ui.puts str}
310
+ output.each{|str| @ui.puts str} unless @ui.ignore_output_on_suspend?
245
311
 
246
312
  case ev_args.first
247
313
  when :breakpoint
@@ -262,7 +328,7 @@ module DEBUGGER__
262
328
  if @displays.empty?
263
329
  wait_command_loop
264
330
  else
265
- request_tc [:eval, :display, @displays]
331
+ request_eval :display, @displays
266
332
  end
267
333
  when :result
268
334
  raise "[BUG] not in subsession" if @subsession_stack.empty?
@@ -277,6 +343,7 @@ module DEBUGGER__
277
343
  end
278
344
  end
279
345
 
346
+ stop_all_threads
280
347
  when :method_breakpoint, :watch_breakpoint
281
348
  bp = ev_args[1]
282
349
  if bp
@@ -290,17 +357,15 @@ module DEBUGGER__
290
357
  obj_inspect = ev_args[2]
291
358
  opt = ev_args[3]
292
359
  add_tracer ObjectTracer.new(@ui, obj_id, obj_inspect, **opt)
360
+ stop_all_threads
293
361
  else
294
- # ignore
362
+ stop_all_threads
295
363
  end
296
364
 
297
365
  wait_command_loop
298
366
 
299
- when :dap_result
300
- dap_event ev_args # server.rb
301
- wait_command_loop
302
- when :cdp_result
303
- cdp_event ev_args
367
+ when :protocol_result
368
+ process_protocol_result ev_args
304
369
  wait_command_loop
305
370
  end
306
371
  end
@@ -316,7 +381,7 @@ module DEBUGGER__
316
381
  if @preset_command && !@preset_command.commands.empty?
317
382
  @preset_command.commands += cs
318
383
  else
319
- @preset_command = PresetCommand.new(cs, name, continue)
384
+ @preset_command = PresetCommands.new(cs, name, continue)
320
385
  end
321
386
 
322
387
  ThreadClient.current.on_init name if kick
@@ -389,97 +454,121 @@ module DEBUGGER__
389
454
  end
390
455
  end
391
456
 
392
- def process_command line
393
- if line.empty?
394
- if @repl_prev_line
395
- line = @repl_prev_line
396
- else
397
- return :retry
398
- end
399
- else
400
- @repl_prev_line = line
401
- end
457
+ private def register_command *names,
458
+ repeat: false, unsafe: true, cancel_auto_continue: false, postmortem: true,
459
+ &b
460
+ cmd = SessionCommand.new(b, repeat, unsafe, cancel_auto_continue, postmortem)
402
461
 
403
- /([^\s]+)(?:\s+(.+))?/ =~ line
404
- cmd, arg = $1, $2
405
-
406
- # p cmd: [cmd, *arg]
462
+ names.each{|name|
463
+ @commands[name] = cmd
464
+ }
465
+ end
407
466
 
408
- case cmd
467
+ def register_default_command
409
468
  ### Control flow
410
469
 
411
470
  # * `s[tep]`
412
471
  # * Step in. Resume the program until next breakable point.
413
472
  # * `s[tep] <n>`
414
473
  # * Step in, resume the program at `<n>`th breakable point.
415
- when 's', 'step'
416
- cancel_auto_continue
417
- check_postmortem
474
+ register_command 's', 'step',
475
+ repeat: true,
476
+ cancel_auto_continue: true,
477
+ postmortem: false do |arg|
418
478
  step_command :in, arg
479
+ end
419
480
 
420
481
  # * `n[ext]`
421
482
  # * Step over. Resume the program until next line.
422
483
  # * `n[ext] <n>`
423
484
  # * Step over, same as `step <n>`.
424
- when 'n', 'next'
425
- cancel_auto_continue
426
- check_postmortem
485
+ register_command 'n', 'next',
486
+ repeat: true,
487
+ cancel_auto_continue: true,
488
+ postmortem: false do |arg|
427
489
  step_command :next, arg
490
+ end
428
491
 
429
492
  # * `fin[ish]`
430
493
  # * Finish this frame. Resume the program until the current frame is finished.
431
494
  # * `fin[ish] <n>`
432
495
  # * Finish `<n>`th frames.
433
- when 'fin', 'finish'
434
- cancel_auto_continue
435
- check_postmortem
436
-
496
+ register_command 'fin', 'finish',
497
+ repeat: true,
498
+ cancel_auto_continue: true,
499
+ postmortem: false do |arg|
437
500
  if arg&.to_i == 0
438
501
  raise 'finish command with 0 does not make sense.'
439
502
  end
440
503
 
441
504
  step_command :finish, arg
505
+ end
442
506
 
443
- # * `c[ontinue]`
507
+ # * `u[ntil]`
508
+ # * Similar to `next` command, but only stop later lines or the end of the current frame.
509
+ # * Similar to gdb's `advance` command.
510
+ # * `u[ntil] <[file:]line>`
511
+ # * Run til the program reaches given location or the end of the current frame.
512
+ # * `u[ntil] <name>`
513
+ # * Run til the program invokes a method `<name>`. `<name>` can be a regexp with `/name/`.
514
+ register_command 'u', 'until',
515
+ repeat: true,
516
+ cancel_auto_continue: true,
517
+ postmortem: false do |arg|
518
+
519
+ step_command :until, arg
520
+ end
521
+
522
+ # * `c` or `cont` or `continue`
444
523
  # * Resume the program.
445
- when 'c', 'continue'
446
- cancel_auto_continue
524
+ register_command 'c', 'cont', 'continue',
525
+ repeat: true,
526
+ cancel_auto_continue: true do |arg|
447
527
  leave_subsession :continue
528
+ end
448
529
 
449
530
  # * `q[uit]` or `Ctrl-D`
450
531
  # * Finish debugger (with the debuggee process on non-remote debugging).
451
- when 'q', 'quit'
532
+ register_command 'q', 'quit' do |arg|
452
533
  if ask 'Really quit?'
453
- @ui.quit arg.to_i
534
+ @ui.quit arg.to_i do
535
+ request_tc :quit
536
+ end
454
537
  leave_subsession :continue
455
538
  else
456
- return :retry
539
+ next :retry
457
540
  end
541
+ end
458
542
 
459
543
  # * `q[uit]!`
460
544
  # * Same as q[uit] but without the confirmation prompt.
461
- when 'q!', 'quit!'
462
- @ui.quit arg.to_i
463
- leave_subsession nil
545
+ register_command 'q!', 'quit!', unsafe: false do |arg|
546
+ @ui.quit arg.to_i do
547
+ request_tc :quit
548
+ end
549
+ leave_subsession :continue
550
+ end
464
551
 
465
552
  # * `kill`
466
553
  # * Stop the debuggee process with `Kernel#exit!`.
467
- when 'kill'
554
+ register_command 'kill' do |arg|
468
555
  if ask 'Really kill?'
469
556
  exit! (arg || 1).to_i
470
557
  else
471
- return :retry
558
+ next :retry
472
559
  end
560
+ end
473
561
 
474
562
  # * `kill!`
475
563
  # * Same as kill but without the confirmation prompt.
476
- when 'kill!'
564
+ register_command 'kill!', unsafe: false do |arg|
477
565
  exit! (arg || 1).to_i
566
+ end
478
567
 
479
568
  # * `sigint`
480
569
  # * Execute SIGINT handler registered by the debuggee.
481
570
  # * Note that this command should be used just after stop by `SIGINT`.
482
- when 'sigint'
571
+ register_command 'sigint' do
483
572
  begin
484
573
  case cmd = @intercepted_sigint_cmd
485
574
  when nil, 'IGNORE', :IGNORE, 'DEFAULT', :DEFAULT
@@ -495,8 +584,9 @@ module DEBUGGER__
495
584
  rescue Exception => e
496
585
  @ui.puts "Exception: #{e}"
497
586
  @ui.puts e.backtrace.map{|line| " #{e}"}
498
- return :retry
587
+ next :retry
499
588
  end
589
+ end
500
590
 
501
591
  ### Breakpoint
502
592
 
@@ -521,22 +611,21 @@ module DEBUGGER__
521
611
  # * `b[reak] if: <expr>`
522
612
  # * break if: `<expr>` is true at any lines.
523
613
  # * Note that this feature is super slow.
524
- when 'b', 'break'
525
- check_postmortem
526
-
614
+ register_command 'b', 'break', postmortem: false, unsafe: false do |arg|
527
615
  if arg == nil
528
616
  show_bps
529
- return :retry
617
+ next :retry
530
618
  else
531
619
  case bp = repl_add_breakpoint(arg)
532
620
  when :noretry
533
621
  when nil
534
- return :retry
622
+ next :retry
535
623
  else
536
624
  show_bps bp
537
- return :retry
625
+ next :retry
538
626
  end
539
627
  end
628
+ end
540
629
 
541
630
  # * `catch <Error>`
542
631
  # * Set breakpoint on raising `<Error>`.
@@ -548,16 +637,16 @@ module DEBUGGER__
548
637
  # * stops and run `<command>`, and continue.
549
638
  # * `catch ... path: <path>`
550
639
  # * stops if the exception is raised from a `<path>`. `<path>` can be a regexp with `/regexp/`.
551
- when 'catch'
552
- check_postmortem
553
-
640
+ register_command 'catch', postmortem: false, unsafe: false do |arg|
554
641
  if arg
555
642
  bp = repl_add_catch_breakpoint arg
556
643
  show_bps bp if bp
557
644
  else
558
645
  show_bps
559
646
  end
560
- return :retry
647
+
648
+ :retry
649
+ end
561
650
 
562
651
  # * `watch @ivar`
563
652
  # * Stop the execution when the result of current scope's `@ivar` is changed.
@@ -570,24 +659,20 @@ module DEBUGGER__
570
659
  # * stops and run `<command>`, and continue.
571
660
  # * `watch ... path: <path>`
572
661
  # * stops if the path matches `<path>`. `<path>` can be a regexp with `/regexp/`.
573
- when 'wat', 'watch'
574
- check_postmortem
575
-
662
+ register_command 'wat', 'watch', postmortem: false, unsafe: false do |arg|
576
663
  if arg && arg.match?(/\A@\w+/)
577
664
  repl_add_watch_breakpoint(arg)
578
665
  else
579
666
  show_bps
580
- return :retry
667
+ :retry
581
668
  end
669
+ end
582
670
 
583
671
  # * `del[ete]`
584
672
  # * delete all breakpoints.
585
673
  # * `del[ete] <bpnum>`
586
674
  # * delete specified breakpoint.
587
- when 'del', 'delete'
588
- check_postmortem
589
-
590
- bp =
675
+ register_command 'del', 'delete', postmortem: false, unsafe: false do |arg|
591
676
  case arg
592
677
  when nil
593
678
  show_bps
@@ -595,12 +680,13 @@ module DEBUGGER__
595
680
  delete_bp
596
681
  end
597
682
  when /\d+/
598
- delete_bp arg.to_i
683
+ bp = delete_bp arg.to_i
599
684
  else
600
685
  nil
601
686
  end
602
687
  @ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp
603
- return :retry
688
+ :retry
689
+ end
604
690
 
605
691
  ### Information
606
692
 
@@ -612,19 +698,20 @@ module DEBUGGER__
612
698
  # * Only shows frames with method name or location info that matches `/regexp/`.
613
699
  # * `bt <num> /regexp/` or `backtrace <num> /regexp/`
614
700
  # * Only shows first `<num>` frames with method name or location info that matches `/regexp/`.
615
- when 'bt', 'backtrace'
701
+ register_command 'bt', 'backtrace', unsafe: false do |arg|
616
702
  case arg
617
703
  when /\A(\d+)\z/
618
- request_tc [:show, :backtrace, arg.to_i, nil]
704
+ request_tc_with_restarted_threads [:show, :backtrace, arg.to_i, nil]
619
705
  when /\A\/(.*)\/\z/
620
706
  pattern = $1
621
- request_tc [:show, :backtrace, nil, Regexp.compile(pattern)]
707
+ request_tc_with_restarted_threads [:show, :backtrace, nil, Regexp.compile(pattern)]
622
708
  when /\A(\d+)\s+\/(.*)\/\z/
623
709
  max, pattern = $1, $2
624
- request_tc [:show, :backtrace, max.to_i, Regexp.compile(pattern)]
710
+ request_tc_with_restarted_threads [:show, :backtrace, max.to_i, Regexp.compile(pattern)]
625
711
  else
626
- request_tc [:show, :backtrace, nil, nil]
712
+ request_tc_with_restarted_threads [:show, :backtrace, nil, nil]
627
713
  end
714
+ end
628
715
 
629
716
  # * `l[ist]`
630
717
  # * Show current frame's source code.
@@ -633,7 +720,7 @@ module DEBUGGER__
633
720
  # * Show predecessor lines as opposed to the `list` command.
634
721
  # * `l[ist] <start>` or `l[ist] <start>-<end>`
635
722
  # * Show current frame's source code from the line <start> to <end> if given.
636
- when 'l', 'list'
723
+ register_command 'l', 'list', repeat: true, unsafe: false do |arg|
637
724
  case arg ? arg.strip : nil
638
725
  when /\A(\d+)\z/
639
726
  request_tc [:show, :list, {start_line: arg.to_i - 1}]
@@ -645,45 +732,63 @@ module DEBUGGER__
645
732
  request_tc [:show, :list]
646
733
  else
647
734
  @ui.puts "Can not handle list argument: #{arg}"
648
- return :retry
735
+ :retry
649
736
  end
737
+ end
738
+
739
+ # * `whereami`
740
+ # * Show the current frame with source code.
741
+ register_command 'whereami', unsafe: false do
742
+ request_tc [:show, :whereami]
743
+ end
650
744
 
651
745
  # * `edit`
652
746
  # * Open the current file on the editor (use `EDITOR` environment variable).
653
747
  # * Note that edited file will not be reloaded.
654
748
  # * `edit <file>`
655
749
  # * Open <file> on the editor.
656
- when 'edit'
750
+ register_command 'edit' do |arg|
657
751
  if @ui.remote?
658
752
  @ui.puts "not supported on the remote console."
659
- return :retry
753
+ next :retry
660
754
  end
661
755
 
662
756
  begin
663
757
  arg = resolve_path(arg) if arg
664
758
  rescue Errno::ENOENT
665
759
  @ui.puts "not found: #{arg}"
666
- return :retry
760
+ next :retry
667
761
  end
668
762
 
669
763
  request_tc [:show, :edit, arg]
764
+ end
765
+
766
+ info_subcommands = nil
767
+ info_subcommands_abbrev = nil
670
768
 
671
769
  # * `i[nfo]`
672
- # * Show information about current frame (local/instance variables and defined constants).
673
- # * `i[nfo] l[ocal[s]]`
770
+ # * Show information about current frame (local/instance variables and defined constants).
771
+ # * `i[nfo]` <subcommand>
772
+ # * `info` has the following sub-commands.
773
+ # * Sub-commands can be specified with few letters which is unambiguous, like `l` for 'locals'.
774
+ # * `i[nfo] l or locals or local_variables`
674
775
  # * Show information about the current frame (local variables)
675
- # * It includes `self` as `%self` and a return value as `%return`.
676
- # * `i[nfo] i[var[s]]` or `i[nfo] instance`
776
+ # * It includes `self` as `%self` and a return value as `_return`.
777
+ # * `i[nfo] i or ivars or instance_variables`
677
778
  # * Show information about instance variables about `self`.
678
- # * `i[nfo] c[onst[s]]` or `i[nfo] constant[s]`
779
+ # * `info ivars <expr>` shows the instance variables of the result of `<expr>`.
780
+ # * `i[nfo] c or consts or constants`
679
781
  # * Show information about accessible constants except toplevel constants.
680
- # * `i[nfo] g[lobal[s]]`
782
+ # * `info consts <expr>` shows the constants of a class/module of the result of `<expr>`
783
+ # * `i[nfo] g or globals or global_variables`
681
784
  # * Show information about global variables
785
+ # * `i[nfo] th or threads`
786
+ # * Show all threads (same as `th[read]`).
787
+ # * `i[nfo] b or breakpoints or w or watchpoints`
788
+ # * Show all breakpoints and watchpoints.
682
789
  # * `i[nfo] ... /regexp/`
683
790
  # * Filter the output with `/regexp/`.
684
- # * `i[nfo] th[read[s]]`
685
- # * Show all threads (same as `th[read]`).
686
- when 'i', 'info'
791
+ register_command 'i', 'info', unsafe: false do |arg|
687
792
  if /\/(.+)\/\z/ =~ arg
688
793
  pat = Regexp.compile($1)
689
794
  sub = $~.pre_match.strip
@@ -691,63 +796,98 @@ module DEBUGGER__
691
796
  sub = arg
692
797
  end
693
798
 
799
+ if /\A(.+?)\b(.+)/ =~ sub
800
+ sub = $1
801
+ opt = $2.strip
802
+ opt = nil if opt.empty?
803
+ end
804
+
805
+ if sub && !info_subcommands
806
+ info_subcommands = {
807
+ locals: %w[ locals local_variables ],
808
+ ivars: %w[ ivars instance_variables ],
809
+ consts: %w[ consts constants ],
810
+ globals:%w[ globals global_variables ],
811
+ threads:%w[ threads ],
812
+ breaks: %w[ breakpoints ],
813
+ watchs: %w[ watchpoints ],
814
+ }
815
+
816
+ require_relative 'abbrev_command'
817
+ info_subcommands_abbrev = AbbrevCommand.new(info_subcommands)
818
+ end
819
+
820
+ if sub
821
+ sub = info_subcommands_abbrev.search sub, :unknown do |candidates|
822
+ # note: unreached now
823
+ @ui.puts "Ambiguous command '#{sub}': #{candidates.join(' ')}"
824
+ end
825
+ end
826
+
694
827
  case sub
695
828
  when nil
696
- request_tc [:show, :default, pat] # something useful
697
- when 'l', /^locals?/
698
- request_tc [:show, :locals, pat]
699
- when 'i', /^ivars?/i, /^instance[_ ]variables?/i
700
- request_tc [:show, :ivars, pat]
701
- when 'c', /^consts?/i, /^constants?/i
702
- request_tc [:show, :consts, pat]
703
- when 'g', /^globals?/i, /^global[_ ]variables?/i
704
- request_tc [:show, :globals, pat]
705
- when 'th', /threads?/
829
+ request_tc_with_restarted_threads [:show, :default, pat] # something useful
830
+ when :locals
831
+ request_tc_with_restarted_threads [:show, :locals, pat]
832
+ when :ivars
833
+ request_tc_with_restarted_threads [:show, :ivars, pat, opt]
834
+ when :consts
835
+ request_tc_with_restarted_threads [:show, :consts, pat, opt]
836
+ when :globals
837
+ request_tc_with_restarted_threads [:show, :globals, pat]
838
+ when :threads
706
839
  thread_list
707
- return :retry
840
+ :retry
841
+ when :breaks, :watchs
842
+ show_bps
843
+ :retry
708
844
  else
709
845
  @ui.puts "unrecognized argument for info command: #{arg}"
710
846
  show_help 'info'
711
- return :retry
847
+ :retry
712
848
  end
849
+ end
713
850
 
714
851
  # * `o[utline]` or `ls`
715
852
  # * Show you available methods, constants, local variables, and instance variables in the current scope.
716
853
  # * `o[utline] <expr>` or `ls <expr>`
717
854
  # * Show you available methods and instance variables of the given object.
718
855
  # * If the object is a class/module, it also lists its constants.
719
- when 'outline', 'o', 'ls'
720
- request_tc [:show, :outline, arg]
856
+ register_command 'outline', 'o', 'ls', unsafe: false do |arg|
857
+ request_tc_with_restarted_threads [:show, :outline, arg]
858
+ end
721
859
 
722
860
  # * `display`
723
861
  # * Show display setting.
724
862
  # * `display <expr>`
725
863
  # * Show the result of `<expr>` at every suspended timing.
726
- when 'display'
864
+ register_command 'display', postmortem: false do |arg|
727
865
  if arg && !arg.empty?
728
866
  @displays << arg
729
- request_tc [:eval, :try_display, @displays]
867
+ request_eval :try_display, @displays
730
868
  else
731
- request_tc [:eval, :display, @displays]
869
+ request_eval :display, @displays
732
870
  end
871
+ end
733
872
 
734
873
  # * `undisplay`
735
874
  # * Remove all display settings.
736
875
  # * `undisplay <displaynum>`
737
876
  # * Remove a specified display setting.
738
- when 'undisplay'
877
+ register_command 'undisplay', postmortem: false, unsafe: false do |arg|
739
878
  case arg
740
879
  when /(\d+)/
741
880
  if @displays[n = $1.to_i]
742
881
  @displays.delete_at n
743
882
  end
744
- request_tc [:eval, :display, @displays]
883
+ request_eval :display, @displays
745
884
  when nil
746
885
  if ask "clear all?", 'N'
747
886
  @displays.clear
748
887
  end
749
- return :retry
888
+ :retry
750
889
  end
890
+ end
751
891
 
752
892
  ### Frame control
753
893
 
@@ -755,53 +895,58 @@ module DEBUGGER__
755
895
  # * Show the current frame.
756
896
  # * `f[rame] <framenum>`
757
897
  # * Specify a current frame. Evaluation are run on specified frame.
758
- when 'frame', 'f'
898
+ register_command 'frame', 'f', unsafe: false do |arg|
759
899
  request_tc [:frame, :set, arg]
900
+ end
760
901
 
761
902
  # * `up`
762
903
  # * Specify the upper frame.
763
- when 'up'
904
+ register_command 'up', repeat: true, unsafe: false do |arg|
764
905
  request_tc [:frame, :up]
906
+ end
765
907
 
766
908
  # * `down`
767
909
  # * Specify the lower frame.
768
- when 'down'
910
+ register_command 'down', repeat: true, unsafe: false do |arg|
769
911
  request_tc [:frame, :down]
912
+ end
770
913
 
771
914
  ### Evaluate
772
915
 
773
916
  # * `p <expr>`
774
917
  # * Evaluate like `p <expr>` on the current frame.
775
- when 'p'
776
- request_tc [:eval, :p, arg.to_s]
918
+ register_command 'p' do |arg|
919
+ request_eval :p, arg.to_s
920
+ end
777
921
 
778
922
  # * `pp <expr>`
779
923
  # * Evaluate like `pp <expr>` on the current frame.
780
- when 'pp'
781
- request_tc [:eval, :pp, arg.to_s]
924
+ register_command 'pp' do |arg|
925
+ request_eval :pp, arg.to_s
926
+ end
782
927
 
783
928
  # * `eval <expr>`
784
929
  # * Evaluate `<expr>` on the current frame.
785
- when 'eval', 'call'
930
+ register_command 'eval', 'call' do |arg|
786
931
  if arg == nil || arg.empty?
787
932
  show_help 'eval'
788
933
  @ui.puts "\nTo evaluate the variable `#{cmd}`, use `pp #{cmd}` instead."
789
- return :retry
934
+ :retry
790
935
  else
791
- request_tc [:eval, :call, arg]
936
+ request_eval :call, arg
792
937
  end
938
+ end
793
939
 
794
940
  # * `irb`
795
941
  # * Invoke `irb` on the current frame.
796
- when 'irb'
942
+ register_command 'irb' do |arg|
797
943
  if @ui.remote?
798
- @ui.puts "not supported on the remote console."
799
- return :retry
944
+ @ui.puts "\nIRB is not supported on the remote console."
945
+ :retry
946
+ else
947
+ request_eval :irb, nil
800
948
  end
801
- request_tc [:eval, :irb]
802
-
803
- # don't repeat irb command
804
- @repl_prev_line = nil
949
+ end
805
950
 
806
951
  ### Trace
807
952
  # * `trace`
@@ -822,7 +967,7 @@ module DEBUGGER__
822
967
  # * Disable tracer specified by `<num>` (use `trace` command to check the numbers).
823
968
  # * `trace off [line|call|pass]`
824
969
  # * Disable all tracers. If `<type>` is provided, disable specified type tracers.
825
- when 'trace'
970
+ register_command 'trace', postmortem: false, unsafe: false do |arg|
826
971
  if (re = /\s+into:\s*(.+)/) =~ arg
827
972
  into = $1
828
973
  arg.sub!(re, '')
@@ -840,22 +985,22 @@ module DEBUGGER__
840
985
  @ui.puts "* \##{i} #{t}"
841
986
  }
842
987
  @ui.puts
843
- return :retry
988
+ :retry
844
989
 
845
990
  when /\Aline\z/
846
991
  add_tracer LineTracer.new(@ui, pattern: pattern, into: into)
847
- return :retry
992
+ :retry
848
993
 
849
994
  when /\Acall\z/
850
995
  add_tracer CallTracer.new(@ui, pattern: pattern, into: into)
851
- return :retry
996
+ :retry
852
997
 
853
998
  when /\Aexception\z/
854
999
  add_tracer ExceptionTracer.new(@ui, pattern: pattern, into: into)
855
- return :retry
1000
+ :retry
856
1001
 
857
1002
  when /\Aobject\s+(.+)/
858
- request_tc [:trace, :object, $1.strip, {pattern: pattern, into: into}]
1003
+ request_tc_with_restarted_threads [:trace, :object, $1.strip, {pattern: pattern, into: into}]
859
1004
 
860
1005
  when /\Aoff\s+(\d+)\z/
861
1006
  if t = @tracers.values[$1.to_i]
@@ -864,7 +1009,7 @@ module DEBUGGER__
864
1009
  else
865
1010
  @ui.puts "Unmatched: #{$1}"
866
1011
  end
867
- return :retry
1012
+ :retry
868
1013
 
869
1014
  when /\Aoff(\s+(line|call|exception|object))?\z/
870
1015
  @tracers.values.each{|t|
@@ -873,12 +1018,13 @@ module DEBUGGER__
873
1018
  @ui.puts "Disable #{t.to_s}"
874
1019
  end
875
1020
  }
876
- return :retry
1021
+ :retry
877
1022
 
878
1023
  else
879
1024
  @ui.puts "Unknown trace option: #{arg.inspect}"
880
- return :retry
1025
+ :retry
881
1026
  end
1027
+ end
882
1028
 
883
1029
  # Record
884
1030
  # * `record`
@@ -890,14 +1036,15 @@ module DEBUGGER__
890
1036
  # * `s[tep]` does stepping forward with the last log.
891
1037
  # * `step reset`
892
1038
  # * Stop replay .
893
- when 'record'
1039
+ register_command 'record', postmortem: false, unsafe: false do |arg|
894
1040
  case arg
895
1041
  when nil, 'on', 'off'
896
1042
  request_tc [:record, arg&.to_sym]
897
1043
  else
898
1044
  @ui.puts "unknown command: #{arg}"
899
- return :retry
1045
+ :retry
900
1046
  end
1047
+ end
901
1048
 
902
1049
  ### Thread control
903
1050
 
@@ -905,7 +1052,7 @@ module DEBUGGER__
905
1052
  # * Show all threads.
906
1053
  # * `th[read] <thnum>`
907
1054
  # * Switch thread specified by `<thnum>`.
908
- when 'th', 'thread'
1055
+ register_command 'th', 'thread', unsafe: false do |arg|
909
1056
  case arg
910
1057
  when nil, 'list', 'l'
911
1058
  thread_list
@@ -914,7 +1061,8 @@ module DEBUGGER__
914
1061
  else
915
1062
  @ui.puts "unknown thread command: #{arg}"
916
1063
  end
917
- return :retry
1064
+ :retry
1065
+ end
918
1066
 
919
1067
  ### Configuration
920
1068
  # * `config`
@@ -927,13 +1075,14 @@ module DEBUGGER__
927
1075
  # * Append `<val>` to `<name>` if it is an array.
928
1076
  # * `config unset <name>`
929
1077
  # * Set <name> to default.
930
- when 'config'
1078
+ register_command 'config', unsafe: false do |arg|
931
1079
  config_command arg
932
- return :retry
1080
+ :retry
1081
+ end
933
1082
 
934
1083
  # * `source <file>`
935
1084
  # * Evaluate lines in `<file>` as debug commands.
936
- when 'source'
1085
+ register_command 'source' do |arg|
937
1086
  if arg
938
1087
  begin
939
1088
  cmds = File.readlines(path = File.expand_path(arg))
@@ -944,7 +1093,8 @@ module DEBUGGER__
944
1093
  else
945
1094
  show_help 'source'
946
1095
  end
947
- return :retry
1096
+ :retry
1097
+ end
948
1098
 
949
1099
  # * `open`
950
1100
  # * open debuggee port on UNIX domain socket and wait for attaching.
@@ -955,26 +1105,28 @@ module DEBUGGER__
955
1105
  # * open debuggee port for VSCode and launch VSCode if available.
956
1106
  # * `open chrome`
957
1107
  # * open debuggee port for Chrome and wait for attaching.
958
- when 'open'
1108
+ register_command 'open' do |arg|
959
1109
  case arg&.downcase
960
1110
  when '', nil
961
- repl_open
962
- when 'vscode'
963
- repl_open_vscode
964
- when /\A(.+):(\d+)\z/
965
- repl_open_tcp $1, $2.to_i
1111
+ ::DEBUGGER__.open nonstop: true
966
1112
  when /\A(\d+)z/
967
- repl_open_tcp nil, $1.to_i
1113
+ ::DEBUGGER__.open_tcp host: nil, port: $1.to_i, nonstop: true
1114
+ when /\A(.+):(\d+)\z/
1115
+ ::DEBUGGER__.open_tcp host: $1, port: $2.to_i, nonstop: true
968
1116
  when 'tcp'
969
- repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0)
1117
+ ::DEBUGGER__.open_tcp host: CONFIG[:host], port: (CONFIG[:port] || 0), nonstop: true
1118
+ when 'vscode'
1119
+ CONFIG[:open] = 'vscode'
1120
+ ::DEBUGGER__.open nonstop: true
970
1121
  when 'chrome', 'cdp'
971
- CONFIG[:open_frontend] = 'chrome'
972
- repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0)
1122
+ CONFIG[:open] = 'chrome'
1123
+ ::DEBUGGER__.open_tcp host: CONFIG[:host], port: (CONFIG[:port] || 0), nonstop: true
973
1124
  else
974
1125
  raise "Unknown arg: #{arg}"
975
1126
  end
976
1127
 
977
- return :retry
1128
+ :retry
1129
+ end
978
1130
 
979
1131
  ### Help
980
1132
 
@@ -982,26 +1134,38 @@ module DEBUGGER__
982
1134
  # * Show help for all commands.
983
1135
  # * `h[elp] <command>`
984
1136
  # * Show help for the given command.
985
- when 'h', 'help', '?'
1137
+ register_command 'h', 'help', '?', unsafe: false do |arg|
986
1138
  show_help arg
987
- return :retry
1139
+ :retry
1140
+ end
1141
+ end
988
1142
 
989
- ### END
1143
+ def process_command line
1144
+ if line.empty?
1145
+ if @repl_prev_line
1146
+ line = @repl_prev_line
1147
+ else
1148
+ return :retry
1149
+ end
1150
+ else
1151
+ @repl_prev_line = line
1152
+ end
1153
+
1154
+ /([^\s]+)(?:\s+(.+))?/ =~ line
1155
+ cmd_name, cmd_arg = $1, $2
1156
+
1157
+ if cmd = @commands[cmd_name]
1158
+ check_postmortem if !cmd.postmortem
1159
+ check_unsafe if cmd.unsafe
1160
+ cancel_auto_continue if cmd.cancel_auto_continue
1161
+ @repl_prev_line = nil if !cmd.repeat
1162
+
1163
+ cmd.block.call(cmd_arg)
990
1164
  else
991
- request_tc [:eval, :pp, line]
992
- =begin
993
1165
  @repl_prev_line = nil
994
- @ui.puts "unknown command: #{line}"
995
- begin
996
- require 'did_you_mean'
997
- spell_checker = DidYouMean::SpellChecker.new(dictionary: DEBUGGER__.commands)
998
- correction = spell_checker.correct(line.split(/\s/).first || '')
999
- @ui.puts "Did you mean? #{correction.join(' or ')}" unless correction.empty?
1000
- rescue LoadError
1001
- # Don't use D
1002
- end
1003
- return :retry
1004
- =end
1166
+ check_unsafe
1167
+
1168
+ request_eval :pp, line
1005
1169
  end
1006
1170
 
1007
1171
  rescue Interrupt
@@ -1017,31 +1181,12 @@ module DEBUGGER__
1017
1181
  return :retry
1018
1182
  end
1019
1183
 
1020
- def repl_open_setup
1021
- @tp_thread_begin.disable
1022
- @ui.activate self
1023
- if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
1024
- thc.mark_as_management
1184
+ def step_command type, arg
1185
+ if type == :until
1186
+ leave_subsession [:step, type, arg]
1187
+ return
1025
1188
  end
1026
- @tp_thread_begin.enable
1027
- end
1028
-
1029
- def repl_open_tcp host, port, **kw
1030
- DEBUGGER__.open_tcp host: host, port: port, nonstop: true, **kw
1031
- repl_open_setup
1032
- end
1033
-
1034
- def repl_open
1035
- DEBUGGER__.open nonstop: true
1036
- repl_open_setup
1037
- end
1038
-
1039
- def repl_open_vscode
1040
- CONFIG[:open_frontend] = 'vscode'
1041
- repl_open
1042
- end
1043
1189
 
1044
- def step_command type, arg
1045
1190
  case arg
1046
1191
  when nil, /\A\d+\z/
1047
1192
  if type == :in && @tc.recorder&.replaying?
@@ -1049,12 +1194,14 @@ module DEBUGGER__
1049
1194
  else
1050
1195
  leave_subsession [:step, type, arg&.to_i]
1051
1196
  end
1052
- when /\Aback\z/, /\Areset\z/
1197
+ when /\A(back)\z/, /\A(back)\s+(\d+)\z/, /\A(reset)\z/
1053
1198
  if type != :in
1054
1199
  @ui.puts "only `step #{arg}` is supported."
1055
1200
  :retry
1056
1201
  else
1057
- request_tc [:step, arg.to_sym]
1202
+ type = $1.to_sym
1203
+ iter = $2&.to_i
1204
+ request_tc [:step, type, iter]
1058
1205
  end
1059
1206
  else
1060
1207
  @ui.puts "Unknown option: #{arg}"
@@ -1248,8 +1395,6 @@ module DEBUGGER__
1248
1395
 
1249
1396
  def add_bp bp
1250
1397
  # don't repeat commands that add breakpoints
1251
- @repl_prev_line = nil
1252
-
1253
1398
  if @bps.has_key? bp.key
1254
1399
  if bp.duplicable?
1255
1400
  bp
@@ -1281,7 +1426,7 @@ module DEBUGGER__
1281
1426
 
1282
1427
  BREAK_KEYWORDS = %w(if: do: pre: path:).freeze
1283
1428
 
1284
- def parse_break arg
1429
+ private def parse_break type, arg
1285
1430
  mode = :sig
1286
1431
  expr = Hash.new{|h, k| h[k] = []}
1287
1432
  arg.split(' ').each{|w|
@@ -1298,13 +1443,18 @@ module DEBUGGER__
1298
1443
  expr[:path] = Regexp.compile($1)
1299
1444
  end
1300
1445
 
1446
+ if expr[:do] || expr[:pre]
1447
+ check_unsafe
1448
+ expr[:cmd] = [type, expr[:pre], expr[:do]]
1449
+ end
1450
+
1301
1451
  expr
1302
1452
  end
1303
1453
 
1304
1454
  def repl_add_breakpoint arg
1305
- expr = parse_break arg.strip
1455
+ expr = parse_break 'break', arg.strip
1306
1456
  cond = expr[:if]
1307
- cmd = ['break', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1457
+ cmd = expr[:cmd]
1308
1458
  path = expr[:path]
1309
1459
 
1310
1460
  case expr[:sig]
@@ -1325,9 +1475,9 @@ module DEBUGGER__
1325
1475
  end
1326
1476
 
1327
1477
  def repl_add_catch_breakpoint arg
1328
- expr = parse_break arg.strip
1478
+ expr = parse_break 'catch', arg.strip
1329
1479
  cond = expr[:if]
1330
- cmd = ['catch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1480
+ cmd = expr[:cmd]
1331
1481
  path = expr[:path]
1332
1482
 
1333
1483
  bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd, path: path)
@@ -1335,9 +1485,9 @@ module DEBUGGER__
1335
1485
  end
1336
1486
 
1337
1487
  def repl_add_watch_breakpoint arg
1338
- expr = parse_break arg.strip
1488
+ expr = parse_break 'watch', arg.strip
1339
1489
  cond = expr[:if]
1340
- cmd = ['watch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1490
+ cmd = expr[:cmd]
1341
1491
  path = Regexp.compile(expr[:path]) if expr[:path]
1342
1492
 
1343
1493
  request_tc [:breakpoint, :watch, expr[:sig], cond, cmd, path]
@@ -1374,7 +1524,7 @@ module DEBUGGER__
1374
1524
  def clear_line_breakpoints path
1375
1525
  path = resolve_path(path)
1376
1526
  clear_breakpoints do |k, bp|
1377
- bp.is_a?(LineBreakpoint) && DEBUGGER__.compare_path(k.first, path)
1527
+ bp.is_a?(LineBreakpoint) && bp.path_is?(path)
1378
1528
  end
1379
1529
  rescue Errno::ENOENT
1380
1530
  # just ignore
@@ -1398,9 +1548,7 @@ module DEBUGGER__
1398
1548
  # tracers
1399
1549
 
1400
1550
  def add_tracer tracer
1401
- # don't repeat commands that add tracers
1402
- @repl_prev_line = nil
1403
- if @tracers.has_key? tracer.key
1551
+ if @tracers[tracer.key]&.enabled?
1404
1552
  tracer.disable
1405
1553
  @ui.puts "Duplicated tracer: #{tracer}"
1406
1554
  else
@@ -1557,10 +1705,11 @@ module DEBUGGER__
1557
1705
  end
1558
1706
 
1559
1707
  private def enter_subsession
1708
+ @subsession_id += 1
1560
1709
  if !@subsession_stack.empty?
1561
- DEBUGGER__.info "Enter subsession (nested #{@subsession_stack.size})"
1710
+ DEBUGGER__.debug{ "Enter subsession (nested #{@subsession_stack.size})" }
1562
1711
  else
1563
- DEBUGGER__.info "Enter subsession"
1712
+ DEBUGGER__.debug{ "Enter subsession" }
1564
1713
  stop_all_threads
1565
1714
  @process_group.lock
1566
1715
  end
@@ -1573,11 +1722,11 @@ module DEBUGGER__
1573
1722
  @subsession_stack.pop
1574
1723
 
1575
1724
  if @subsession_stack.empty?
1576
- DEBUGGER__.info "Leave subsession"
1725
+ DEBUGGER__.debug{ "Leave subsession" }
1577
1726
  @process_group.unlock
1578
1727
  restart_all_threads
1579
1728
  else
1580
- DEBUGGER__.info "Leave subsession (nested #{@subsession_stack.size})"
1729
+ DEBUGGER__.debug{ "Leave subsession (nested #{@subsession_stack.size})" }
1581
1730
  end
1582
1731
 
1583
1732
  request_tc type if type
@@ -1599,25 +1748,30 @@ module DEBUGGER__
1599
1748
  file_path, reloaded = @sr.add(iseq, src)
1600
1749
  @ui.event :load, file_path, reloaded
1601
1750
 
1602
- pending_line_breakpoints = @bps.find_all do |key, bp|
1603
- LineBreakpoint === bp && !bp.iseq
1604
- end
1751
+ # check breakpoints
1752
+ if file_path
1753
+ @bps.find_all do |_key, bp|
1754
+ LineBreakpoint === bp && bp.path_is?(file_path) && (iseq.first_lineno..iseq.last_line).cover?(bp.line)
1755
+ end.each do |_key, bp|
1756
+ if !bp.iseq
1757
+ bp.try_activate iseq
1758
+ elsif reloaded
1759
+ @bps.delete bp.key # to allow duplicate
1605
1760
 
1606
- pending_line_breakpoints.each do |_key, bp|
1607
- if DEBUGGER__.compare_path(bp.path, (iseq.absolute_path || iseq.path))
1608
- bp.try_activate iseq
1609
- end
1610
- end
1761
+ # When we delete a breakpoint from the @bps hash, we also need to deactivate it or else its tracepoint event
1762
+ # will continue to be enabled and we'll suspend on ghost breakpoints
1763
+ bp.delete
1611
1764
 
1612
- if reloaded
1613
- @bps.find_all do |key, bp|
1614
- LineBreakpoint === bp && DEBUGGER__.compare_path(bp.path, file_path)
1615
- end.each do |_key, bp|
1616
- @bps.delete bp.key # to allow duplicate
1617
- if nbp = LineBreakpoint.copy(bp, iseq)
1765
+ nbp = LineBreakpoint.copy(bp, iseq)
1618
1766
  add_bp nbp
1619
1767
  end
1620
1768
  end
1769
+ else # !file_path => file_path is not existing
1770
+ @bps.find_all do |_key, bp|
1771
+ LineBreakpoint === bp && !bp.iseq && DEBUGGER__.compare_path(bp.path, (iseq.absolute_path || iseq.path))
1772
+ end.each do |_key, bp|
1773
+ bp.try_activate iseq
1774
+ end
1621
1775
  end
1622
1776
  end
1623
1777
 
@@ -1716,6 +1870,12 @@ module DEBUGGER__
1716
1870
  end
1717
1871
  end
1718
1872
 
1873
+ def check_unsafe
1874
+ if @unsafe_context
1875
+ raise RuntimeError, "#{@repl_prev_line.dump} is not allowed on unsafe context."
1876
+ end
1877
+ end
1878
+
1719
1879
  def enter_postmortem_session exc
1720
1880
  return unless exc.instance_variable_defined? :@__debugger_postmortem_frames
1721
1881
 
@@ -1795,6 +1955,17 @@ module DEBUGGER__
1795
1955
  end
1796
1956
  end
1797
1957
 
1958
+ def set_no_sigint_hook old, new
1959
+ return unless old != new
1960
+ return unless @ui.respond_to? :activate_sigint
1961
+
1962
+ if old # no -> yes
1963
+ @ui.activate_sigint
1964
+ else
1965
+ @ui.deactivate_sigint
1966
+ end
1967
+ end
1968
+
1798
1969
  def save_int_trap cmd
1799
1970
  prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, cmd
1800
1971
  prev
@@ -1838,6 +2009,13 @@ module DEBUGGER__
1838
2009
  def after_fork_parent
1839
2010
  @ui.after_fork_parent
1840
2011
  end
2012
+
2013
+ # experimental API
2014
+ def extend_feature session: nil, thread_client: nil, ui: nil
2015
+ Session.include session if session
2016
+ ThreadClient.include thread_client if thread_client
2017
+ @ui.extend ui if ui
2018
+ end
1841
2019
  end
1842
2020
 
1843
2021
  class ProcessGroup
@@ -1886,9 +2064,11 @@ module DEBUGGER__
1886
2064
 
1887
2065
  def after_fork child: true
1888
2066
  if child || !@lock_file
1889
- @m = Mutex.new
1890
- @lock_level = 0
1891
- @lock_file = open(@lock_tempfile.path, 'w')
2067
+ @m = Mutex.new unless @m
2068
+ @m.synchronize do
2069
+ @lock_level = 0
2070
+ @lock_file = open(@lock_tempfile.path, 'w')
2071
+ end
1892
2072
  end
1893
2073
  end
1894
2074
 
@@ -1897,7 +2077,7 @@ module DEBUGGER__
1897
2077
  end
1898
2078
 
1899
2079
  def locked?
1900
- # DEBUGGER__.info "locked? #{@lock_level}"
2080
+ # DEBUGGER__.debug{ "locked? #{@lock_level}" }
1901
2081
  @lock_level > 0
1902
2082
  end
1903
2083
 
@@ -1982,6 +2162,10 @@ module DEBUGGER__
1982
2162
  end
1983
2163
  end
1984
2164
 
2165
+ def ignore_output_on_suspend?
2166
+ false
2167
+ end
2168
+
1985
2169
  def flush
1986
2170
  end
1987
2171
  end
@@ -2006,6 +2190,7 @@ module DEBUGGER__
2006
2190
  case loc.absolute_path
2007
2191
  when dir_prefix
2008
2192
  when %r{rubygems/core_ext/kernel_require\.rb}
2193
+ when %r{bundled_gems\.rb}
2009
2194
  else
2010
2195
  return loc if loc.absolute_path
2011
2196
  end
@@ -2018,19 +2203,22 @@ module DEBUGGER__
2018
2203
  def self.start nonstop: false, **kw
2019
2204
  CONFIG.set_config(**kw)
2020
2205
 
2021
- unless defined? SESSION
2022
- require_relative 'local'
2023
- initialize_session{ UI_LocalConsole.new }
2206
+ if CONFIG[:open]
2207
+ open nonstop: nonstop, **kw
2208
+ else
2209
+ unless defined? SESSION
2210
+ require_relative 'local'
2211
+ initialize_session{ UI_LocalConsole.new }
2212
+ end
2213
+ setup_initial_suspend unless nonstop
2024
2214
  end
2025
-
2026
- setup_initial_suspend unless nonstop
2027
2215
  end
2028
2216
 
2029
2217
  def self.open host: nil, port: CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
2030
2218
  CONFIG.set_config(**kw)
2031
2219
  require_relative 'server'
2032
2220
 
2033
- if port || CONFIG[:open_frontend] == 'chrome' || (!::Addrinfo.respond_to?(:unix))
2221
+ if port || CONFIG[:open] == 'chrome' || (!::Addrinfo.respond_to?(:unix))
2034
2222
  open_tcp host: host, port: (port || 0), nonstop: nonstop
2035
2223
  else
2036
2224
  open_unix sock_path: sock_path, sock_dir: sock_dir, nonstop: nonstop
@@ -2092,6 +2280,18 @@ module DEBUGGER__
2092
2280
  end
2093
2281
  end
2094
2282
 
2283
+ # Exiting control
2284
+
2285
+ class << self
2286
+ def skip_all
2287
+ @skip_all = true
2288
+ end
2289
+
2290
+ def skip?
2291
+ @skip_all
2292
+ end
2293
+ end
2294
+
2095
2295
  def self.load_rc
2096
2296
  [[File.expand_path('~/.rdbgrc'), true],
2097
2297
  [File.expand_path('~/.rdbgrc.rb'), true],
@@ -2120,7 +2320,7 @@ module DEBUGGER__
2120
2320
  end
2121
2321
 
2122
2322
  # Inspector
2123
-
2323
+
2124
2324
  SHORT_INSPECT_LENGTH = 40
2125
2325
 
2126
2326
  class LimitedPP
@@ -2191,6 +2391,7 @@ module DEBUGGER__
2191
2391
  def self.log level, msg
2192
2392
  if check_loglevel level
2193
2393
  @logfile = STDERR unless defined? @logfile
2394
+ return if @logfile.closed?
2194
2395
 
2195
2396
  if defined? SESSION
2196
2397
  pi = SESSION.process_info
@@ -2222,7 +2423,7 @@ module DEBUGGER__
2222
2423
  # depend on the file system. So this check is only roughly estimation.
2223
2424
 
2224
2425
  def self.compare_path(a, b)
2225
- a.downcase == b.downcase
2426
+ a&.downcase == b&.downcase
2226
2427
  end
2227
2428
  else
2228
2429
  def self.compare_path(a, b)
@@ -2273,8 +2474,24 @@ module DEBUGGER__
2273
2474
  end
2274
2475
  end
2275
2476
 
2276
- private def __fork_setup_for_debugger
2277
- fork_mode = CONFIG[:fork_mode]
2477
+ module DaemonInterceptor
2478
+ def daemon(*args)
2479
+ return super unless defined?(SESSION) && SESSION.active?
2480
+
2481
+ _, child_hook = __fork_setup_for_debugger(:child)
2482
+
2483
+ unless SESSION.remote?
2484
+ DEBUGGER__.warn "Can't debug the code after Process.daemon locally. Use the remote debugging feature."
2485
+ end
2486
+
2487
+ super.tap do
2488
+ child_hook.call
2489
+ end
2490
+ end
2491
+ end
2492
+
2493
+ private def __fork_setup_for_debugger fork_mode = nil
2494
+ fork_mode ||= CONFIG[:fork_mode]
2278
2495
 
2279
2496
  if fork_mode == :both && CONFIG[:parent_on_fork]
2280
2497
  fork_mode = :parent
@@ -2289,19 +2506,19 @@ module DEBUGGER__
2289
2506
  # Do nothing
2290
2507
  }
2291
2508
  child_hook = -> {
2292
- DEBUGGER__.warn "Detaching after fork from child process #{Process.pid}"
2509
+ DEBUGGER__.info "Detaching after fork from child process #{Process.pid}"
2293
2510
  SESSION.deactivate
2294
2511
  }
2295
2512
  when :child
2296
2513
  SESSION.before_fork false
2297
2514
 
2298
2515
  parent_hook = -> child_pid {
2299
- DEBUGGER__.warn "Detaching after fork from parent process #{Process.pid}"
2516
+ DEBUGGER__.info "Detaching after fork from parent process #{Process.pid}"
2300
2517
  SESSION.after_fork_parent
2301
2518
  SESSION.deactivate
2302
2519
  }
2303
2520
  child_hook = -> {
2304
- DEBUGGER__.warn "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2521
+ DEBUGGER__.info "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2305
2522
  SESSION.activate on_fork: true
2306
2523
  }
2307
2524
  when :both
@@ -2312,7 +2529,7 @@ module DEBUGGER__
2312
2529
  SESSION.after_fork_parent
2313
2530
  }
2314
2531
  child_hook = -> {
2315
- DEBUGGER__.warn "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2532
+ DEBUGGER__.info "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2316
2533
  SESSION.process_group.after_fork child: true
2317
2534
  SESSION.activate on_fork: true
2318
2535
  }
@@ -2324,7 +2541,17 @@ module DEBUGGER__
2324
2541
 
2325
2542
  module TrapInterceptor
2326
2543
  def trap sig, *command, &command_proc
2327
- case sig&.to_sym
2544
+ sym =
2545
+ case sig
2546
+ when String
2547
+ sig.to_sym
2548
+ when Integer
2549
+ Signal.signame(sig)&.to_sym
2550
+ else
2551
+ sig
2552
+ end
2553
+
2554
+ case sym
2328
2555
  when :INT, :SIGINT
2329
2556
  if defined?(SESSION) && SESSION.active? && SESSION.intercept_trap_sigint?
2330
2557
  return SESSION.save_int_trap(command.empty? ? command_proc : command.first)
@@ -2339,6 +2566,7 @@ module DEBUGGER__
2339
2566
  module ::Process
2340
2567
  class << self
2341
2568
  prepend ForkInterceptor
2569
+ prepend DaemonInterceptor
2342
2570
  end
2343
2571
  end
2344
2572
 
@@ -2374,6 +2602,7 @@ module DEBUGGER__
2374
2602
  module ::Process
2375
2603
  class << self
2376
2604
  prepend ForkInterceptor
2605
+ prepend DaemonInterceptor
2377
2606
  end
2378
2607
  end
2379
2608
  end
@@ -2390,10 +2619,17 @@ module Kernel
2390
2619
  return if !defined?(::DEBUGGER__::SESSION) || !::DEBUGGER__::SESSION.active?
2391
2620
 
2392
2621
  if pre || (do_expr = binding.local_variable_get(:do))
2393
- cmds = ['binding.break', pre, do_expr]
2622
+ cmds = ['#debugger', pre, do_expr]
2394
2623
  end
2395
2624
 
2396
- loc = caller_locations(up_level, 1).first; ::DEBUGGER__.add_line_breakpoint loc.path, loc.lineno + 1, oneshot: true, command: cmds
2625
+ if ::DEBUGGER__::SESSION.in_subsession?
2626
+ if cmds
2627
+ commands = [*cmds[1], *cmds[2]].map{|c| c.split(';;').join("\n")}
2628
+ ::DEBUGGER__::SESSION.add_preset_commands cmds[0], commands, kick: false, continue: false
2629
+ end
2630
+ else
2631
+ loc = caller_locations(up_level, 1).first; ::DEBUGGER__.add_line_breakpoint loc.path, loc.lineno + 1, oneshot: true, command: cmds
2632
+ end
2397
2633
  self
2398
2634
  end
2399
2635