debug 1.4.0 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
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