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
data/lib/debug/session.rb CHANGED
@@ -7,7 +7,7 @@ return if ENV['RUBY_DEBUG_ENABLE'] == '0'
7
7
  if $0.end_with?('bin/bundle') && ARGV.first == 'exec'
8
8
  trace_var(:$0) do |file|
9
9
  trace_var(:$0, nil)
10
- if /-r (#{__dir__}\S+)/ =~ ENV['RUBYOPT']
10
+ if /-r (#{Regexp.escape(__dir__)}\S+)/ =~ ENV['RUBYOPT']
11
11
  lib = $1
12
12
  $LOADED_FEATURES.delete_if{|path| path.start_with?(__dir__)}
13
13
  ENV['RUBY_DEBUG_INITIAL_SUSPEND_PATH'] = file
@@ -19,6 +19,15 @@ if $0.end_with?('bin/bundle') && ARGV.first == 'exec'
19
19
  return
20
20
  end
21
21
 
22
+ # restore RUBYOPT
23
+ if (added_opt = ENV['RUBY_DEBUG_ADDED_RUBYOPT']) &&
24
+ (rubyopt = ENV['RUBYOPT']) &&
25
+ rubyopt.end_with?(added_opt)
26
+
27
+ ENV['RUBYOPT'] = rubyopt.delete_suffix(added_opt)
28
+ ENV['RUBY_DEBUG_ADDED_RUBYOPT'] = nil
29
+ end
30
+
22
31
  require_relative 'frame_info'
23
32
  require_relative 'config'
24
33
  require_relative 'thread_client'
@@ -31,7 +40,8 @@ $LOADED_FEATURES << 'debug.rb'
31
40
  $LOADED_FEATURES << File.expand_path(File.join(__dir__, '..', 'debug.rb'))
32
41
  require 'debug' # invalidate the $LOADED_FEATURE cache
33
42
 
34
- require 'json' if ENV['RUBY_DEBUG_TEST_MODE']
43
+ require 'json' if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal'
44
+ require 'pp'
35
45
 
36
46
  class RubyVM::InstructionSequence
37
47
  def traceable_lines_norec lines
@@ -56,34 +66,37 @@ class RubyVM::InstructionSequence
56
66
 
57
67
  def type
58
68
  self.to_a[9]
59
- end
69
+ end unless method_defined?(:type)
60
70
 
61
- def argc
62
- self.to_a[4][:arg_size]
63
- end
64
-
65
- def locals
66
- self.to_a[10]
67
- end
71
+ def parameters_symbols
72
+ ary = self.to_a
73
+ argc = ary[4][:arg_size]
74
+ locals = ary.to_a[10]
75
+ locals[0...argc]
76
+ end unless method_defined?(:parameters_symbols)
68
77
 
69
78
  def last_line
70
79
  self.to_a[4][:code_location][2]
71
- end
80
+ end unless method_defined?(:last_line)
72
81
 
73
82
  def first_line
74
83
  self.to_a[4][:code_location][0]
75
- end
76
- end
84
+ end unless method_defined?(:first_line)
85
+ end if defined?(RubyVM::InstructionSequence)
77
86
 
78
87
  module DEBUGGER__
79
- 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
+
80
91
  class PostmortemError < RuntimeError; end
81
92
 
82
93
  class Session
83
- attr_reader :intercepted_sigint_cmd, :process_group
94
+ attr_reader :intercepted_sigint_cmd, :process_group, :subsession_id
84
95
 
85
- def initialize ui
86
- @ui = ui
96
+ include Color
97
+
98
+ def initialize
99
+ @ui = nil
87
100
  @sr = SourceRepository.new
88
101
  @bps = {} # bp.key => bp
89
102
  # [file, line] => LineBreakpoint
@@ -104,48 +117,69 @@ module DEBUGGER__
104
117
  @intercept_trap_sigint = false
105
118
  @intercepted_sigint_cmd = 'DEFAULT'
106
119
  @process_group = ProcessGroup.new
107
- @subsession = nil
120
+ @subsession_stack = []
121
+ @subsession_id = 0
108
122
 
109
123
  @frame_map = {} # for DAP: {id => [threadId, frame_depth]} and CDP: {id => frame_depth}
110
124
  @var_map = {1 => [:globals], } # {id => ...} for DAP
111
125
  @src_map = {} # {id => src}
112
126
 
113
- @script_paths = [File.absolute_path($0)] # for CDP
127
+ @scr_id_map = {} # for CDP
114
128
  @obj_map = {} # { object_id => ... } for CDP
115
129
 
116
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
+
117
138
  @tp_load_script = TracePoint.new(:script_compiled){|tp|
118
- 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
119
141
  }
120
142
  @tp_load_script.enable
121
143
 
122
144
  @thread_stopper = thread_stopper
123
-
124
- activate
125
-
126
145
  self.postmortem = CONFIG[:postmortem]
146
+
147
+ register_default_command
127
148
  end
128
149
 
129
150
  def active?
130
151
  !@q_evt.closed?
131
152
  end
132
153
 
133
- def break_at? file, line
134
- @bps.has_key? [file, line]
154
+ def remote?
155
+ @ui.remote?
135
156
  end
136
157
 
137
- def activate on_fork: false
138
- @tp_thread_begin&.disable
139
- @tp_thread_begin = nil
140
-
141
- if on_fork
142
- @ui.activate self, on_fork: true
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
143
163
  else
144
- @ui.activate self, on_fork: false
164
+ false
145
165
  end
166
+ end
167
+
168
+ def activate ui = nil, on_fork: false
169
+ @ui = ui if ui
170
+
171
+ @tp_thread_begin&.disable
172
+ @tp_thread_end&.disable
173
+ @tp_thread_begin = nil
174
+ @tp_thread_end = nil
175
+ @ui.activate self, on_fork: on_fork
146
176
 
147
177
  q = Queue.new
178
+ first_q = Queue.new
148
179
  @session_server = Thread.new do
180
+ # make sure `@session_server` is assigned
181
+ first_q.pop; first_q = nil
182
+
149
183
  Thread.current.name = 'DEBUGGER__::SESSION@server'
150
184
  Thread.current.abort_on_exception = true
151
185
 
@@ -163,12 +197,24 @@ module DEBUGGER__
163
197
  end
164
198
  @tp_thread_begin.enable
165
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
+
166
205
  # session start
167
206
  q << true
168
207
  session_server_main
169
208
  end
209
+ first_q << :ok
170
210
 
171
211
  q.pop
212
+
213
+ # For activating irb:rdbg with startup config like `RUBY_DEBUG_IRB_CONSOLE=1`
214
+ # Because in that case the `Config#if_updated` callback would not be triggered
215
+ if CONFIG[:irb_console] && !CONFIG[:open]
216
+ activate_irb_integration
217
+ end
172
218
  end
173
219
 
174
220
  def deactivate
@@ -176,6 +222,7 @@ module DEBUGGER__
176
222
  @thread_stopper.disable
177
223
  @tp_load_script.disable
178
224
  @tp_thread_begin.disable
225
+ @tp_thread_end.disable
179
226
  @bps.each_value{|bp| bp.disable}
180
227
  @th_clients.each_value{|thc| thc.close}
181
228
  @tracers.values.each{|t| t.disable}
@@ -187,6 +234,16 @@ module DEBUGGER__
187
234
  def reset_ui ui
188
235
  @ui.deactivate
189
236
  @ui = ui
237
+
238
+ # activate new ui
239
+ @tp_thread_begin.disable
240
+ @tp_thread_end.disable
241
+ @ui.activate self
242
+ if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
243
+ thc.mark_as_management
244
+ end
245
+ @tp_thread_begin.enable
246
+ @tp_thread_end.enable
190
247
  end
191
248
 
192
249
  def pop_event
@@ -201,63 +258,81 @@ module DEBUGGER__
201
258
  deactivate
202
259
  end
203
260
 
261
+ def request_tc(req)
262
+ @tc << req
263
+ end
264
+
265
+ def request_tc_with_restarted_threads(req)
266
+ restart_all_threads
267
+ request_tc(req)
268
+ end
269
+
270
+ def request_eval type, src
271
+ request_tc_with_restarted_threads [:eval, type, src]
272
+ end
273
+
204
274
  def process_event evt
205
275
  # variable `@internal_info` is only used for test
206
276
  tc, output, ev, @internal_info, *ev_args = evt
207
- output.each{|str| @ui.puts str} if ev != :suspend
208
277
 
209
- case ev
278
+ output.each{|str| @ui.puts str} if ev != :suspend
210
279
 
211
- when :thread_begin # special event, tc is nil
280
+ # special event, tc is nil
281
+ # and we don't want to set @tc to the newly created thread's ThreadClient
282
+ if ev == :thread_begin
212
283
  th = ev_args.shift
213
284
  q = ev_args.shift
214
285
  on_thread_begin th
215
286
  q << true
216
287
 
217
- when :init
218
- wait_command_loop tc
288
+ return
289
+ end
219
290
 
291
+ @tc = tc
292
+
293
+ case ev
294
+ when :init
295
+ enter_subsession
296
+ wait_command_loop
220
297
  when :load
221
298
  iseq, src = ev_args
222
299
  on_load iseq, src
223
- @ui.event :load
224
- tc << :continue
300
+ request_tc :continue
225
301
 
226
302
  when :trace
227
303
  trace_id, msg = ev_args
228
304
  if t = @tracers.values.find{|t| t.object_id == trace_id}
229
305
  t.puts msg
230
306
  end
231
- tc << :continue
307
+ request_tc :continue
232
308
 
233
309
  when :suspend
234
310
  enter_subsession if ev_args.first != :replay
235
- output.each{|str| @ui.puts str}
311
+ output.each{|str| @ui.puts str} unless @ui.ignore_output_on_suspend?
236
312
 
237
313
  case ev_args.first
238
314
  when :breakpoint
239
315
  bp, i = bp_index ev_args[1]
240
316
  clean_bps unless bp
241
- @ui.event :suspend_bp, i, bp, tc.id
317
+ @ui.event :suspend_bp, i, bp, @tc.id
242
318
  when :trap
243
- @ui.event :suspend_trap, sig = ev_args[1], tc.id
319
+ @ui.event :suspend_trap, sig = ev_args[1], @tc.id
244
320
 
245
321
  if sig == :SIGINT && (@intercepted_sigint_cmd.kind_of?(Proc) || @intercepted_sigint_cmd.kind_of?(String))
246
322
  @ui.puts "#{@intercepted_sigint_cmd.inspect} is registered as SIGINT handler."
247
323
  @ui.puts "`sigint` command execute it."
248
324
  end
249
325
  else
250
- @ui.event :suspended, tc.id
326
+ @ui.event :suspended, @tc.id
251
327
  end
252
328
 
253
329
  if @displays.empty?
254
- wait_command_loop tc
330
+ wait_command_loop
255
331
  else
256
- tc << [:eval, :display, @displays]
332
+ request_eval :display, @displays
257
333
  end
258
-
259
334
  when :result
260
- raise "[BUG] not in subsession" unless @subsession
335
+ raise "[BUG] not in subsession" if @subsession_stack.empty?
261
336
 
262
337
  case ev_args.first
263
338
  when :try_display
@@ -269,6 +344,7 @@ module DEBUGGER__
269
344
  end
270
345
  end
271
346
 
347
+ stop_all_threads
272
348
  when :method_breakpoint, :watch_breakpoint
273
349
  bp = ev_args[1]
274
350
  if bp
@@ -282,18 +358,16 @@ module DEBUGGER__
282
358
  obj_inspect = ev_args[2]
283
359
  opt = ev_args[3]
284
360
  add_tracer ObjectTracer.new(@ui, obj_id, obj_inspect, **opt)
361
+ stop_all_threads
285
362
  else
286
- # ignore
363
+ stop_all_threads
287
364
  end
288
365
 
289
- wait_command_loop tc
366
+ wait_command_loop
290
367
 
291
- when :dap_result
292
- dap_event ev_args # server.rb
293
- wait_command_loop tc
294
- when :cdp_result
295
- cdp_event ev_args
296
- wait_command_loop tc
368
+ when :protocol_result
369
+ process_protocol_result ev_args
370
+ wait_command_loop
297
371
  end
298
372
  end
299
373
 
@@ -308,7 +382,7 @@ module DEBUGGER__
308
382
  if @preset_command && !@preset_command.commands.empty?
309
383
  @preset_command.commands += cs
310
384
  else
311
- @preset_command = PresetCommand.new(cs, name, continue)
385
+ @preset_command = PresetCommands.new(cs, name, continue)
312
386
  end
313
387
 
314
388
  ThreadClient.current.on_init name if kick
@@ -326,9 +400,7 @@ module DEBUGGER__
326
400
  "DEBUGGER__::SESSION"
327
401
  end
328
402
 
329
- def wait_command_loop tc
330
- @tc = tc
331
-
403
+ def wait_command_loop
332
404
  loop do
333
405
  case wait_command
334
406
  when :retry
@@ -369,7 +441,7 @@ module DEBUGGER__
369
441
  @ui.puts "(rdbg:#{@preset_command.source}) #{line}"
370
442
  end
371
443
  else
372
- @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE']
444
+ @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal'
373
445
  line = @ui.readline prompt
374
446
  end
375
447
 
@@ -383,97 +455,121 @@ module DEBUGGER__
383
455
  end
384
456
  end
385
457
 
386
- def process_command line
387
- if line.empty?
388
- if @repl_prev_line
389
- line = @repl_prev_line
390
- else
391
- return :retry
392
- end
393
- else
394
- @repl_prev_line = line
395
- end
396
-
397
- /([^\s]+)(?:\s+(.+))?/ =~ line
398
- cmd, arg = $1, $2
458
+ private def register_command *names,
459
+ repeat: false, unsafe: true, cancel_auto_continue: false, postmortem: true,
460
+ &b
461
+ cmd = SessionCommand.new(b, repeat, unsafe, cancel_auto_continue, postmortem)
399
462
 
400
- # p cmd: [cmd, *arg]
463
+ names.each{|name|
464
+ @commands[name] = cmd
465
+ }
466
+ end
401
467
 
402
- case cmd
468
+ def register_default_command
403
469
  ### Control flow
404
470
 
405
471
  # * `s[tep]`
406
472
  # * Step in. Resume the program until next breakable point.
407
473
  # * `s[tep] <n>`
408
474
  # * Step in, resume the program at `<n>`th breakable point.
409
- when 's', 'step'
410
- cancel_auto_continue
411
- check_postmortem
475
+ register_command 's', 'step',
476
+ repeat: true,
477
+ cancel_auto_continue: true,
478
+ postmortem: false do |arg|
412
479
  step_command :in, arg
480
+ end
413
481
 
414
482
  # * `n[ext]`
415
483
  # * Step over. Resume the program until next line.
416
484
  # * `n[ext] <n>`
417
485
  # * Step over, same as `step <n>`.
418
- when 'n', 'next'
419
- cancel_auto_continue
420
- check_postmortem
486
+ register_command 'n', 'next',
487
+ repeat: true,
488
+ cancel_auto_continue: true,
489
+ postmortem: false do |arg|
421
490
  step_command :next, arg
491
+ end
422
492
 
423
493
  # * `fin[ish]`
424
494
  # * Finish this frame. Resume the program until the current frame is finished.
425
495
  # * `fin[ish] <n>`
426
496
  # * Finish `<n>`th frames.
427
- when 'fin', 'finish'
428
- cancel_auto_continue
429
- check_postmortem
430
-
497
+ register_command 'fin', 'finish',
498
+ repeat: true,
499
+ cancel_auto_continue: true,
500
+ postmortem: false do |arg|
431
501
  if arg&.to_i == 0
432
502
  raise 'finish command with 0 does not make sense.'
433
503
  end
434
504
 
435
505
  step_command :finish, arg
506
+ end
436
507
 
437
- # * `c[ontinue]`
508
+ # * `u[ntil]`
509
+ # * Similar to `next` command, but only stop later lines or the end of the current frame.
510
+ # * Similar to gdb's `advance` command.
511
+ # * `u[ntil] <[file:]line>`
512
+ # * Run til the program reaches given location or the end of the current frame.
513
+ # * `u[ntil] <name>`
514
+ # * Run til the program invokes a method `<name>`. `<name>` can be a regexp with `/name/`.
515
+ register_command 'u', 'until',
516
+ repeat: true,
517
+ cancel_auto_continue: true,
518
+ postmortem: false do |arg|
519
+
520
+ step_command :until, arg
521
+ end
522
+
523
+ # * `c` or `cont` or `continue`
438
524
  # * Resume the program.
439
- when 'c', 'continue'
440
- cancel_auto_continue
525
+ register_command 'c', 'cont', 'continue',
526
+ repeat: true,
527
+ cancel_auto_continue: true do |arg|
441
528
  leave_subsession :continue
529
+ end
442
530
 
443
531
  # * `q[uit]` or `Ctrl-D`
444
532
  # * Finish debugger (with the debuggee process on non-remote debugging).
445
- when 'q', 'quit'
533
+ register_command 'q', 'quit' do |arg|
446
534
  if ask 'Really quit?'
447
- @ui.quit arg.to_i
535
+ @ui.quit arg.to_i do
536
+ request_tc :quit
537
+ end
448
538
  leave_subsession :continue
449
539
  else
450
- return :retry
540
+ next :retry
451
541
  end
542
+ end
452
543
 
453
544
  # * `q[uit]!`
454
545
  # * Same as q[uit] but without the confirmation prompt.
455
- when 'q!', 'quit!'
456
- @ui.quit arg.to_i
457
- leave_subsession nil
546
+ register_command 'q!', 'quit!', unsafe: false do |arg|
547
+ @ui.quit arg.to_i do
548
+ request_tc :quit
549
+ end
550
+ leave_subsession :continue
551
+ end
458
552
 
459
553
  # * `kill`
460
554
  # * Stop the debuggee process with `Kernel#exit!`.
461
- when 'kill'
555
+ register_command 'kill' do |arg|
462
556
  if ask 'Really kill?'
463
557
  exit! (arg || 1).to_i
464
558
  else
465
- return :retry
559
+ next :retry
466
560
  end
561
+ end
467
562
 
468
563
  # * `kill!`
469
564
  # * Same as kill but without the confirmation prompt.
470
- when 'kill!'
565
+ register_command 'kill!', unsafe: false do |arg|
471
566
  exit! (arg || 1).to_i
567
+ end
472
568
 
473
569
  # * `sigint`
474
570
  # * Execute SIGINT handler registered by the debuggee.
475
571
  # * Note that this command should be used just after stop by `SIGINT`.
476
- when 'sigint'
572
+ register_command 'sigint' do
477
573
  begin
478
574
  case cmd = @intercepted_sigint_cmd
479
575
  when nil, 'IGNORE', :IGNORE, 'DEFAULT', :DEFAULT
@@ -489,8 +585,9 @@ module DEBUGGER__
489
585
  rescue Exception => e
490
586
  @ui.puts "Exception: #{e}"
491
587
  @ui.puts e.backtrace.map{|line| " #{e}"}
492
- return :retry
588
+ next :retry
493
589
  end
590
+ end
494
591
 
495
592
  ### Breakpoint
496
593
 
@@ -510,53 +607,26 @@ module DEBUGGER__
510
607
  # * break and run `<command>` before stopping.
511
608
  # * `b[reak] ... do: <command>`
512
609
  # * break and run `<command>`, and continue.
513
- # * `b[reak] ... path: <path_regexp>`
514
- # * break if the triggering event's path matches <path_regexp>.
610
+ # * `b[reak] ... path: <path>`
611
+ # * break if the path matches to `<path>`. `<path>` can be a regexp with `/regexp/`.
515
612
  # * `b[reak] if: <expr>`
516
613
  # * break if: `<expr>` is true at any lines.
517
614
  # * Note that this feature is super slow.
518
- when 'b', 'break'
519
- check_postmortem
520
-
615
+ register_command 'b', 'break', postmortem: false, unsafe: false do |arg|
521
616
  if arg == nil
522
617
  show_bps
523
- return :retry
618
+ next :retry
524
619
  else
525
620
  case bp = repl_add_breakpoint(arg)
526
621
  when :noretry
527
622
  when nil
528
- return :retry
623
+ next :retry
529
624
  else
530
625
  show_bps bp
531
- return :retry
626
+ next :retry
532
627
  end
533
628
  end
534
-
535
- # skip
536
- when 'bv'
537
- check_postmortem
538
- require 'json'
539
-
540
- h = Hash.new{|h, k| h[k] = []}
541
- @bps.each_value{|bp|
542
- if LineBreakpoint === bp
543
- h[bp.path] << {lnum: bp.line}
544
- end
545
- }
546
- if h.empty?
547
- # TODO: clean?
548
- else
549
- open(".rdb_breakpoints.json", 'w'){|f| JSON.dump(h, f)}
550
- end
551
-
552
- vimsrc = File.join(__dir__, 'bp.vim')
553
- system("vim -R -S #{vimsrc} #{@tc.location.path}")
554
-
555
- if File.exist?(".rdb_breakpoints.json")
556
- pp JSON.load(File.read(".rdb_breakpoints.json"))
557
- end
558
-
559
- return :retry
629
+ end
560
630
 
561
631
  # * `catch <Error>`
562
632
  # * Set breakpoint on raising `<Error>`.
@@ -566,18 +636,18 @@ module DEBUGGER__
566
636
  # * runs `<command>` before stopping.
567
637
  # * `catch ... do: <command>`
568
638
  # * stops and run `<command>`, and continue.
569
- # * `catch ... path: <path_regexp>`
570
- # * stops if the exception is raised from a path that matches <path_regexp>.
571
- when 'catch'
572
- check_postmortem
573
-
639
+ # * `catch ... path: <path>`
640
+ # * stops if the exception is raised from a `<path>`. `<path>` can be a regexp with `/regexp/`.
641
+ register_command 'catch', postmortem: false, unsafe: false do |arg|
574
642
  if arg
575
643
  bp = repl_add_catch_breakpoint arg
576
644
  show_bps bp if bp
577
645
  else
578
646
  show_bps
579
647
  end
580
- return :retry
648
+
649
+ :retry
650
+ end
581
651
 
582
652
  # * `watch @ivar`
583
653
  # * Stop the execution when the result of current scope's `@ivar` is changed.
@@ -588,26 +658,22 @@ module DEBUGGER__
588
658
  # * runs `<command>` before stopping.
589
659
  # * `watch ... do: <command>`
590
660
  # * stops and run `<command>`, and continue.
591
- # * `watch ... path: <path_regexp>`
592
- # * stops if the triggering event's path matches <path_regexp>.
593
- when 'wat', 'watch'
594
- check_postmortem
595
-
661
+ # * `watch ... path: <path>`
662
+ # * stops if the path matches `<path>`. `<path>` can be a regexp with `/regexp/`.
663
+ register_command 'wat', 'watch', postmortem: false, unsafe: false do |arg|
596
664
  if arg && arg.match?(/\A@\w+/)
597
665
  repl_add_watch_breakpoint(arg)
598
666
  else
599
667
  show_bps
600
- return :retry
668
+ :retry
601
669
  end
670
+ end
602
671
 
603
672
  # * `del[ete]`
604
673
  # * delete all breakpoints.
605
674
  # * `del[ete] <bpnum>`
606
675
  # * delete specified breakpoint.
607
- when 'del', 'delete'
608
- check_postmortem
609
-
610
- bp =
676
+ register_command 'del', 'delete', postmortem: false, unsafe: false do |arg|
611
677
  case arg
612
678
  when nil
613
679
  show_bps
@@ -615,12 +681,13 @@ module DEBUGGER__
615
681
  delete_bp
616
682
  end
617
683
  when /\d+/
618
- delete_bp arg.to_i
684
+ bp = delete_bp arg.to_i
619
685
  else
620
686
  nil
621
687
  end
622
688
  @ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp
623
- return :retry
689
+ :retry
690
+ end
624
691
 
625
692
  ### Information
626
693
 
@@ -632,19 +699,20 @@ module DEBUGGER__
632
699
  # * Only shows frames with method name or location info that matches `/regexp/`.
633
700
  # * `bt <num> /regexp/` or `backtrace <num> /regexp/`
634
701
  # * Only shows first `<num>` frames with method name or location info that matches `/regexp/`.
635
- when 'bt', 'backtrace'
702
+ register_command 'bt', 'backtrace', unsafe: false do |arg|
636
703
  case arg
637
704
  when /\A(\d+)\z/
638
- @tc << [:show, :backtrace, arg.to_i, nil]
705
+ request_tc_with_restarted_threads [:show, :backtrace, arg.to_i, nil]
639
706
  when /\A\/(.*)\/\z/
640
707
  pattern = $1
641
- @tc << [:show, :backtrace, nil, Regexp.compile(pattern)]
708
+ request_tc_with_restarted_threads [:show, :backtrace, nil, Regexp.compile(pattern)]
642
709
  when /\A(\d+)\s+\/(.*)\/\z/
643
710
  max, pattern = $1, $2
644
- @tc << [:show, :backtrace, max.to_i, Regexp.compile(pattern)]
711
+ request_tc_with_restarted_threads [:show, :backtrace, max.to_i, Regexp.compile(pattern)]
645
712
  else
646
- @tc << [:show, :backtrace, nil, nil]
713
+ request_tc_with_restarted_threads [:show, :backtrace, nil, nil]
647
714
  end
715
+ end
648
716
 
649
717
  # * `l[ist]`
650
718
  # * Show current frame's source code.
@@ -653,57 +721,75 @@ module DEBUGGER__
653
721
  # * Show predecessor lines as opposed to the `list` command.
654
722
  # * `l[ist] <start>` or `l[ist] <start>-<end>`
655
723
  # * Show current frame's source code from the line <start> to <end> if given.
656
- when 'l', 'list'
724
+ register_command 'l', 'list', repeat: true, unsafe: false do |arg|
657
725
  case arg ? arg.strip : nil
658
726
  when /\A(\d+)\z/
659
- @tc << [:show, :list, {start_line: arg.to_i - 1}]
727
+ request_tc [:show, :list, {start_line: arg.to_i - 1}]
660
728
  when /\A-\z/
661
- @tc << [:show, :list, {dir: -1}]
729
+ request_tc [:show, :list, {dir: -1}]
662
730
  when /\A(\d+)-(\d+)\z/
663
- @tc << [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}]
731
+ request_tc [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}]
664
732
  when nil
665
- @tc << [:show, :list]
733
+ request_tc [:show, :list]
666
734
  else
667
735
  @ui.puts "Can not handle list argument: #{arg}"
668
- return :retry
736
+ :retry
669
737
  end
738
+ end
739
+
740
+ # * `whereami`
741
+ # * Show the current frame with source code.
742
+ register_command 'whereami', unsafe: false do
743
+ request_tc [:show, :whereami]
744
+ end
670
745
 
671
746
  # * `edit`
672
747
  # * Open the current file on the editor (use `EDITOR` environment variable).
673
748
  # * Note that edited file will not be reloaded.
674
749
  # * `edit <file>`
675
750
  # * Open <file> on the editor.
676
- when 'edit'
751
+ register_command 'edit' do |arg|
677
752
  if @ui.remote?
678
753
  @ui.puts "not supported on the remote console."
679
- return :retry
754
+ next :retry
680
755
  end
681
756
 
682
757
  begin
683
758
  arg = resolve_path(arg) if arg
684
759
  rescue Errno::ENOENT
685
760
  @ui.puts "not found: #{arg}"
686
- return :retry
761
+ next :retry
687
762
  end
688
763
 
689
- @tc << [:show, :edit, arg]
764
+ request_tc [:show, :edit, arg]
765
+ end
766
+
767
+ info_subcommands = nil
768
+ info_subcommands_abbrev = nil
690
769
 
691
770
  # * `i[nfo]`
692
- # * Show information about current frame (local/instance variables and defined constants).
693
- # * `i[nfo] l[ocal[s]]`
771
+ # * Show information about current frame (local/instance variables and defined constants).
772
+ # * `i[nfo]` <subcommand>
773
+ # * `info` has the following sub-commands.
774
+ # * Sub-commands can be specified with few letters which is unambiguous, like `l` for 'locals'.
775
+ # * `i[nfo] l or locals or local_variables`
694
776
  # * Show information about the current frame (local variables)
695
- # * It includes `self` as `%self` and a return value as `%return`.
696
- # * `i[nfo] i[var[s]]` or `i[nfo] instance`
777
+ # * It includes `self` as `%self` and a return value as `_return`.
778
+ # * `i[nfo] i or ivars or instance_variables`
697
779
  # * Show information about instance variables about `self`.
698
- # * `i[nfo] c[onst[s]]` or `i[nfo] constant[s]`
780
+ # * `info ivars <expr>` shows the instance variables of the result of `<expr>`.
781
+ # * `i[nfo] c or consts or constants`
699
782
  # * Show information about accessible constants except toplevel constants.
700
- # * `i[nfo] g[lobal[s]]`
783
+ # * `info consts <expr>` shows the constants of a class/module of the result of `<expr>`
784
+ # * `i[nfo] g or globals or global_variables`
701
785
  # * Show information about global variables
702
- # * `i[nfo] ... </pattern/>`
703
- # * Filter the output with `</pattern/>`.
704
- # * `i[nfo] th[read[s]]`
786
+ # * `i[nfo] th or threads`
705
787
  # * Show all threads (same as `th[read]`).
706
- when 'i', 'info'
788
+ # * `i[nfo] b or breakpoints or w or watchpoints`
789
+ # * Show all breakpoints and watchpoints.
790
+ # * `i[nfo] ... /regexp/`
791
+ # * Filter the output with `/regexp/`.
792
+ register_command 'i', 'info', unsafe: false do |arg|
707
793
  if /\/(.+)\/\z/ =~ arg
708
794
  pat = Regexp.compile($1)
709
795
  sub = $~.pre_match.strip
@@ -711,63 +797,98 @@ module DEBUGGER__
711
797
  sub = arg
712
798
  end
713
799
 
800
+ if /\A(.+?)\b(.+)/ =~ sub
801
+ sub = $1
802
+ opt = $2.strip
803
+ opt = nil if opt.empty?
804
+ end
805
+
806
+ if sub && !info_subcommands
807
+ info_subcommands = {
808
+ locals: %w[ locals local_variables ],
809
+ ivars: %w[ ivars instance_variables ],
810
+ consts: %w[ consts constants ],
811
+ globals:%w[ globals global_variables ],
812
+ threads:%w[ threads ],
813
+ breaks: %w[ breakpoints ],
814
+ watchs: %w[ watchpoints ],
815
+ }
816
+
817
+ require_relative 'abbrev_command'
818
+ info_subcommands_abbrev = AbbrevCommand.new(info_subcommands)
819
+ end
820
+
821
+ if sub
822
+ sub = info_subcommands_abbrev.search sub, :unknown do |candidates|
823
+ # note: unreached now
824
+ @ui.puts "Ambiguous command '#{sub}': #{candidates.join(' ')}"
825
+ end
826
+ end
827
+
714
828
  case sub
715
829
  when nil
716
- @tc << [:show, :default, pat] # something useful
717
- when 'l', /^locals?/
718
- @tc << [:show, :locals, pat]
719
- when 'i', /^ivars?/i, /^instance[_ ]variables?/i
720
- @tc << [:show, :ivars, pat]
721
- when 'c', /^consts?/i, /^constants?/i
722
- @tc << [:show, :consts, pat]
723
- when 'g', /^globals?/i, /^global[_ ]variables?/i
724
- @tc << [:show, :globals, pat]
725
- when 'th', /threads?/
830
+ request_tc_with_restarted_threads [:show, :default, pat] # something useful
831
+ when :locals
832
+ request_tc_with_restarted_threads [:show, :locals, pat]
833
+ when :ivars
834
+ request_tc_with_restarted_threads [:show, :ivars, pat, opt]
835
+ when :consts
836
+ request_tc_with_restarted_threads [:show, :consts, pat, opt]
837
+ when :globals
838
+ request_tc_with_restarted_threads [:show, :globals, pat]
839
+ when :threads
726
840
  thread_list
727
- return :retry
841
+ :retry
842
+ when :breaks, :watchs
843
+ show_bps
844
+ :retry
728
845
  else
729
846
  @ui.puts "unrecognized argument for info command: #{arg}"
730
847
  show_help 'info'
731
- return :retry
848
+ :retry
732
849
  end
850
+ end
733
851
 
734
852
  # * `o[utline]` or `ls`
735
853
  # * Show you available methods, constants, local variables, and instance variables in the current scope.
736
854
  # * `o[utline] <expr>` or `ls <expr>`
737
855
  # * Show you available methods and instance variables of the given object.
738
856
  # * If the object is a class/module, it also lists its constants.
739
- when 'outline', 'o', 'ls'
740
- @tc << [:show, :outline, arg]
857
+ register_command 'outline', 'o', 'ls', unsafe: false do |arg|
858
+ request_tc_with_restarted_threads [:show, :outline, arg]
859
+ end
741
860
 
742
861
  # * `display`
743
862
  # * Show display setting.
744
863
  # * `display <expr>`
745
864
  # * Show the result of `<expr>` at every suspended timing.
746
- when 'display'
865
+ register_command 'display', postmortem: false do |arg|
747
866
  if arg && !arg.empty?
748
867
  @displays << arg
749
- @tc << [:eval, :try_display, @displays]
868
+ request_eval :try_display, @displays
750
869
  else
751
- @tc << [:eval, :display, @displays]
870
+ request_eval :display, @displays
752
871
  end
872
+ end
753
873
 
754
874
  # * `undisplay`
755
875
  # * Remove all display settings.
756
876
  # * `undisplay <displaynum>`
757
877
  # * Remove a specified display setting.
758
- when 'undisplay'
878
+ register_command 'undisplay', postmortem: false, unsafe: false do |arg|
759
879
  case arg
760
880
  when /(\d+)/
761
881
  if @displays[n = $1.to_i]
762
882
  @displays.delete_at n
763
883
  end
764
- @tc << [:eval, :display, @displays]
884
+ request_eval :display, @displays
765
885
  when nil
766
886
  if ask "clear all?", 'N'
767
887
  @displays.clear
768
888
  end
769
- return :retry
889
+ :retry
770
890
  end
891
+ end
771
892
 
772
893
  ### Frame control
773
894
 
@@ -775,53 +896,59 @@ module DEBUGGER__
775
896
  # * Show the current frame.
776
897
  # * `f[rame] <framenum>`
777
898
  # * Specify a current frame. Evaluation are run on specified frame.
778
- when 'frame', 'f'
779
- @tc << [:frame, :set, arg]
899
+ register_command 'frame', 'f', unsafe: false do |arg|
900
+ request_tc [:frame, :set, arg]
901
+ end
780
902
 
781
903
  # * `up`
782
904
  # * Specify the upper frame.
783
- when 'up'
784
- @tc << [:frame, :up]
905
+ register_command 'up', repeat: true, unsafe: false do |arg|
906
+ request_tc [:frame, :up]
907
+ end
785
908
 
786
909
  # * `down`
787
910
  # * Specify the lower frame.
788
- when 'down'
789
- @tc << [:frame, :down]
911
+ register_command 'down', repeat: true, unsafe: false do |arg|
912
+ request_tc [:frame, :down]
913
+ end
790
914
 
791
915
  ### Evaluate
792
916
 
793
917
  # * `p <expr>`
794
918
  # * Evaluate like `p <expr>` on the current frame.
795
- when 'p'
796
- @tc << [:eval, :p, arg.to_s]
919
+ register_command 'p' do |arg|
920
+ request_eval :p, arg.to_s
921
+ end
797
922
 
798
923
  # * `pp <expr>`
799
924
  # * Evaluate like `pp <expr>` on the current frame.
800
- when 'pp'
801
- @tc << [:eval, :pp, arg.to_s]
925
+ register_command 'pp' do |arg|
926
+ request_eval :pp, arg.to_s
927
+ end
802
928
 
803
929
  # * `eval <expr>`
804
930
  # * Evaluate `<expr>` on the current frame.
805
- when 'eval', 'call'
931
+ register_command 'eval', 'call' do |arg|
806
932
  if arg == nil || arg.empty?
807
933
  show_help 'eval'
808
934
  @ui.puts "\nTo evaluate the variable `#{cmd}`, use `pp #{cmd}` instead."
809
- return :retry
935
+ :retry
810
936
  else
811
- @tc << [:eval, :call, arg]
937
+ request_eval :call, arg
812
938
  end
939
+ end
813
940
 
814
941
  # * `irb`
815
- # * Invoke `irb` on the current frame.
816
- when 'irb'
942
+ # * Activate and switch to `irb:rdbg` console
943
+ register_command 'irb' do |arg|
817
944
  if @ui.remote?
818
- @ui.puts "not supported on the remote console."
819
- return :retry
945
+ @ui.puts "\nIRB is not supported on the remote console."
946
+ else
947
+ config_set :irb_console, true
820
948
  end
821
- @tc << [:eval, :irb]
822
949
 
823
- # don't repeat irb command
824
- @repl_prev_line = nil
950
+ :retry
951
+ end
825
952
 
826
953
  ### Trace
827
954
  # * `trace`
@@ -834,15 +961,15 @@ module DEBUGGER__
834
961
  # * Add an exception tracer. It indicates raising exceptions.
835
962
  # * `trace object <expr>`
836
963
  # * Add an object tracer. It indicates that an object by `<expr>` is passed as a parameter or a receiver on method call.
837
- # * `trace ... </pattern/>`
838
- # * Indicates only matched events to `</pattern/>` (RegExp).
964
+ # * `trace ... /regexp/`
965
+ # * Indicates only matched events to `/regexp/`.
839
966
  # * `trace ... into: <file>`
840
967
  # * Save trace information into: `<file>`.
841
968
  # * `trace off <num>`
842
969
  # * Disable tracer specified by `<num>` (use `trace` command to check the numbers).
843
970
  # * `trace off [line|call|pass]`
844
971
  # * Disable all tracers. If `<type>` is provided, disable specified type tracers.
845
- when 'trace'
972
+ register_command 'trace', postmortem: false, unsafe: false do |arg|
846
973
  if (re = /\s+into:\s*(.+)/) =~ arg
847
974
  into = $1
848
975
  arg.sub!(re, '')
@@ -860,22 +987,22 @@ module DEBUGGER__
860
987
  @ui.puts "* \##{i} #{t}"
861
988
  }
862
989
  @ui.puts
863
- return :retry
990
+ :retry
864
991
 
865
992
  when /\Aline\z/
866
993
  add_tracer LineTracer.new(@ui, pattern: pattern, into: into)
867
- return :retry
994
+ :retry
868
995
 
869
996
  when /\Acall\z/
870
997
  add_tracer CallTracer.new(@ui, pattern: pattern, into: into)
871
- return :retry
998
+ :retry
872
999
 
873
1000
  when /\Aexception\z/
874
1001
  add_tracer ExceptionTracer.new(@ui, pattern: pattern, into: into)
875
- return :retry
1002
+ :retry
876
1003
 
877
1004
  when /\Aobject\s+(.+)/
878
- @tc << [:trace, :object, $1.strip, {pattern: pattern, into: into}]
1005
+ request_tc_with_restarted_threads [:trace, :object, $1.strip, {pattern: pattern, into: into}]
879
1006
 
880
1007
  when /\Aoff\s+(\d+)\z/
881
1008
  if t = @tracers.values[$1.to_i]
@@ -884,7 +1011,7 @@ module DEBUGGER__
884
1011
  else
885
1012
  @ui.puts "Unmatched: #{$1}"
886
1013
  end
887
- return :retry
1014
+ :retry
888
1015
 
889
1016
  when /\Aoff(\s+(line|call|exception|object))?\z/
890
1017
  @tracers.values.each{|t|
@@ -893,12 +1020,13 @@ module DEBUGGER__
893
1020
  @ui.puts "Disable #{t.to_s}"
894
1021
  end
895
1022
  }
896
- return :retry
1023
+ :retry
897
1024
 
898
1025
  else
899
1026
  @ui.puts "Unknown trace option: #{arg.inspect}"
900
- return :retry
1027
+ :retry
901
1028
  end
1029
+ end
902
1030
 
903
1031
  # Record
904
1032
  # * `record`
@@ -910,14 +1038,15 @@ module DEBUGGER__
910
1038
  # * `s[tep]` does stepping forward with the last log.
911
1039
  # * `step reset`
912
1040
  # * Stop replay .
913
- when 'record'
1041
+ register_command 'record', postmortem: false, unsafe: false do |arg|
914
1042
  case arg
915
1043
  when nil, 'on', 'off'
916
- @tc << [:record, arg&.to_sym]
1044
+ request_tc [:record, arg&.to_sym]
917
1045
  else
918
1046
  @ui.puts "unknown command: #{arg}"
919
- return :retry
1047
+ :retry
920
1048
  end
1049
+ end
921
1050
 
922
1051
  ### Thread control
923
1052
 
@@ -925,7 +1054,7 @@ module DEBUGGER__
925
1054
  # * Show all threads.
926
1055
  # * `th[read] <thnum>`
927
1056
  # * Switch thread specified by `<thnum>`.
928
- when 'th', 'thread'
1057
+ register_command 'th', 'thread', unsafe: false do |arg|
929
1058
  case arg
930
1059
  when nil, 'list', 'l'
931
1060
  thread_list
@@ -934,7 +1063,8 @@ module DEBUGGER__
934
1063
  else
935
1064
  @ui.puts "unknown thread command: #{arg}"
936
1065
  end
937
- return :retry
1066
+ :retry
1067
+ end
938
1068
 
939
1069
  ### Configuration
940
1070
  # * `config`
@@ -947,13 +1077,14 @@ module DEBUGGER__
947
1077
  # * Append `<val>` to `<name>` if it is an array.
948
1078
  # * `config unset <name>`
949
1079
  # * Set <name> to default.
950
- when 'config'
1080
+ register_command 'config', unsafe: false do |arg|
951
1081
  config_command arg
952
- return :retry
1082
+ :retry
1083
+ end
953
1084
 
954
1085
  # * `source <file>`
955
1086
  # * Evaluate lines in `<file>` as debug commands.
956
- when 'source'
1087
+ register_command 'source' do |arg|
957
1088
  if arg
958
1089
  begin
959
1090
  cmds = File.readlines(path = File.expand_path(arg))
@@ -964,7 +1095,8 @@ module DEBUGGER__
964
1095
  else
965
1096
  show_help 'source'
966
1097
  end
967
- return :retry
1098
+ :retry
1099
+ end
968
1100
 
969
1101
  # * `open`
970
1102
  # * open debuggee port on UNIX domain socket and wait for attaching.
@@ -975,26 +1107,28 @@ module DEBUGGER__
975
1107
  # * open debuggee port for VSCode and launch VSCode if available.
976
1108
  # * `open chrome`
977
1109
  # * open debuggee port for Chrome and wait for attaching.
978
- when 'open'
1110
+ register_command 'open' do |arg|
979
1111
  case arg&.downcase
980
1112
  when '', nil
981
- repl_open_unix
982
- when 'vscode'
983
- repl_open_vscode
984
- when /\A(.+):(\d+)\z/
985
- repl_open_tcp $1, $2.to_i
1113
+ ::DEBUGGER__.open nonstop: true
986
1114
  when /\A(\d+)z/
987
- repl_open_tcp nil, $1.to_i
1115
+ ::DEBUGGER__.open_tcp host: nil, port: $1.to_i, nonstop: true
1116
+ when /\A(.+):(\d+)\z/
1117
+ ::DEBUGGER__.open_tcp host: $1, port: $2.to_i, nonstop: true
988
1118
  when 'tcp'
989
- repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0)
1119
+ ::DEBUGGER__.open_tcp host: CONFIG[:host], port: (CONFIG[:port] || 0), nonstop: true
1120
+ when 'vscode'
1121
+ CONFIG[:open] = 'vscode'
1122
+ ::DEBUGGER__.open nonstop: true
990
1123
  when 'chrome', 'cdp'
991
- CONFIG[:open_frontend] = 'chrome'
992
- repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0)
1124
+ CONFIG[:open] = 'chrome'
1125
+ ::DEBUGGER__.open_tcp host: CONFIG[:host], port: (CONFIG[:port] || 0), nonstop: true
993
1126
  else
994
1127
  raise "Unknown arg: #{arg}"
995
1128
  end
996
1129
 
997
- return :retry
1130
+ :retry
1131
+ end
998
1132
 
999
1133
  ### Help
1000
1134
 
@@ -1002,30 +1136,38 @@ module DEBUGGER__
1002
1136
  # * Show help for all commands.
1003
1137
  # * `h[elp] <command>`
1004
1138
  # * Show help for the given command.
1005
- when 'h', 'help', '?'
1006
- if arg
1007
- show_help arg
1139
+ register_command 'h', 'help', '?', unsafe: false do |arg|
1140
+ show_help arg
1141
+ :retry
1142
+ end
1143
+ end
1144
+
1145
+ def process_command line
1146
+ if line.empty?
1147
+ if @repl_prev_line
1148
+ line = @repl_prev_line
1008
1149
  else
1009
- @ui.puts DEBUGGER__.help
1150
+ return :retry
1010
1151
  end
1011
- return :retry
1152
+ else
1153
+ @repl_prev_line = line
1154
+ end
1012
1155
 
1013
- ### END
1156
+ /([^\s]+)(?:\s+(.+))?/ =~ line
1157
+ cmd_name, cmd_arg = $1, $2
1158
+
1159
+ if cmd = @commands[cmd_name]
1160
+ check_postmortem if !cmd.postmortem
1161
+ check_unsafe if cmd.unsafe
1162
+ cancel_auto_continue if cmd.cancel_auto_continue
1163
+ @repl_prev_line = nil if !cmd.repeat
1164
+
1165
+ cmd.block.call(cmd_arg)
1014
1166
  else
1015
- @tc << [:eval, :pp, line]
1016
- =begin
1017
1167
  @repl_prev_line = nil
1018
- @ui.puts "unknown command: #{line}"
1019
- begin
1020
- require 'did_you_mean'
1021
- spell_checker = DidYouMean::SpellChecker.new(dictionary: DEBUGGER__.commands)
1022
- correction = spell_checker.correct(line.split(/\s/).first || '')
1023
- @ui.puts "Did you mean? #{correction.join(' or ')}" unless correction.empty?
1024
- rescue LoadError
1025
- # Don't use D
1026
- end
1027
- return :retry
1028
- =end
1168
+ check_unsafe
1169
+
1170
+ request_eval :pp, line
1029
1171
  end
1030
1172
 
1031
1173
  rescue Interrupt
@@ -1041,44 +1183,27 @@ module DEBUGGER__
1041
1183
  return :retry
1042
1184
  end
1043
1185
 
1044
- def repl_open_setup
1045
- @tp_thread_begin.disable
1046
- @ui.activate self
1047
- if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
1048
- thc.mark_as_management
1186
+ def step_command type, arg
1187
+ if type == :until
1188
+ leave_subsession [:step, type, arg]
1189
+ return
1049
1190
  end
1050
- @tp_thread_begin.enable
1051
- end
1052
1191
 
1053
- def repl_open_tcp host, port, **kw
1054
- DEBUGGER__.open_tcp host: host, port: port, nonstop: true, **kw
1055
- repl_open_setup
1056
- end
1057
-
1058
- def repl_open_unix
1059
- DEBUGGER__.open_unix nonstop: true
1060
- repl_open_setup
1061
- end
1062
-
1063
- def repl_open_vscode
1064
- CONFIG[:open_frontend] = 'vscode'
1065
- repl_open_unix
1066
- end
1067
-
1068
- def step_command type, arg
1069
1192
  case arg
1070
1193
  when nil, /\A\d+\z/
1071
1194
  if type == :in && @tc.recorder&.replaying?
1072
- @tc << [:step, type, arg&.to_i]
1195
+ request_tc [:step, type, arg&.to_i]
1073
1196
  else
1074
1197
  leave_subsession [:step, type, arg&.to_i]
1075
1198
  end
1076
- when /\Aback\z/, /\Areset\z/
1199
+ when /\A(back)\z/, /\A(back)\s+(\d+)\z/, /\A(reset)\z/
1077
1200
  if type != :in
1078
1201
  @ui.puts "only `step #{arg}` is supported."
1079
1202
  :retry
1080
1203
  else
1081
- @tc << [:step, arg.to_sym]
1204
+ type = $1.to_sym
1205
+ iter = $2&.to_i
1206
+ request_tc [:step, type, iter]
1082
1207
  end
1083
1208
  else
1084
1209
  @ui.puts "Unknown option: #{arg}"
@@ -1088,11 +1213,18 @@ module DEBUGGER__
1088
1213
 
1089
1214
  def config_show key
1090
1215
  key = key.to_sym
1091
- if CONFIG_SET[key]
1216
+ config_detail = CONFIG_SET[key]
1217
+
1218
+ if config_detail
1092
1219
  v = CONFIG[key]
1093
- kv = "#{key} = #{v.nil? ? '(default)' : v.inspect}"
1094
- desc = CONFIG_SET[key][1]
1095
- line = "%-30s \# %s" % [kv, desc]
1220
+ kv = "#{key} = #{v.inspect}"
1221
+ desc = config_detail[1]
1222
+
1223
+ if config_default = config_detail[3]
1224
+ desc += " (default: #{config_default})"
1225
+ end
1226
+
1227
+ line = "%-34s \# %s" % [kv, desc]
1096
1228
  if line.size > SESSION.width
1097
1229
  @ui.puts "\# #{desc}\n#{kv}"
1098
1230
  else
@@ -1142,7 +1274,7 @@ module DEBUGGER__
1142
1274
  config_set $1, $2, append: true
1143
1275
 
1144
1276
  when /\A\s*append\s+(\w+)\s+(.+)\z/
1145
- config_set $1, $2
1277
+ config_set $1, $2, append: true
1146
1278
 
1147
1279
  when /\A(\w+)\z/
1148
1280
  config_show $1
@@ -1159,16 +1291,50 @@ module DEBUGGER__
1159
1291
  end
1160
1292
  end
1161
1293
 
1162
- def show_help arg
1163
- DEBUGGER__.helps.each{|cat, cs|
1164
- cs.each{|ws, desc|
1165
- if ws.include? arg
1166
- @ui.puts desc
1167
- return
1294
+ def show_help arg = nil
1295
+ instructions = (DEBUGGER__.commands.keys + DEBUGGER__.commands.values).uniq
1296
+ print_instructions = proc do |desc|
1297
+ desc.split("\n").each do |line|
1298
+ next if line.start_with?(" ") # workaround for step back
1299
+ formatted_line = line.gsub(/[\[\]\*]/, "").strip
1300
+ instructions.each do |inst|
1301
+ if formatted_line.start_with?("`#{inst}")
1302
+ desc.sub!(line, colorize(line, [:CYAN, :BOLD]))
1303
+ end
1304
+ end
1305
+ end
1306
+ @ui.puts desc
1307
+ end
1308
+
1309
+ print_category = proc do |cat|
1310
+ @ui.puts "\n"
1311
+ @ui.puts colorize("### #{cat}", [:GREEN, :BOLD])
1312
+ @ui.puts "\n"
1313
+ end
1314
+
1315
+ DEBUGGER__.helps.each { |cat, cs|
1316
+ # categories
1317
+ if arg.nil?
1318
+ print_category.call(cat)
1319
+ else
1320
+ cs.each { |ws, _|
1321
+ if ws.include?(arg)
1322
+ print_category.call(cat)
1323
+ break
1324
+ end
1325
+ }
1326
+ end
1327
+
1328
+ # instructions
1329
+ cs.each { |ws, desc|
1330
+ if arg.nil? || ws.include?(arg)
1331
+ print_instructions.call(desc.dup)
1332
+ return if arg
1168
1333
  end
1169
1334
  }
1170
1335
  }
1171
- @ui.puts "not found: #{arg}"
1336
+
1337
+ @ui.puts "not found: #{arg}" if arg
1172
1338
  end
1173
1339
 
1174
1340
  def ask msg, default = 'Y'
@@ -1231,12 +1397,13 @@ module DEBUGGER__
1231
1397
 
1232
1398
  def add_bp bp
1233
1399
  # don't repeat commands that add breakpoints
1234
- @repl_prev_line = nil
1235
-
1236
1400
  if @bps.has_key? bp.key
1237
- unless bp.duplicable?
1401
+ if bp.duplicable?
1402
+ bp
1403
+ else
1238
1404
  @ui.puts "duplicated breakpoint: #{bp}"
1239
1405
  bp.disable
1406
+ nil
1240
1407
  end
1241
1408
  else
1242
1409
  @bps[bp.key] = bp
@@ -1261,7 +1428,7 @@ module DEBUGGER__
1261
1428
 
1262
1429
  BREAK_KEYWORDS = %w(if: do: pre: path:).freeze
1263
1430
 
1264
- def parse_break arg
1431
+ private def parse_break type, arg
1265
1432
  mode = :sig
1266
1433
  expr = Hash.new{|h, k| h[k] = []}
1267
1434
  arg.split(' ').each{|w|
@@ -1272,14 +1439,25 @@ module DEBUGGER__
1272
1439
  end
1273
1440
  }
1274
1441
  expr.default_proc = nil
1275
- expr.transform_values{|v| v.join(' ')}
1442
+ expr = expr.transform_values{|v| v.join(' ')}
1443
+
1444
+ if (path = expr[:path]) && path =~ /\A\/(.*)\/\z/
1445
+ expr[:path] = Regexp.compile($1)
1446
+ end
1447
+
1448
+ if expr[:do] || expr[:pre]
1449
+ check_unsafe
1450
+ expr[:cmd] = [type, expr[:pre], expr[:do]]
1451
+ end
1452
+
1453
+ expr
1276
1454
  end
1277
1455
 
1278
1456
  def repl_add_breakpoint arg
1279
- expr = parse_break arg.strip
1457
+ expr = parse_break 'break', arg.strip
1280
1458
  cond = expr[:if]
1281
- cmd = ['break', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1282
- path = Regexp.compile(expr[:path]) if expr[:path]
1459
+ cmd = expr[:cmd]
1460
+ path = expr[:path]
1283
1461
 
1284
1462
  case expr[:sig]
1285
1463
  when /\A(\d+)\z/
@@ -1287,10 +1465,10 @@ module DEBUGGER__
1287
1465
  when /\A(.+)[:\s+](\d+)\z/
1288
1466
  add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
1289
1467
  when /\A(.+)([\.\#])(.+)\z/
1290
- @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd, path]
1468
+ request_tc [:breakpoint, :method, $1, $2, $3, cond, cmd, path]
1291
1469
  return :noretry
1292
1470
  when nil
1293
- add_check_breakpoint cond, path
1471
+ add_check_breakpoint cond, path, cmd
1294
1472
  else
1295
1473
  @ui.puts "Unknown breakpoint format: #{arg}"
1296
1474
  @ui.puts
@@ -1299,31 +1477,31 @@ module DEBUGGER__
1299
1477
  end
1300
1478
 
1301
1479
  def repl_add_catch_breakpoint arg
1302
- expr = parse_break arg.strip
1480
+ expr = parse_break 'catch', arg.strip
1303
1481
  cond = expr[:if]
1304
- cmd = ['catch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1305
- path = Regexp.compile(expr[:path]) if expr[:path]
1482
+ cmd = expr[:cmd]
1483
+ path = expr[:path]
1306
1484
 
1307
1485
  bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd, path: path)
1308
1486
  add_bp bp
1309
1487
  end
1310
1488
 
1311
1489
  def repl_add_watch_breakpoint arg
1312
- expr = parse_break arg.strip
1490
+ expr = parse_break 'watch', arg.strip
1313
1491
  cond = expr[:if]
1314
- cmd = ['watch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
1492
+ cmd = expr[:cmd]
1315
1493
  path = Regexp.compile(expr[:path]) if expr[:path]
1316
1494
 
1317
- @tc << [:breakpoint, :watch, expr[:sig], cond, cmd, path]
1495
+ request_tc [:breakpoint, :watch, expr[:sig], cond, cmd, path]
1318
1496
  end
1319
1497
 
1320
- def add_catch_breakpoint pat
1321
- bp = CatchBreakpoint.new(pat)
1498
+ def add_catch_breakpoint pat, cond: nil
1499
+ bp = CatchBreakpoint.new(pat, cond: cond)
1322
1500
  add_bp bp
1323
1501
  end
1324
1502
 
1325
- def add_check_breakpoint expr, path
1326
- bp = CheckBreakpoint.new(expr, path)
1503
+ def add_check_breakpoint cond, path, command
1504
+ bp = CheckBreakpoint.new(cond: cond, path: path, command: command)
1327
1505
  add_bp bp
1328
1506
  end
1329
1507
 
@@ -1336,6 +1514,34 @@ module DEBUGGER__
1336
1514
  @ui.puts e.message
1337
1515
  end
1338
1516
 
1517
+ def clear_breakpoints(&condition)
1518
+ @bps.delete_if do |k, bp|
1519
+ if condition.call(k, bp)
1520
+ bp.delete
1521
+ true
1522
+ end
1523
+ end
1524
+ end
1525
+
1526
+ def clear_line_breakpoints path
1527
+ path = resolve_path(path)
1528
+ clear_breakpoints do |k, bp|
1529
+ bp.is_a?(LineBreakpoint) && bp.path_is?(path)
1530
+ end
1531
+ rescue Errno::ENOENT
1532
+ # just ignore
1533
+ end
1534
+
1535
+ def clear_catch_breakpoints *exception_names
1536
+ clear_breakpoints do |k, bp|
1537
+ bp.is_a?(CatchBreakpoint) && exception_names.include?(k[1])
1538
+ end
1539
+ end
1540
+
1541
+ def clear_all_breakpoints
1542
+ clear_breakpoints{true}
1543
+ end
1544
+
1339
1545
  def add_iseq_breakpoint iseq, **kw
1340
1546
  bp = ISeqBreakpoint.new(iseq, [:line], **kw)
1341
1547
  add_bp bp
@@ -1344,9 +1550,7 @@ module DEBUGGER__
1344
1550
  # tracers
1345
1551
 
1346
1552
  def add_tracer tracer
1347
- # don't repeat commands that add tracers
1348
- @repl_prev_line = nil
1349
- if @tracers.has_key? tracer.key
1553
+ if @tracers[tracer.key]&.enabled?
1350
1554
  tracer.disable
1351
1555
  @ui.puts "Duplicated tracer: #{tracer}"
1352
1556
  else
@@ -1503,42 +1707,72 @@ module DEBUGGER__
1503
1707
  end
1504
1708
 
1505
1709
  private def enter_subsession
1506
- raise "already in subsession" if @subsession
1507
- @subsession = true
1508
- stop_all_threads
1509
- @process_group.lock
1510
- DEBUGGER__.info "enter_subsession"
1710
+ @subsession_id += 1
1711
+ if !@subsession_stack.empty?
1712
+ DEBUGGER__.debug{ "Enter subsession (nested #{@subsession_stack.size})" }
1713
+ else
1714
+ DEBUGGER__.debug{ "Enter subsession" }
1715
+ stop_all_threads
1716
+ @process_group.lock
1717
+ end
1718
+
1719
+ @subsession_stack << true
1511
1720
  end
1512
1721
 
1513
1722
  private def leave_subsession type
1514
- DEBUGGER__.info "leave_subsession"
1515
- @process_group.unlock
1516
- restart_all_threads
1517
- @tc << type if type
1723
+ raise '[BUG] leave_subsession: not entered' if @subsession_stack.empty?
1724
+ @subsession_stack.pop
1725
+
1726
+ if @subsession_stack.empty?
1727
+ DEBUGGER__.debug{ "Leave subsession" }
1728
+ @process_group.unlock
1729
+ restart_all_threads
1730
+ else
1731
+ DEBUGGER__.debug{ "Leave subsession (nested #{@subsession_stack.size})" }
1732
+ end
1733
+
1734
+ request_tc type if type
1518
1735
  @tc = nil
1519
- @subsession = false
1520
1736
  rescue Exception => e
1521
- STDERR.puts [e, e.backtrace].inspect
1737
+ STDERR.puts PP.pp([e, e.backtrace], ''.dup)
1522
1738
  raise
1523
1739
  end
1524
1740
 
1525
1741
  def in_subsession?
1526
- @subsession
1742
+ !@subsession_stack.empty?
1527
1743
  end
1528
1744
 
1529
1745
  ## event
1530
1746
 
1531
1747
  def on_load iseq, src
1532
1748
  DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}"
1533
- @sr.add iseq, src
1534
-
1535
- pending_line_breakpoints = @bps.find_all do |key, bp|
1536
- LineBreakpoint === bp && !bp.iseq
1537
- end
1538
1749
 
1539
- pending_line_breakpoints.each do |_key, bp|
1540
- if bp.path == (iseq.absolute_path || iseq.path)
1541
- bp.try_activate
1750
+ file_path, reloaded = @sr.add(iseq, src)
1751
+ @ui.event :load, file_path, reloaded
1752
+
1753
+ # check breakpoints
1754
+ if file_path
1755
+ @bps.find_all do |_key, bp|
1756
+ LineBreakpoint === bp && bp.path_is?(file_path) && (iseq.first_lineno..iseq.last_line).cover?(bp.line)
1757
+ end.each do |_key, bp|
1758
+ if !bp.iseq
1759
+ bp.try_activate iseq
1760
+ elsif reloaded
1761
+ @bps.delete bp.key # to allow duplicate
1762
+
1763
+ # When we delete a breakpoint from the @bps hash, we also need to deactivate it or else its tracepoint event
1764
+ # will continue to be enabled and we'll suspend on ghost breakpoints
1765
+ bp.delete
1766
+
1767
+ nbp = LineBreakpoint.copy(bp, iseq)
1768
+ add_bp nbp
1769
+ end
1770
+ end
1771
+ else # !file_path => file_path is not existing
1772
+ @bps.find_all do |_key, bp|
1773
+ LineBreakpoint === bp && !bp.iseq && DEBUGGER__.compare_path(bp.path, (iseq.absolute_path || iseq.path))
1774
+ end.each do |_key, bp|
1775
+ bp.try_activate iseq
1542
1776
  end
1543
1777
  end
1544
1778
  end
@@ -1563,9 +1797,10 @@ module DEBUGGER__
1563
1797
 
1564
1798
  def method_added tp
1565
1799
  b = tp.binding
1800
+
1566
1801
  if var_name = b.local_variables.first
1567
1802
  mid = b.local_variable_get(var_name)
1568
- unresolved = false
1803
+ resolved = true
1569
1804
 
1570
1805
  @bps.each{|k, bp|
1571
1806
  case bp
@@ -1576,15 +1811,57 @@ module DEBUGGER__
1576
1811
  end
1577
1812
  end
1578
1813
 
1579
- unresolved = true unless bp.enabled?
1814
+ resolved = false if !bp.enabled?
1580
1815
  end
1581
1816
  }
1582
- unless unresolved
1583
- METHOD_ADDED_TRACKER.disable
1817
+
1818
+ if resolved
1819
+ Session.deactivate_method_added_trackers
1820
+ end
1821
+
1822
+ case mid
1823
+ when :method_added, :singleton_method_added
1824
+ Session.create_method_added_tracker(tp.self, mid)
1825
+ Session.activate_method_added_trackers unless resolved
1584
1826
  end
1585
1827
  end
1586
1828
  end
1587
1829
 
1830
+ class ::Module
1831
+ undef method_added
1832
+ def method_added mid; end
1833
+ end
1834
+
1835
+ class ::BasicObject
1836
+ undef singleton_method_added
1837
+ def singleton_method_added mid; end
1838
+ end
1839
+
1840
+ def self.create_method_added_tracker mod, method_added_id, method_accessor = :method
1841
+ m = mod.__send__(method_accessor, method_added_id)
1842
+ METHOD_ADDED_TRACKERS[m] = TracePoint.new(:call) do |tp|
1843
+ SESSION.method_added tp
1844
+ end
1845
+ end
1846
+
1847
+ def self.activate_method_added_trackers
1848
+ METHOD_ADDED_TRACKERS.each do |m, tp|
1849
+ tp.enable(target: m) unless tp.enabled?
1850
+ rescue ArgumentError
1851
+ DEBUGGER__.warn "Methods defined under #{m.owner} can not track by the debugger."
1852
+ end
1853
+ end
1854
+
1855
+ def self.deactivate_method_added_trackers
1856
+ METHOD_ADDED_TRACKERS.each do |m, tp|
1857
+ tp.disable if tp.enabled?
1858
+ end
1859
+ end
1860
+
1861
+ METHOD_ADDED_TRACKERS = Hash.new
1862
+ create_method_added_tracker Module, :method_added, :instance_method
1863
+ create_method_added_tracker BasicObject, :singleton_method_added, :instance_method
1864
+
1588
1865
  def width
1589
1866
  @ui.width
1590
1867
  end
@@ -1595,6 +1872,18 @@ module DEBUGGER__
1595
1872
  end
1596
1873
  end
1597
1874
 
1875
+ def check_unsafe
1876
+ if @unsafe_context
1877
+ raise RuntimeError, "#{@repl_prev_line.dump} is not allowed on unsafe context."
1878
+ end
1879
+ end
1880
+
1881
+ def activate_irb_integration
1882
+ require_relative "irb_integration"
1883
+ thc = get_thread_client(@session_server)
1884
+ thc.activate_irb_integration
1885
+ end
1886
+
1598
1887
  def enter_postmortem_session exc
1599
1888
  return unless exc.instance_variable_defined? :@__debugger_postmortem_frames
1600
1889
 
@@ -1674,6 +1963,17 @@ module DEBUGGER__
1674
1963
  end
1675
1964
  end
1676
1965
 
1966
+ def set_no_sigint_hook old, new
1967
+ return unless old != new
1968
+ return unless @ui.respond_to? :activate_sigint
1969
+
1970
+ if old # no -> yes
1971
+ @ui.activate_sigint
1972
+ else
1973
+ @ui.deactivate_sigint
1974
+ end
1975
+ end
1976
+
1677
1977
  def save_int_trap cmd
1678
1978
  prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, cmd
1679
1979
  prev
@@ -1717,6 +2017,13 @@ module DEBUGGER__
1717
2017
  def after_fork_parent
1718
2018
  @ui.after_fork_parent
1719
2019
  end
2020
+
2021
+ # experimental API
2022
+ def extend_feature session: nil, thread_client: nil, ui: nil
2023
+ Session.include session if session
2024
+ ThreadClient.include thread_client if thread_client
2025
+ @ui.extend ui if ui
2026
+ end
1720
2027
  end
1721
2028
 
1722
2029
  class ProcessGroup
@@ -1765,9 +2072,11 @@ module DEBUGGER__
1765
2072
 
1766
2073
  def after_fork child: true
1767
2074
  if child || !@lock_file
1768
- @m = Mutex.new
1769
- @lock_level = 0
1770
- @lock_file = open(@lock_tempfile.path, 'w')
2075
+ @m = Mutex.new unless @m
2076
+ @m.synchronize do
2077
+ @lock_level = 0
2078
+ @lock_file = open(@lock_tempfile.path, 'w')
2079
+ end
1771
2080
  end
1772
2081
  end
1773
2082
 
@@ -1776,7 +2085,7 @@ module DEBUGGER__
1776
2085
  end
1777
2086
 
1778
2087
  def locked?
1779
- # DEBUGGER__.info "locked? #{@lock_level}"
2088
+ # DEBUGGER__.debug{ "locked? #{@lock_level}" }
1780
2089
  @lock_level > 0
1781
2090
  end
1782
2091
 
@@ -1860,6 +2169,13 @@ module DEBUGGER__
1860
2169
  puts "\nStop by #{args.first}"
1861
2170
  end
1862
2171
  end
2172
+
2173
+ def ignore_output_on_suspend?
2174
+ false
2175
+ end
2176
+
2177
+ def flush
2178
+ end
1863
2179
  end
1864
2180
 
1865
2181
  # manual configuration methods
@@ -1876,12 +2192,13 @@ module DEBUGGER__
1876
2192
  # nil for -r
1877
2193
  def self.require_location
1878
2194
  locs = caller_locations
1879
- dir_prefix = /#{__dir__}/
2195
+ dir_prefix = /#{Regexp.escape(__dir__)}/
1880
2196
 
1881
2197
  locs.each do |loc|
1882
2198
  case loc.absolute_path
1883
2199
  when dir_prefix
1884
2200
  when %r{rubygems/core_ext/kernel_require\.rb}
2201
+ when %r{bundled_gems\.rb}
1885
2202
  else
1886
2203
  return loc if loc.absolute_path
1887
2204
  end
@@ -1894,18 +2211,22 @@ module DEBUGGER__
1894
2211
  def self.start nonstop: false, **kw
1895
2212
  CONFIG.set_config(**kw)
1896
2213
 
1897
- unless defined? SESSION
1898
- require_relative 'local'
1899
- initialize_session UI_LocalConsole.new
2214
+ if CONFIG[:open]
2215
+ open nonstop: nonstop, **kw
2216
+ else
2217
+ unless defined? SESSION
2218
+ require_relative 'local'
2219
+ initialize_session{ UI_LocalConsole.new }
2220
+ end
2221
+ setup_initial_suspend unless nonstop
1900
2222
  end
1901
-
1902
- setup_initial_suspend unless nonstop
1903
2223
  end
1904
2224
 
1905
2225
  def self.open host: nil, port: CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
1906
2226
  CONFIG.set_config(**kw)
2227
+ require_relative 'server'
1907
2228
 
1908
- if port || CONFIG[:open_frontend] == 'chrome'
2229
+ if port || CONFIG[:open] == 'chrome' || (!::Addrinfo.respond_to?(:unix))
1909
2230
  open_tcp host: host, port: (port || 0), nonstop: nonstop
1910
2231
  else
1911
2232
  open_unix sock_path: sock_path, sock_dir: sock_dir, nonstop: nonstop
@@ -1919,7 +2240,7 @@ module DEBUGGER__
1919
2240
  if defined? SESSION
1920
2241
  SESSION.reset_ui UI_TcpServer.new(host: host, port: port)
1921
2242
  else
1922
- initialize_session UI_TcpServer.new(host: host, port: port)
2243
+ initialize_session{ UI_TcpServer.new(host: host, port: port) }
1923
2244
  end
1924
2245
 
1925
2246
  setup_initial_suspend unless nonstop
@@ -1932,7 +2253,7 @@ module DEBUGGER__
1932
2253
  if defined? SESSION
1933
2254
  SESSION.reset_ui UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path)
1934
2255
  else
1935
- initialize_session UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path)
2256
+ initialize_session{ UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path) }
1936
2257
  end
1937
2258
 
1938
2259
  setup_initial_suspend unless nonstop
@@ -1959,13 +2280,26 @@ module DEBUGGER__
1959
2280
  end
1960
2281
 
1961
2282
  class << self
1962
- define_method :initialize_session do |ui|
2283
+ define_method :initialize_session do |&init_ui|
1963
2284
  DEBUGGER__.info "Session start (pid: #{Process.pid})"
1964
- ::DEBUGGER__.const_set(:SESSION, Session.new(ui))
2285
+ ::DEBUGGER__.const_set(:SESSION, Session.new)
2286
+ SESSION.activate init_ui.call
1965
2287
  load_rc
1966
2288
  end
1967
2289
  end
1968
2290
 
2291
+ # Exiting control
2292
+
2293
+ class << self
2294
+ def skip_all
2295
+ @skip_all = true
2296
+ end
2297
+
2298
+ def skip?
2299
+ @skip_all
2300
+ end
2301
+ end
2302
+
1969
2303
  def self.load_rc
1970
2304
  [[File.expand_path('~/.rdbgrc'), true],
1971
2305
  [File.expand_path('~/.rdbgrc.rb'), true],
@@ -1993,34 +2327,53 @@ module DEBUGGER__
1993
2327
  end
1994
2328
  end
1995
2329
 
1996
- class ::Module
1997
- undef method_added
1998
- def method_added mid; end
1999
- def singleton_method_added mid; end
2000
- end
2330
+ # Inspector
2001
2331
 
2002
- def self.method_added tp
2003
- begin
2004
- SESSION.method_added tp
2005
- rescue Exception => e
2006
- p e
2332
+ SHORT_INSPECT_LENGTH = 40
2333
+
2334
+ class LimitedPP
2335
+ def self.pp(obj, max=80)
2336
+ out = self.new(max)
2337
+ catch out do
2338
+ PP.singleline_pp(obj, out)
2339
+ end
2340
+ out.buf
2007
2341
  end
2008
- end
2009
2342
 
2010
- METHOD_ADDED_TRACKER = self.create_method_added_tracker
2343
+ attr_reader :buf
2011
2344
 
2012
- SHORT_INSPECT_LENGTH = 40
2345
+ def initialize max
2346
+ @max = max
2347
+ @cnt = 0
2348
+ @buf = String.new
2349
+ end
2013
2350
 
2014
- def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false
2015
- str = obj.inspect
2351
+ def <<(other)
2352
+ @buf << other
2016
2353
 
2017
- if short && str.length > max_length
2018
- str[0...max_length] + '...'
2354
+ if @buf.size >= @max
2355
+ @buf = @buf[0..@max] + '...'
2356
+ throw self
2357
+ end
2358
+ end
2359
+ end
2360
+
2361
+ def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false
2362
+ if short
2363
+ LimitedPP.pp(obj, max_length)
2364
+ else
2365
+ obj.inspect
2366
+ end
2367
+ rescue NoMethodError => e
2368
+ klass, oid = M_CLASS.bind_call(obj), M_OBJECT_ID.bind_call(obj)
2369
+ if obj == (r = e.receiver)
2370
+ "<\##{klass.name}#{oid} does not have \#inspect>"
2019
2371
  else
2020
- str
2372
+ rklass, roid = M_CLASS.bind_call(r), M_OBJECT_ID.bind_call(r)
2373
+ "<\##{klass.name}:#{roid} contains <\##{rklass}:#{roid} and it does not have #inspect>"
2021
2374
  end
2022
2375
  rescue Exception => e
2023
- str = "<#inspect raises #{e.inspect}>"
2376
+ "<#inspect raises #{e.inspect}>"
2024
2377
  end
2025
2378
 
2026
2379
  def self.warn msg
@@ -2031,18 +2384,28 @@ module DEBUGGER__
2031
2384
  log :INFO, msg
2032
2385
  end
2033
2386
 
2034
- def self.log level, msg
2035
- @logfile = STDERR unless defined? @logfile
2036
-
2387
+ def self.check_loglevel level
2037
2388
  lv = LOG_LEVELS[level]
2038
- config_lv = LOG_LEVELS[CONFIG[:log_level] || :WARN]
2389
+ config_lv = LOG_LEVELS[CONFIG[:log_level]]
2390
+ lv <= config_lv
2391
+ end
2039
2392
 
2040
- if defined? SESSION
2041
- pi = SESSION.process_info
2042
- process_info = pi ? "[#{pi}]" : nil
2393
+ def self.debug(&b)
2394
+ if check_loglevel :DEBUG
2395
+ log :DEBUG, b.call
2043
2396
  end
2397
+ end
2398
+
2399
+ def self.log level, msg
2400
+ if check_loglevel level
2401
+ @logfile = STDERR unless defined? @logfile
2402
+ return if @logfile.closed?
2403
+
2404
+ if defined? SESSION
2405
+ pi = SESSION.process_info
2406
+ process_info = pi ? "[#{pi}]" : nil
2407
+ end
2044
2408
 
2045
- if lv <= config_lv
2046
2409
  if level == :WARN
2047
2410
  # :WARN on debugger is general information
2048
2411
  @logfile.puts "DEBUGGER#{process_info}: #{msg}"
@@ -2062,17 +2425,85 @@ module DEBUGGER__
2062
2425
  yield
2063
2426
  end
2064
2427
 
2428
+ if File.identical?(__FILE__.upcase, __FILE__.downcase)
2429
+ # For case insensitive file system (like Windows)
2430
+ # Note that this check is not enough because case sensitive/insensitive is
2431
+ # depend on the file system. So this check is only roughly estimation.
2432
+
2433
+ def self.compare_path(a, b)
2434
+ a&.downcase == b&.downcase
2435
+ end
2436
+ else
2437
+ def self.compare_path(a, b)
2438
+ a == b
2439
+ end
2440
+ end
2441
+
2065
2442
  module ForkInterceptor
2066
- def fork(&given_block)
2067
- return super unless defined?(SESSION) && SESSION.active?
2443
+ if Process.respond_to? :_fork
2444
+ def _fork
2445
+ return super unless defined?(SESSION) && SESSION.active?
2446
+
2447
+ parent_hook, child_hook = __fork_setup_for_debugger
2068
2448
 
2069
- unless fork_mode = CONFIG[:fork_mode]
2070
- if CONFIG[:parent_on_fork]
2071
- fork_mode = :parent
2449
+ super.tap do |pid|
2450
+ if pid != 0
2451
+ # after fork: parent
2452
+ parent_hook.call pid
2453
+ else
2454
+ # after fork: child
2455
+ child_hook.call
2456
+ end
2457
+ end
2458
+ end
2459
+ else
2460
+ def fork(&given_block)
2461
+ return super unless defined?(SESSION) && SESSION.active?
2462
+ parent_hook, child_hook = __fork_setup_for_debugger
2463
+
2464
+ if given_block
2465
+ new_block = proc {
2466
+ # after fork: child
2467
+ child_hook.call
2468
+ given_block.call
2469
+ }
2470
+ super(&new_block).tap{|pid| parent_hook.call(pid)}
2072
2471
  else
2073
- fork_mode = :both
2472
+ super.tap do |pid|
2473
+ if pid
2474
+ # after fork: parent
2475
+ parent_hook.call pid
2476
+ else
2477
+ # after fork: child
2478
+ child_hook.call
2479
+ end
2480
+ end
2481
+ end
2482
+ end
2483
+ end
2484
+
2485
+ module DaemonInterceptor
2486
+ def daemon(*args)
2487
+ return super unless defined?(SESSION) && SESSION.active?
2488
+
2489
+ _, child_hook = __fork_setup_for_debugger(:child)
2490
+
2491
+ unless SESSION.remote?
2492
+ DEBUGGER__.warn "Can't debug the code after Process.daemon locally. Use the remote debugging feature."
2493
+ end
2494
+
2495
+ super.tap do
2496
+ child_hook.call
2074
2497
  end
2075
2498
  end
2499
+ end
2500
+
2501
+ private def __fork_setup_for_debugger fork_mode = nil
2502
+ fork_mode ||= CONFIG[:fork_mode]
2503
+
2504
+ if fork_mode == :both && CONFIG[:parent_on_fork]
2505
+ fork_mode = :parent
2506
+ end
2076
2507
 
2077
2508
  parent_pid = Process.pid
2078
2509
 
@@ -2083,19 +2514,19 @@ module DEBUGGER__
2083
2514
  # Do nothing
2084
2515
  }
2085
2516
  child_hook = -> {
2086
- DEBUGGER__.warn "Detaching after fork from child process #{Process.pid}"
2517
+ DEBUGGER__.info "Detaching after fork from child process #{Process.pid}"
2087
2518
  SESSION.deactivate
2088
2519
  }
2089
2520
  when :child
2090
2521
  SESSION.before_fork false
2091
2522
 
2092
2523
  parent_hook = -> child_pid {
2093
- DEBUGGER__.warn "Detaching after fork from parent process #{Process.pid}"
2524
+ DEBUGGER__.info "Detaching after fork from parent process #{Process.pid}"
2094
2525
  SESSION.after_fork_parent
2095
2526
  SESSION.deactivate
2096
2527
  }
2097
2528
  child_hook = -> {
2098
- DEBUGGER__.warn "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2529
+ DEBUGGER__.info "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2099
2530
  SESSION.activate on_fork: true
2100
2531
  }
2101
2532
  when :both
@@ -2106,38 +2537,29 @@ module DEBUGGER__
2106
2537
  SESSION.after_fork_parent
2107
2538
  }
2108
2539
  child_hook = -> {
2109
- DEBUGGER__.warn "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2540
+ DEBUGGER__.info "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2110
2541
  SESSION.process_group.after_fork child: true
2111
2542
  SESSION.activate on_fork: true
2112
2543
  }
2113
2544
  end
2114
2545
 
2115
- if given_block
2116
- new_block = proc {
2117
- # after fork: child
2118
- child_hook.call
2119
- given_block.call
2120
- }
2121
- pid = super(&new_block)
2122
- parent_hook.call(pid)
2123
- pid
2124
- else
2125
- if pid = super
2126
- # after fork: parent
2127
- parent_hook.call pid
2128
- else
2129
- # after fork: child
2130
- child_hook.call
2131
- end
2132
-
2133
- pid
2134
- end
2546
+ return parent_hook, child_hook
2135
2547
  end
2136
2548
  end
2137
2549
 
2138
2550
  module TrapInterceptor
2139
2551
  def trap sig, *command, &command_proc
2140
- case sig&.to_sym
2552
+ sym =
2553
+ case sig
2554
+ when String
2555
+ sig.to_sym
2556
+ when Integer
2557
+ Signal.signame(sig)&.to_sym
2558
+ else
2559
+ sig
2560
+ end
2561
+
2562
+ case sym
2141
2563
  when :INT, :SIGINT
2142
2564
  if defined?(SESSION) && SESSION.active? && SESSION.intercept_trap_sigint?
2143
2565
  return SESSION.save_int_trap(command.empty? ? command_proc : command.first)
@@ -2148,28 +2570,48 @@ module DEBUGGER__
2148
2570
  end
2149
2571
  end
2150
2572
 
2151
- if RUBY_VERSION >= '3.0.0'
2573
+ if Process.respond_to? :_fork
2574
+ module ::Process
2575
+ class << self
2576
+ prepend ForkInterceptor
2577
+ prepend DaemonInterceptor
2578
+ end
2579
+ end
2580
+
2581
+ # trap
2152
2582
  module ::Kernel
2153
- prepend ForkInterceptor
2154
2583
  prepend TrapInterceptor
2155
2584
  end
2585
+ module ::Signal
2586
+ class << self
2587
+ prepend TrapInterceptor
2588
+ end
2589
+ end
2156
2590
  else
2157
- class ::Object
2158
- include ForkInterceptor
2159
- include TrapInterceptor
2591
+ if RUBY_VERSION >= '3.0.0'
2592
+ module ::Kernel
2593
+ prepend ForkInterceptor
2594
+ prepend TrapInterceptor
2595
+ end
2596
+ else
2597
+ class ::Object
2598
+ include ForkInterceptor
2599
+ include TrapInterceptor
2600
+ end
2160
2601
  end
2161
- end
2162
2602
 
2163
- module ::Kernel
2164
- class << self
2165
- prepend ForkInterceptor
2166
- prepend TrapInterceptor
2603
+ module ::Kernel
2604
+ class << self
2605
+ prepend ForkInterceptor
2606
+ prepend TrapInterceptor
2607
+ end
2167
2608
  end
2168
- end
2169
2609
 
2170
- module ::Process
2171
- class << self
2172
- prepend ForkInterceptor
2610
+ module ::Process
2611
+ class << self
2612
+ prepend ForkInterceptor
2613
+ prepend DaemonInterceptor
2614
+ end
2173
2615
  end
2174
2616
  end
2175
2617
 
@@ -2185,10 +2627,17 @@ module Kernel
2185
2627
  return if !defined?(::DEBUGGER__::SESSION) || !::DEBUGGER__::SESSION.active?
2186
2628
 
2187
2629
  if pre || (do_expr = binding.local_variable_get(:do))
2188
- cmds = ['binding.break', pre, do_expr]
2630
+ cmds = ['#debugger', pre, do_expr]
2189
2631
  end
2190
2632
 
2191
- loc = caller_locations(up_level, 1).first; ::DEBUGGER__.add_line_breakpoint loc.path, loc.lineno + 1, oneshot: true, command: cmds
2633
+ if ::DEBUGGER__::SESSION.in_subsession?
2634
+ if cmds
2635
+ commands = [*cmds[1], *cmds[2]].map{|c| c.split(';;').join("\n")}
2636
+ ::DEBUGGER__::SESSION.add_preset_commands cmds[0], commands, kick: false, continue: false
2637
+ end
2638
+ else
2639
+ loc = caller_locations(up_level, 1).first; ::DEBUGGER__.add_line_breakpoint loc.path, loc.lineno + 1, oneshot: true, command: cmds
2640
+ end
2192
2641
  self
2193
2642
  end
2194
2643
 
@@ -2199,3 +2648,12 @@ class Binding
2199
2648
  alias break debugger
2200
2649
  alias b debugger
2201
2650
  end
2651
+
2652
+ # for Ruby 2.6 compatibility
2653
+ unless method(:p).unbind.respond_to? :bind_call
2654
+ class UnboundMethod
2655
+ def bind_call(obj, *args)
2656
+ self.bind(obj).call(*args)
2657
+ end
2658
+ end
2659
+ end