ed-precompiled_debug 1.11.0-x86_64-linux

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.
@@ -0,0 +1,2667 @@
1
+ # frozen_string_literal: true
2
+
3
+ return if ENV['RUBY_DEBUG_ENABLE'] == '0'
4
+
5
+ # skip to load debugger for bundle exec
6
+
7
+ if $0.end_with?('bin/bundle') && ARGV.first == 'exec'
8
+ trace_var(:$0) do |file|
9
+ trace_var(:$0, nil)
10
+ if /-r (#{Regexp.escape(__dir__)}\S+)/ =~ ENV['RUBYOPT']
11
+ lib = $1
12
+ $LOADED_FEATURES.delete_if{|path| path.start_with?(__dir__)}
13
+ ENV['RUBY_DEBUG_INITIAL_SUSPEND_PATH'] = file
14
+ require lib
15
+ ENV['RUBY_DEBUG_INITIAL_SUSPEND_PATH'] = nil
16
+ end
17
+ end
18
+
19
+ return
20
+ end
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
+
31
+ require_relative 'frame_info'
32
+ require_relative 'config'
33
+ require_relative 'thread_client'
34
+ require_relative 'source_repository'
35
+ require_relative 'breakpoint'
36
+ require_relative 'tracer'
37
+
38
+ # To prevent loading old lib/debug.rb in Ruby 2.6 to 3.0
39
+ $LOADED_FEATURES << 'debug.rb'
40
+ $LOADED_FEATURES << File.expand_path(File.join(__dir__, '..', 'debug.rb'))
41
+ require 'debug' # invalidate the $LOADED_FEATURE cache
42
+
43
+ require 'json' if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal'
44
+ require 'pp'
45
+
46
+ class RubyVM::InstructionSequence
47
+ def traceable_lines_norec lines
48
+ code = self.to_a[13]
49
+ line = 0
50
+ code.each{|e|
51
+ case e
52
+ when Integer
53
+ line = e
54
+ when Symbol
55
+ if /\ARUBY_EVENT_/ =~ e.to_s
56
+ lines[line] = [e, *lines[line]]
57
+ end
58
+ end
59
+ }
60
+ end
61
+
62
+ def traceable_lines_rec lines
63
+ self.each_child{|ci| ci.traceable_lines_rec(lines)}
64
+ traceable_lines_norec lines
65
+ end
66
+
67
+ def type
68
+ self.to_a[9]
69
+ end unless method_defined?(:type)
70
+
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)
77
+
78
+ def last_line
79
+ self.to_a[4][:code_location][2]
80
+ end unless method_defined?(:last_line)
81
+
82
+ def first_line
83
+ self.to_a[4][:code_location][0]
84
+ end unless method_defined?(:first_line)
85
+ end if defined?(RubyVM::InstructionSequence)
86
+
87
+ module DEBUGGER__
88
+ PresetCommands = Struct.new(:commands, :source, :auto_continue)
89
+ SessionCommand = Struct.new(:block, :repeat, :unsafe, :cancel_auto_continue, :postmortem)
90
+
91
+ class PostmortemError < RuntimeError; end
92
+
93
+ class Session
94
+ attr_reader :intercepted_sigint_cmd, :process_group, :subsession_id
95
+
96
+ include Color
97
+
98
+ def initialize
99
+ @ui = nil
100
+ @sr = SourceRepository.new
101
+ @bps = {} # bp.key => bp
102
+ # [file, line] => LineBreakpoint
103
+ # "Error" => CatchBreakpoint
104
+ # "Foo#bar" => MethodBreakpoint
105
+ # [:watch, ivar] => WatchIVarBreakpoint
106
+ # [:check, expr] => CheckBreakpoint
107
+ #
108
+ @tracers = {}
109
+ @th_clients = {} # {Thread => ThreadClient}
110
+ @q_evt = Queue.new
111
+ @displays = []
112
+ @tc = nil
113
+ @tc_id = 0
114
+ @preset_command = nil
115
+ @postmortem_hook = nil
116
+ @postmortem = false
117
+ @intercept_trap_sigint = false
118
+ @intercepted_sigint_cmd = 'DEFAULT'
119
+ @process_group = ProcessGroup.new
120
+ @subsession_stack = []
121
+ @subsession_id = 0
122
+
123
+ @frame_map = {} # for DAP: {id => [threadId, frame_depth]} and CDP: {id => frame_depth}
124
+ @var_map = {1 => [:globals], } # {id => ...} for DAP
125
+ @src_map = {} # {id => src}
126
+
127
+ @scr_id_map = {} # for CDP
128
+ @obj_map = {} # { object_id => ... } for CDP
129
+
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
+
138
+ @tp_load_script = TracePoint.new(:script_compiled){|tp|
139
+ eval_script = tp.eval_script unless @has_keep_script_lines
140
+ ThreadClient.current.on_load tp.instruction_sequence, eval_script
141
+ }
142
+ @tp_load_script.enable
143
+
144
+ @thread_stopper = thread_stopper
145
+ self.postmortem = CONFIG[:postmortem]
146
+
147
+ register_default_command
148
+ end
149
+
150
+ def active?
151
+ !@q_evt.closed?
152
+ end
153
+
154
+ def remote?
155
+ @ui.remote?
156
+ end
157
+
158
+ def stop_stepping? file, line, subsession_id = nil
159
+ if @bps.has_key? [file, line]
160
+ true
161
+ elsif subsession_id && @subsession_id != subsession_id
162
+ true
163
+ else
164
+ false
165
+ end
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
176
+
177
+ q = Queue.new
178
+ first_q = Queue.new
179
+ @session_server = Thread.new do
180
+ # make sure `@session_server` is assigned
181
+ first_q.pop; first_q = nil
182
+
183
+ Thread.current.name = 'DEBUGGER__::SESSION@server'
184
+ Thread.current.abort_on_exception = true
185
+
186
+ # Thread management
187
+ setup_threads
188
+ thc = get_thread_client Thread.current
189
+ thc.mark_as_management
190
+
191
+ if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
192
+ thc.mark_as_management
193
+ end
194
+
195
+ @tp_thread_begin = TracePoint.new(:thread_begin) do |tp|
196
+ get_thread_client
197
+ end
198
+ @tp_thread_begin.enable
199
+
200
+ @tp_thread_end = TracePoint.new(:thread_end) do |tp|
201
+ @th_clients.delete(Thread.current)
202
+ end
203
+ @tp_thread_end.enable
204
+
205
+ # session start
206
+ q << true
207
+ session_server_main
208
+ end
209
+ first_q << :ok
210
+
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
218
+ end
219
+
220
+ def deactivate
221
+ get_thread_client.deactivate
222
+ @thread_stopper.disable
223
+ @tp_load_script.disable
224
+ @tp_thread_begin.disable
225
+ @tp_thread_end.disable
226
+ @bps.each_value{|bp| bp.disable}
227
+ @th_clients.each_value{|thc| thc.close}
228
+ @tracers.values.each{|t| t.disable}
229
+ @q_evt.close
230
+ @ui&.deactivate
231
+ @ui = nil
232
+ end
233
+
234
+ def reset_ui ui
235
+ @ui.deactivate
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
247
+ end
248
+
249
+ def pop_event
250
+ @q_evt.pop
251
+ end
252
+
253
+ def session_server_main
254
+ while evt = pop_event
255
+ process_event evt
256
+ end
257
+ ensure
258
+ deactivate
259
+ end
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
+
274
+ def process_event evt
275
+ # variable `@internal_info` is only used for test
276
+ tc, output, ev, @internal_info, *ev_args = evt
277
+
278
+ output.each{|str| @ui.puts str} if ev != :suspend
279
+
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
283
+ th = ev_args.shift
284
+ q = ev_args.shift
285
+ on_thread_begin th
286
+ q << true
287
+
288
+ return
289
+ end
290
+
291
+ @tc = tc
292
+
293
+ case ev
294
+ when :init
295
+ enter_subsession
296
+ wait_command_loop
297
+ when :load
298
+ iseq, src = ev_args
299
+ on_load iseq, src
300
+ request_tc :continue
301
+
302
+ when :trace
303
+ trace_id, msg = ev_args
304
+ if t = @tracers.values.find{|t| t.object_id == trace_id}
305
+ t.puts msg
306
+ end
307
+ request_tc :continue
308
+
309
+ when :suspend
310
+ enter_subsession if ev_args.first != :replay
311
+ output.each{|str| @ui.puts str} unless @ui.ignore_output_on_suspend?
312
+
313
+ case ev_args.first
314
+ when :breakpoint
315
+ bp, i = bp_index ev_args[1]
316
+ clean_bps unless bp
317
+ @ui.event :suspend_bp, i, bp, @tc.id
318
+ when :trap
319
+ @ui.event :suspend_trap, sig = ev_args[1], @tc.id
320
+
321
+ if sig == :SIGINT && (@intercepted_sigint_cmd.kind_of?(Proc) || @intercepted_sigint_cmd.kind_of?(String))
322
+ @ui.puts "#{@intercepted_sigint_cmd.inspect} is registered as SIGINT handler."
323
+ @ui.puts "`sigint` command execute it."
324
+ end
325
+ else
326
+ @ui.event :suspended, @tc.id
327
+ end
328
+
329
+ if @displays.empty?
330
+ wait_command_loop
331
+ else
332
+ request_eval :display, @displays
333
+ end
334
+ when :result
335
+ raise "[BUG] not in subsession" if @subsession_stack.empty?
336
+
337
+ case ev_args.first
338
+ when :try_display
339
+ failed_results = ev_args[1]
340
+ if failed_results.size > 0
341
+ i, _msg = failed_results.last
342
+ if i+1 == @displays.size
343
+ @ui.puts "canceled: #{@displays.pop}"
344
+ end
345
+ end
346
+
347
+ stop_all_threads
348
+ when :method_breakpoint, :watch_breakpoint
349
+ bp = ev_args[1]
350
+ if bp
351
+ add_bp(bp)
352
+ show_bps bp
353
+ else
354
+ # can't make a bp
355
+ end
356
+ when :trace_pass
357
+ obj_id = ev_args[1]
358
+ obj_inspect = ev_args[2]
359
+ opt = ev_args[3]
360
+ add_tracer ObjectTracer.new(@ui, obj_id, obj_inspect, **opt)
361
+ stop_all_threads
362
+ else
363
+ stop_all_threads
364
+ end
365
+
366
+ wait_command_loop
367
+
368
+ when :protocol_result
369
+ process_protocol_result ev_args
370
+ wait_command_loop
371
+ end
372
+ end
373
+
374
+ def add_preset_commands name, cmds, kick: true, continue: true
375
+ cs = cmds.map{|c|
376
+ c.each_line.map{|line|
377
+ line = line.strip.gsub(/\A\s*\#.*/, '').strip
378
+ line unless line.empty?
379
+ }.compact
380
+ }.flatten.compact
381
+
382
+ if @preset_command && !@preset_command.commands.empty?
383
+ @preset_command.commands += cs
384
+ else
385
+ @preset_command = PresetCommands.new(cs, name, continue)
386
+ end
387
+
388
+ ThreadClient.current.on_init name if kick
389
+ end
390
+
391
+ def source iseq
392
+ if !CONFIG[:no_color]
393
+ @sr.get_colored(iseq)
394
+ else
395
+ @sr.get(iseq)
396
+ end
397
+ end
398
+
399
+ def inspect
400
+ "DEBUGGER__::SESSION"
401
+ end
402
+
403
+ def wait_command_loop
404
+ loop do
405
+ case wait_command
406
+ when :retry
407
+ # nothing
408
+ else
409
+ break
410
+ end
411
+ rescue Interrupt
412
+ @ui.puts "\n^C"
413
+ retry
414
+ end
415
+ end
416
+
417
+ def prompt
418
+ if @postmortem
419
+ '(rdbg:postmortem) '
420
+ elsif @process_group.multi?
421
+ "(rdbg@#{process_info}) "
422
+ else
423
+ '(rdbg) '
424
+ end
425
+ end
426
+
427
+ def wait_command
428
+ if @preset_command
429
+ if @preset_command.commands.empty?
430
+ if @preset_command.auto_continue
431
+ @preset_command = nil
432
+
433
+ leave_subsession :continue
434
+ return
435
+ else
436
+ @preset_command = nil
437
+ return :retry
438
+ end
439
+ else
440
+ line = @preset_command.commands.shift
441
+ @ui.puts "(rdbg:#{@preset_command.source}) #{line}"
442
+ end
443
+ else
444
+ @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal'
445
+ line = @ui.readline prompt
446
+ end
447
+
448
+ case line
449
+ when String
450
+ process_command line
451
+ when Hash
452
+ process_protocol_request line # defined in server.rb
453
+ else
454
+ raise "unexpected input: #{line.inspect}"
455
+ end
456
+ end
457
+
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)
462
+
463
+ names.each{|name|
464
+ @commands[name] = cmd
465
+ }
466
+ end
467
+
468
+ def register_default_command
469
+ ### Control flow
470
+
471
+ # * `s[tep]`
472
+ # * Step in. Resume the program until next breakable point.
473
+ # * `s[tep] <n>`
474
+ # * Step in, resume the program at `<n>`th breakable point.
475
+ register_command 's', 'step',
476
+ repeat: true,
477
+ cancel_auto_continue: true,
478
+ postmortem: false do |arg|
479
+ step_command :in, arg
480
+ end
481
+
482
+ # * `n[ext]`
483
+ # * Step over. Resume the program until next line.
484
+ # * `n[ext] <n>`
485
+ # * Step over, same as `step <n>`.
486
+ register_command 'n', 'next',
487
+ repeat: true,
488
+ cancel_auto_continue: true,
489
+ postmortem: false do |arg|
490
+ step_command :next, arg
491
+ end
492
+
493
+ # * `fin[ish]`
494
+ # * Finish this frame. Resume the program until the current frame is finished.
495
+ # * `fin[ish] <n>`
496
+ # * Finish `<n>`th frames.
497
+ register_command 'fin', 'finish',
498
+ repeat: true,
499
+ cancel_auto_continue: true,
500
+ postmortem: false do |arg|
501
+ if arg&.to_i == 0
502
+ raise 'finish command with 0 does not make sense.'
503
+ end
504
+
505
+ step_command :finish, arg
506
+ end
507
+
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`
524
+ # * Resume the program.
525
+ register_command 'c', 'cont', 'continue',
526
+ repeat: true,
527
+ cancel_auto_continue: true do |arg|
528
+ leave_subsession :continue
529
+ end
530
+
531
+ # * `q[uit]` or `Ctrl-D`
532
+ # * Finish debugger (with the debuggee process on non-remote debugging).
533
+ register_command 'q', 'quit' do |arg|
534
+ if ask 'Really quit?'
535
+ @ui.quit arg.to_i do
536
+ request_tc :quit
537
+ end
538
+ leave_subsession :continue
539
+ else
540
+ next :retry
541
+ end
542
+ end
543
+
544
+ # * `q[uit]!`
545
+ # * Same as q[uit] but without the confirmation prompt.
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
552
+
553
+ # * `kill`
554
+ # * Stop the debuggee process with `Kernel#exit!`.
555
+ register_command 'kill' do |arg|
556
+ if ask 'Really kill?'
557
+ exit! (arg || 1).to_i
558
+ else
559
+ next :retry
560
+ end
561
+ end
562
+
563
+ # * `kill!`
564
+ # * Same as kill but without the confirmation prompt.
565
+ register_command 'kill!', unsafe: false do |arg|
566
+ exit! (arg || 1).to_i
567
+ end
568
+
569
+ # * `sigint`
570
+ # * Execute SIGINT handler registered by the debuggee.
571
+ # * Note that this command should be used just after stop by `SIGINT`.
572
+ register_command 'sigint' do
573
+ begin
574
+ case cmd = @intercepted_sigint_cmd
575
+ when nil, 'IGNORE', :IGNORE, 'DEFAULT', :DEFAULT
576
+ # ignore
577
+ when String
578
+ eval(cmd)
579
+ when Proc
580
+ cmd.call
581
+ end
582
+
583
+ leave_subsession :continue
584
+
585
+ rescue Exception => e
586
+ @ui.puts "Exception: #{e}"
587
+ @ui.puts e.backtrace.map{|line| " #{e}"}
588
+ next :retry
589
+ end
590
+ end
591
+
592
+ ### Breakpoint
593
+
594
+ # * `b[reak]`
595
+ # * Show all breakpoints.
596
+ # * `b[reak] <line>`
597
+ # * Set breakpoint on `<line>` at the current frame's file.
598
+ # * `b[reak] <file>:<line>` or `<file> <line>`
599
+ # * Set breakpoint on `<file>:<line>`.
600
+ # * `b[reak] <class>#<name>`
601
+ # * Set breakpoint on the method `<class>#<name>`.
602
+ # * `b[reak] <expr>.<name>`
603
+ # * Set breakpoint on the method `<expr>.<name>`.
604
+ # * `b[reak] ... if: <expr>`
605
+ # * break if `<expr>` is true at specified location.
606
+ # * `b[reak] ... pre: <command>`
607
+ # * break and run `<command>` before stopping.
608
+ # * `b[reak] ... do: <command>`
609
+ # * break and run `<command>`, and continue.
610
+ # * `b[reak] ... path: <path>`
611
+ # * break if the path matches to `<path>`. `<path>` can be a regexp with `/regexp/`.
612
+ # * `b[reak] if: <expr>`
613
+ # * break if: `<expr>` is true at any lines.
614
+ # * Note that this feature is super slow.
615
+ register_command 'b', 'break', postmortem: false, unsafe: false do |arg|
616
+ if arg == nil
617
+ show_bps
618
+ next :retry
619
+ else
620
+ case bp = repl_add_breakpoint(arg)
621
+ when :noretry
622
+ when nil
623
+ next :retry
624
+ else
625
+ show_bps bp
626
+ next :retry
627
+ end
628
+ end
629
+ end
630
+
631
+ # * `catch <Error>`
632
+ # * Set breakpoint on raising `<Error>`.
633
+ # * `catch ... if: <expr>`
634
+ # * stops only if `<expr>` is true as well.
635
+ # * `catch ... pre: <command>`
636
+ # * runs `<command>` before stopping.
637
+ # * `catch ... do: <command>`
638
+ # * stops and run `<command>`, and continue.
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|
642
+ if arg
643
+ bp = repl_add_catch_breakpoint arg
644
+ show_bps bp if bp
645
+ else
646
+ show_bps
647
+ end
648
+
649
+ :retry
650
+ end
651
+
652
+ # * `watch @ivar`
653
+ # * Stop the execution when the result of current scope's `@ivar` is changed.
654
+ # * Note that this feature is super slow.
655
+ # * `watch ... if: <expr>`
656
+ # * stops only if `<expr>` is true as well.
657
+ # * `watch ... pre: <command>`
658
+ # * runs `<command>` before stopping.
659
+ # * `watch ... do: <command>`
660
+ # * stops and run `<command>`, and continue.
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|
664
+ if arg && arg.match?(/\A@\w+/)
665
+ repl_add_watch_breakpoint(arg)
666
+ else
667
+ show_bps
668
+ :retry
669
+ end
670
+ end
671
+
672
+ # * `del[ete]`
673
+ # * delete all breakpoints.
674
+ # * `del[ete] <bpnum>`
675
+ # * delete specified breakpoint.
676
+ register_command 'del', 'delete', postmortem: false, unsafe: false do |arg|
677
+ case arg
678
+ when nil
679
+ show_bps
680
+ if ask "Remove all breakpoints?", 'N'
681
+ delete_bp
682
+ end
683
+ when /\d+/
684
+ bp = delete_bp arg.to_i
685
+ else
686
+ nil
687
+ end
688
+ @ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp
689
+ :retry
690
+ end
691
+
692
+ ### Information
693
+
694
+ # * `bt` or `backtrace`
695
+ # * Show backtrace (frame) information.
696
+ # * `bt <num>` or `backtrace <num>`
697
+ # * Only shows first `<num>` frames.
698
+ # * `bt /regexp/` or `backtrace /regexp/`
699
+ # * Only shows frames with method name or location info that matches `/regexp/`.
700
+ # * `bt <num> /regexp/` or `backtrace <num> /regexp/`
701
+ # * Only shows first `<num>` frames with method name or location info that matches `/regexp/`.
702
+ register_command 'bt', 'backtrace', unsafe: false do |arg|
703
+ case arg
704
+ when /\A(\d+)\z/
705
+ request_tc_with_restarted_threads [:show, :backtrace, arg.to_i, nil]
706
+ when /\A\/(.*)\/\z/
707
+ pattern = $1
708
+ request_tc_with_restarted_threads [:show, :backtrace, nil, Regexp.compile(pattern)]
709
+ when /\A(\d+)\s+\/(.*)\/\z/
710
+ max, pattern = $1, $2
711
+ request_tc_with_restarted_threads [:show, :backtrace, max.to_i, Regexp.compile(pattern)]
712
+ else
713
+ request_tc_with_restarted_threads [:show, :backtrace, nil, nil]
714
+ end
715
+ end
716
+
717
+ # * `l[ist]`
718
+ # * Show current frame's source code.
719
+ # * Next `list` command shows the successor lines.
720
+ # * `l[ist] -`
721
+ # * Show predecessor lines as opposed to the `list` command.
722
+ # * `l[ist] <start>` or `l[ist] <start>-<end>`
723
+ # * Show current frame's source code from the line <start> to <end> if given.
724
+ register_command 'l', 'list', repeat: true, unsafe: false do |arg|
725
+ case arg ? arg.strip : nil
726
+ when /\A(\d+)\z/
727
+ request_tc [:show, :list, {start_line: arg.to_i - 1}]
728
+ when /\A-\z/
729
+ request_tc [:show, :list, {dir: -1}]
730
+ when /\A(\d+)-(\d+)\z/
731
+ request_tc [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}]
732
+ when nil
733
+ request_tc [:show, :list]
734
+ else
735
+ @ui.puts "Can not handle list argument: #{arg}"
736
+ :retry
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
745
+
746
+ # * `edit`
747
+ # * Open the current file on the editor (use `EDITOR` environment variable).
748
+ # * Note that edited file will not be reloaded.
749
+ # * `edit <file>`
750
+ # * Open <file> on the editor.
751
+ register_command 'edit' do |arg|
752
+ if @ui.remote?
753
+ @ui.puts "not supported on the remote console."
754
+ next :retry
755
+ end
756
+
757
+ begin
758
+ arg = resolve_path(arg) if arg
759
+ rescue Errno::ENOENT
760
+ @ui.puts "not found: #{arg}"
761
+ next :retry
762
+ end
763
+
764
+ request_tc [:show, :edit, arg]
765
+ end
766
+
767
+ info_subcommands = nil
768
+ info_subcommands_abbrev = nil
769
+
770
+ # * `i[nfo]`
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`
776
+ # * Show information about the current frame (local variables)
777
+ # * It includes `self` as `%self` and a return value as `_return`.
778
+ # * `i[nfo] i or ivars or instance_variables`
779
+ # * Show information about instance variables about `self`.
780
+ # * `info ivars <expr>` shows the instance variables of the result of `<expr>`.
781
+ # * `i[nfo] c or consts or constants`
782
+ # * Show information about accessible constants except toplevel constants.
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`
785
+ # * Show information about global variables
786
+ # * `i[nfo] th or threads`
787
+ # * Show all threads (same as `th[read]`).
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|
793
+ if /\/(.+)\/\z/ =~ arg
794
+ pat = Regexp.compile($1)
795
+ sub = $~.pre_match.strip
796
+ else
797
+ sub = arg
798
+ end
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
+
828
+ case sub
829
+ when nil
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
840
+ thread_list
841
+ :retry
842
+ when :breaks, :watchs
843
+ show_bps
844
+ :retry
845
+ else
846
+ @ui.puts "unrecognized argument for info command: #{arg}"
847
+ show_help 'info'
848
+ :retry
849
+ end
850
+ end
851
+
852
+ # * `o[utline]` or `ls`
853
+ # * Show you available methods, constants, local variables, and instance variables in the current scope.
854
+ # * `o[utline] <expr>` or `ls <expr>`
855
+ # * Show you available methods and instance variables of the given object.
856
+ # * If the object is a class/module, it also lists its constants.
857
+ register_command 'outline', 'o', 'ls', unsafe: false do |arg|
858
+ request_tc_with_restarted_threads [:show, :outline, arg]
859
+ end
860
+
861
+ # * `display`
862
+ # * Show display setting.
863
+ # * `display <expr>`
864
+ # * Show the result of `<expr>` at every suspended timing.
865
+ register_command 'display', postmortem: false do |arg|
866
+ if arg && !arg.empty?
867
+ @displays << arg
868
+ request_eval :try_display, @displays
869
+ else
870
+ request_eval :display, @displays
871
+ end
872
+ end
873
+
874
+ # * `undisplay`
875
+ # * Remove all display settings.
876
+ # * `undisplay <displaynum>`
877
+ # * Remove a specified display setting.
878
+ register_command 'undisplay', postmortem: false, unsafe: false do |arg|
879
+ case arg
880
+ when /(\d+)/
881
+ if @displays[n = $1.to_i]
882
+ @displays.delete_at n
883
+ end
884
+ request_eval :display, @displays
885
+ when nil
886
+ if ask "clear all?", 'N'
887
+ @displays.clear
888
+ end
889
+ :retry
890
+ end
891
+ end
892
+
893
+ ### Frame control
894
+
895
+ # * `f[rame]`
896
+ # * Show the current frame.
897
+ # * `f[rame] <framenum>`
898
+ # * Specify a current frame. Evaluation are run on specified frame.
899
+ register_command 'frame', 'f', unsafe: false do |arg|
900
+ request_tc [:frame, :set, arg]
901
+ end
902
+
903
+ # * `up`
904
+ # * Specify the upper frame.
905
+ register_command 'up', repeat: true, unsafe: false do |arg|
906
+ request_tc [:frame, :up]
907
+ end
908
+
909
+ # * `down`
910
+ # * Specify the lower frame.
911
+ register_command 'down', repeat: true, unsafe: false do |arg|
912
+ request_tc [:frame, :down]
913
+ end
914
+
915
+ ### Evaluate
916
+
917
+ # * `p <expr>`
918
+ # * Evaluate like `p <expr>` on the current frame.
919
+ register_command 'p' do |arg|
920
+ request_eval :p, arg.to_s
921
+ end
922
+
923
+ # * `pp <expr>`
924
+ # * Evaluate like `pp <expr>` on the current frame.
925
+ register_command 'pp' do |arg|
926
+ request_eval :pp, arg.to_s
927
+ end
928
+
929
+ # * `eval <expr>`
930
+ # * Evaluate `<expr>` on the current frame.
931
+ register_command 'eval', 'call' do |arg|
932
+ if arg == nil || arg.empty?
933
+ show_help 'eval'
934
+ :retry
935
+ else
936
+ request_eval :call, arg
937
+ end
938
+ end
939
+
940
+ # * `irb`
941
+ # * Activate and switch to `irb:rdbg` console
942
+ register_command 'irb' do |arg|
943
+ if @ui.remote?
944
+ @ui.puts "\nIRB is not supported on the remote console."
945
+ else
946
+ config_set :irb_console, true
947
+ end
948
+
949
+ :retry
950
+ end
951
+
952
+ ### Trace
953
+ # * `trace`
954
+ # * Show available tracers list.
955
+ # * `trace line`
956
+ # * Add a line tracer. It indicates line events.
957
+ # * `trace call`
958
+ # * Add a call tracer. It indicate call/return events.
959
+ # * `trace exception`
960
+ # * Add an exception tracer. It indicates raising exceptions.
961
+ # * `trace object <expr>`
962
+ # * Add an object tracer. It indicates that an object by `<expr>` is passed as a parameter or a receiver on method call.
963
+ # * `trace ... /regexp/`
964
+ # * Indicates only matched events to `/regexp/`.
965
+ # * `trace ... into: <file>`
966
+ # * Save trace information into: `<file>`.
967
+ # * `trace off <num>`
968
+ # * Disable tracer specified by `<num>` (use `trace` command to check the numbers).
969
+ # * `trace off [line|call|pass]`
970
+ # * Disable all tracers. If `<type>` is provided, disable specified type tracers.
971
+ register_command 'trace', postmortem: false, unsafe: false do |arg|
972
+ if (re = /\s+into:\s*(.+)/) =~ arg
973
+ into = $1
974
+ arg.sub!(re, '')
975
+ end
976
+
977
+ if (re = /\s\/(.+)\/\z/) =~ arg
978
+ pattern = $1
979
+ arg.sub!(re, '')
980
+ end
981
+
982
+ case arg
983
+ when nil
984
+ @ui.puts 'Tracers:'
985
+ @tracers.values.each_with_index{|t, i|
986
+ @ui.puts "* \##{i} #{t}"
987
+ }
988
+ @ui.puts
989
+ :retry
990
+
991
+ when /\Aline\z/
992
+ add_tracer LineTracer.new(@ui, pattern: pattern, into: into)
993
+ :retry
994
+
995
+ when /\Acall\z/
996
+ add_tracer CallTracer.new(@ui, pattern: pattern, into: into)
997
+ :retry
998
+
999
+ when /\Aexception\z/
1000
+ add_tracer ExceptionTracer.new(@ui, pattern: pattern, into: into)
1001
+ :retry
1002
+
1003
+ when /\Aobject\s+(.+)/
1004
+ request_tc_with_restarted_threads [:trace, :object, $1.strip, {pattern: pattern, into: into}]
1005
+
1006
+ when /\Aoff\s+(\d+)\z/
1007
+ if t = @tracers.values[$1.to_i]
1008
+ t.disable
1009
+ @ui.puts "Disable #{t.to_s}"
1010
+ else
1011
+ @ui.puts "Unmatched: #{$1}"
1012
+ end
1013
+ :retry
1014
+
1015
+ when /\Aoff(\s+(line|call|exception|object))?\z/
1016
+ @tracers.values.each{|t|
1017
+ if $2.nil? || t.type == $2
1018
+ t.disable
1019
+ @ui.puts "Disable #{t.to_s}"
1020
+ end
1021
+ }
1022
+ :retry
1023
+
1024
+ else
1025
+ @ui.puts "Unknown trace option: #{arg.inspect}"
1026
+ :retry
1027
+ end
1028
+ end
1029
+
1030
+ # Record
1031
+ # * `record`
1032
+ # * Show recording status.
1033
+ # * `record [on|off]`
1034
+ # * Start/Stop recording.
1035
+ # * `step back`
1036
+ # * Start replay. Step back with the last execution log.
1037
+ # * `s[tep]` does stepping forward with the last log.
1038
+ # * `step reset`
1039
+ # * Stop replay .
1040
+ register_command 'record', postmortem: false, unsafe: false do |arg|
1041
+ case arg
1042
+ when nil, 'on', 'off'
1043
+ request_tc [:record, arg&.to_sym]
1044
+ else
1045
+ @ui.puts "unknown command: #{arg}"
1046
+ :retry
1047
+ end
1048
+ end
1049
+
1050
+ ### Thread control
1051
+
1052
+ # * `th[read]`
1053
+ # * Show all threads.
1054
+ # * `th[read] <thnum>`
1055
+ # * Switch thread specified by `<thnum>`.
1056
+ register_command 'th', 'thread', unsafe: false do |arg|
1057
+ case arg
1058
+ when nil, 'list', 'l'
1059
+ thread_list
1060
+ when /(\d+)/
1061
+ switch_thread $1.to_i
1062
+ else
1063
+ @ui.puts "unknown thread command: #{arg}"
1064
+ end
1065
+ :retry
1066
+ end
1067
+
1068
+ ### Configuration
1069
+ # * `config`
1070
+ # * Show all configuration with description.
1071
+ # * `config <name>`
1072
+ # * Show current configuration of <name>.
1073
+ # * `config set <name> <val>` or `config <name> = <val>`
1074
+ # * Set <name> to <val>.
1075
+ # * `config append <name> <val>` or `config <name> << <val>`
1076
+ # * Append `<val>` to `<name>` if it is an array.
1077
+ # * `config unset <name>`
1078
+ # * Set <name> to default.
1079
+ register_command 'config', unsafe: false do |arg|
1080
+ config_command arg
1081
+ :retry
1082
+ end
1083
+
1084
+ # * `source <file>`
1085
+ # * Evaluate lines in `<file>` as debug commands.
1086
+ register_command 'source' do |arg|
1087
+ if arg
1088
+ begin
1089
+ cmds = File.readlines(path = File.expand_path(arg))
1090
+ add_preset_commands path, cmds, kick: true, continue: false
1091
+ rescue Errno::ENOENT
1092
+ @ui.puts "File not found: #{arg}"
1093
+ end
1094
+ else
1095
+ show_help 'source'
1096
+ end
1097
+ :retry
1098
+ end
1099
+
1100
+ # * `open`
1101
+ # * open debuggee port on UNIX domain socket and wait for attaching.
1102
+ # * Note that `open` command is EXPERIMENTAL.
1103
+ # * `open [<host>:]<port>`
1104
+ # * open debuggee port on TCP/IP with given `[<host>:]<port>` and wait for attaching.
1105
+ # * `open vscode`
1106
+ # * open debuggee port for VSCode and launch VSCode if available.
1107
+ # * `open chrome`
1108
+ # * open debuggee port for Chrome and wait for attaching.
1109
+ register_command 'open' do |arg|
1110
+ case arg&.downcase
1111
+ when '', nil
1112
+ ::DEBUGGER__.open nonstop: true
1113
+ when /\A(\d+)z/
1114
+ ::DEBUGGER__.open_tcp host: nil, port: $1.to_i, nonstop: true
1115
+ when /\A(.+):(\d+)\z/
1116
+ ::DEBUGGER__.open_tcp host: $1, port: $2.to_i, nonstop: true
1117
+ when 'tcp'
1118
+ ::DEBUGGER__.open_tcp host: CONFIG[:host], port: (CONFIG[:port] || 0), nonstop: true
1119
+ when 'vscode'
1120
+ CONFIG[:open] = 'vscode'
1121
+ ::DEBUGGER__.open nonstop: true
1122
+ when 'chrome', 'cdp'
1123
+ CONFIG[:open] = 'chrome'
1124
+ ::DEBUGGER__.open_tcp host: CONFIG[:host], port: (CONFIG[:port] || 0), nonstop: true
1125
+ else
1126
+ raise "Unknown arg: #{arg}"
1127
+ end
1128
+
1129
+ :retry
1130
+ end
1131
+
1132
+ ### Help
1133
+
1134
+ # * `h[elp]`
1135
+ # * Show help for all commands.
1136
+ # * `h[elp] <command>`
1137
+ # * Show help for the given command.
1138
+ register_command 'h', 'help', '?', unsafe: false do |arg|
1139
+ show_help arg
1140
+ :retry
1141
+ end
1142
+ end
1143
+
1144
+ def process_command line
1145
+ if line.empty?
1146
+ if @repl_prev_line && !CONFIG[:no_repeat]
1147
+ line = @repl_prev_line
1148
+ else
1149
+ return :retry
1150
+ end
1151
+ else
1152
+ @repl_prev_line = line
1153
+ end
1154
+
1155
+ /([^\s]+)(?:\s+(.+))?/ =~ line
1156
+ cmd_name, cmd_arg = $1, $2
1157
+
1158
+ if cmd = @commands[cmd_name]
1159
+ check_postmortem if !cmd.postmortem
1160
+ check_unsafe if cmd.unsafe
1161
+ cancel_auto_continue if cmd.cancel_auto_continue
1162
+ @repl_prev_line = nil if !cmd.repeat
1163
+
1164
+ cmd.block.call(cmd_arg)
1165
+ else
1166
+ @repl_prev_line = nil
1167
+ check_unsafe
1168
+
1169
+ request_eval :pp, line
1170
+ end
1171
+
1172
+ rescue Interrupt
1173
+ return :retry
1174
+ rescue SystemExit
1175
+ raise
1176
+ rescue PostmortemError => e
1177
+ @ui.puts e.message
1178
+ return :retry
1179
+ rescue Exception => e
1180
+ @ui.puts "[REPL ERROR] #{e.inspect}"
1181
+ @ui.puts e.backtrace.map{|e| ' ' + e}
1182
+ return :retry
1183
+ end
1184
+
1185
+ def step_command type, arg
1186
+ if type == :until
1187
+ leave_subsession [:step, type, arg]
1188
+ return
1189
+ end
1190
+
1191
+ case arg
1192
+ when nil, /\A\d+\z/
1193
+ if type == :in && @tc.recorder&.replaying?
1194
+ request_tc [:step, type, arg&.to_i]
1195
+ else
1196
+ leave_subsession [:step, type, arg&.to_i]
1197
+ end
1198
+ when /\A(back)\z/, /\A(back)\s+(\d+)\z/, /\A(reset)\z/
1199
+ if type != :in
1200
+ @ui.puts "only `step #{arg}` is supported."
1201
+ :retry
1202
+ else
1203
+ type = $1.to_sym
1204
+ iter = $2&.to_i
1205
+ request_tc [:step, type, iter]
1206
+ end
1207
+ else
1208
+ @ui.puts "Unknown option: #{arg}"
1209
+ :retry
1210
+ end
1211
+ end
1212
+
1213
+ def config_show key
1214
+ key = key.to_sym
1215
+ config_detail = CONFIG_SET[key]
1216
+
1217
+ if config_detail
1218
+ v = CONFIG[key]
1219
+ kv = "#{key} = #{v.inspect}"
1220
+ desc = config_detail[1]
1221
+
1222
+ if config_default = config_detail[3]
1223
+ desc += " (default: #{config_default})"
1224
+ end
1225
+
1226
+ line = "%-34s \# %s" % [kv, desc]
1227
+ if line.size > SESSION.width
1228
+ @ui.puts "\# #{desc}\n#{kv}"
1229
+ else
1230
+ @ui.puts line
1231
+ end
1232
+ else
1233
+ @ui.puts "Unknown configuration: #{key}. 'config' shows all configurations."
1234
+ end
1235
+ end
1236
+
1237
+ def config_set key, val, append: false
1238
+ if CONFIG_SET[key = key.to_sym]
1239
+ begin
1240
+ if append
1241
+ CONFIG.append_config(key, val)
1242
+ else
1243
+ CONFIG[key] = val
1244
+ end
1245
+ rescue => e
1246
+ @ui.puts e.message
1247
+ end
1248
+ end
1249
+
1250
+ config_show key
1251
+ end
1252
+
1253
+ def config_command arg
1254
+ case arg
1255
+ when nil
1256
+ CONFIG_SET.each do |k, _|
1257
+ config_show k
1258
+ end
1259
+
1260
+ when /\Aunset\s+(.+)\z/
1261
+ if CONFIG_SET[key = $1.to_sym]
1262
+ CONFIG[key] = nil
1263
+ end
1264
+ config_show key
1265
+
1266
+ when /\A(\w+)\s*=\s*(.+)\z/
1267
+ config_set $1, $2
1268
+
1269
+ when /\A\s*set\s+(\w+)\s+(.+)\z/
1270
+ config_set $1, $2
1271
+
1272
+ when /\A(\w+)\s*<<\s*(.+)\z/
1273
+ config_set $1, $2, append: true
1274
+
1275
+ when /\A\s*append\s+(\w+)\s+(.+)\z/
1276
+ config_set $1, $2, append: true
1277
+
1278
+ when /\A(\w+)\z/
1279
+ config_show $1
1280
+
1281
+ else
1282
+ @ui.puts "Can not parse parameters: #{arg}"
1283
+ end
1284
+ end
1285
+
1286
+
1287
+ def cancel_auto_continue
1288
+ if @preset_command&.auto_continue
1289
+ @preset_command.auto_continue = false
1290
+ end
1291
+ end
1292
+
1293
+ def show_help arg = nil
1294
+ instructions = (DEBUGGER__.commands.keys + DEBUGGER__.commands.values).uniq
1295
+ print_instructions = proc do |desc|
1296
+ desc.split("\n").each do |line|
1297
+ next if line.start_with?(" ") # workaround for step back
1298
+ formatted_line = line.gsub(/[\[\]\*]/, "").strip
1299
+ instructions.each do |inst|
1300
+ if formatted_line.start_with?("`#{inst}")
1301
+ desc.sub!(line, colorize(line, [:CYAN, :BOLD]))
1302
+ end
1303
+ end
1304
+ end
1305
+ @ui.puts desc
1306
+ end
1307
+
1308
+ print_category = proc do |cat|
1309
+ @ui.puts "\n"
1310
+ @ui.puts colorize("### #{cat}", [:GREEN, :BOLD])
1311
+ @ui.puts "\n"
1312
+ end
1313
+
1314
+ DEBUGGER__.helps.each { |cat, cs|
1315
+ # categories
1316
+ if arg.nil?
1317
+ print_category.call(cat)
1318
+ else
1319
+ cs.each { |ws, _|
1320
+ if ws.include?(arg)
1321
+ print_category.call(cat)
1322
+ break
1323
+ end
1324
+ }
1325
+ end
1326
+
1327
+ # instructions
1328
+ cs.each { |ws, desc|
1329
+ if arg.nil? || ws.include?(arg)
1330
+ print_instructions.call(desc.dup)
1331
+ return if arg
1332
+ end
1333
+ }
1334
+ }
1335
+
1336
+ @ui.puts "not found: #{arg}" if arg
1337
+ end
1338
+
1339
+ def ask msg, default = 'Y'
1340
+ opts = '[y/n]'.tr(default.downcase, default)
1341
+ input = @ui.ask("#{msg} #{opts} ")
1342
+ input = default if input.empty?
1343
+ case input
1344
+ when 'y', 'Y'
1345
+ true
1346
+ else
1347
+ false
1348
+ end
1349
+ end
1350
+
1351
+ # breakpoint management
1352
+
1353
+ def iterate_bps
1354
+ deleted_bps = []
1355
+ i = 0
1356
+ @bps.each{|key, bp|
1357
+ if !bp.deleted?
1358
+ yield key, bp, i
1359
+ i += 1
1360
+ else
1361
+ deleted_bps << bp
1362
+ end
1363
+ }
1364
+ ensure
1365
+ deleted_bps.each{|bp| @bps.delete bp}
1366
+ end
1367
+
1368
+ def show_bps specific_bp = nil
1369
+ iterate_bps do |key, bp, i|
1370
+ @ui.puts "#%d %s" % [i, bp.to_s] if !specific_bp || bp == specific_bp
1371
+ end
1372
+ end
1373
+
1374
+ def bp_index specific_bp_key
1375
+ iterate_bps do |key, bp, i|
1376
+ if key == specific_bp_key
1377
+ return [bp, i]
1378
+ end
1379
+ end
1380
+ nil
1381
+ end
1382
+
1383
+ def rehash_bps
1384
+ bps = @bps.values
1385
+ @bps.clear
1386
+ bps.each{|bp|
1387
+ add_bp bp
1388
+ }
1389
+ end
1390
+
1391
+ def clean_bps
1392
+ @bps.delete_if{|_k, bp|
1393
+ bp.deleted?
1394
+ }
1395
+ end
1396
+
1397
+ def add_bp bp
1398
+ # don't repeat commands that add breakpoints
1399
+ if @bps.has_key? bp.key
1400
+ if bp.duplicable?
1401
+ bp
1402
+ else
1403
+ @ui.puts "duplicated breakpoint: #{bp}"
1404
+ bp.disable
1405
+ nil
1406
+ end
1407
+ else
1408
+ @bps[bp.key] = bp
1409
+ end
1410
+ end
1411
+
1412
+ def delete_bp arg = nil
1413
+ case arg
1414
+ when nil
1415
+ @bps.each{|key, bp| bp.delete}
1416
+ @bps.clear
1417
+ else
1418
+ del_bp = nil
1419
+ iterate_bps{|key, bp, i| del_bp = bp if i == arg}
1420
+ if del_bp
1421
+ del_bp.delete
1422
+ @bps.delete del_bp.key
1423
+ return [arg, del_bp]
1424
+ end
1425
+ end
1426
+ end
1427
+
1428
+ BREAK_KEYWORDS = %w(if: do: pre: path:).freeze
1429
+
1430
+ private def parse_break type, arg
1431
+ mode = :sig
1432
+ expr = Hash.new{|h, k| h[k] = []}
1433
+ arg.split(' ').each{|w|
1434
+ if BREAK_KEYWORDS.any?{|pat| w == pat}
1435
+ mode = w[0..-2].to_sym
1436
+ else
1437
+ expr[mode] << w
1438
+ end
1439
+ }
1440
+ expr.default_proc = nil
1441
+ expr = expr.transform_values{|v| v.join(' ')}
1442
+
1443
+ if (path = expr[:path]) && path =~ /\A\/(.*)\/\z/
1444
+ expr[:path] = Regexp.compile($1)
1445
+ end
1446
+
1447
+ if expr[:do] || expr[:pre]
1448
+ check_unsafe
1449
+ expr[:cmd] = [type, expr[:pre], expr[:do]]
1450
+ end
1451
+
1452
+ expr
1453
+ end
1454
+
1455
+ def repl_add_breakpoint arg
1456
+ expr = parse_break 'break', arg.strip
1457
+ cond = expr[:if]
1458
+ cmd = expr[:cmd]
1459
+ path = expr[:path]
1460
+
1461
+ case expr[:sig]
1462
+ when /\A(\d+)\z/
1463
+ add_line_breakpoint @tc.location.path, $1.to_i, cond: cond, command: cmd
1464
+ when /\A(.+)[:\s+](\d+)\z/
1465
+ add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
1466
+ when /\A(.+)([\.\#])(.+)\z/
1467
+ request_tc [:breakpoint, :method, $1, $2, $3, cond, cmd, path]
1468
+ return :noretry
1469
+ when nil
1470
+ add_check_breakpoint cond, path, cmd
1471
+ else
1472
+ @ui.puts "Unknown breakpoint format: #{arg}"
1473
+ @ui.puts
1474
+ show_help 'b'
1475
+ end
1476
+ end
1477
+
1478
+ def repl_add_catch_breakpoint arg
1479
+ expr = parse_break 'catch', arg.strip
1480
+ cond = expr[:if]
1481
+ cmd = expr[:cmd]
1482
+ path = expr[:path]
1483
+
1484
+ bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd, path: path)
1485
+ add_bp bp
1486
+ end
1487
+
1488
+ def repl_add_watch_breakpoint arg
1489
+ expr = parse_break 'watch', arg.strip
1490
+ cond = expr[:if]
1491
+ cmd = expr[:cmd]
1492
+ path = Regexp.compile(expr[:path]) if expr[:path]
1493
+
1494
+ request_tc [:breakpoint, :watch, expr[:sig], cond, cmd, path]
1495
+ end
1496
+
1497
+ def add_catch_breakpoint pat, cond: nil
1498
+ bp = CatchBreakpoint.new(pat, cond: cond)
1499
+ add_bp bp
1500
+ end
1501
+
1502
+ def add_check_breakpoint cond, path, command
1503
+ bp = CheckBreakpoint.new(cond: cond, path: path, command: command)
1504
+ add_bp bp
1505
+ end
1506
+
1507
+ def add_line_breakpoint file, line, **kw
1508
+ file = resolve_path(file)
1509
+ bp = LineBreakpoint.new(file, line, **kw)
1510
+
1511
+ add_bp bp
1512
+ rescue Errno::ENOENT => e
1513
+ @ui.puts e.message
1514
+ end
1515
+
1516
+ def clear_breakpoints(&condition)
1517
+ @bps.delete_if do |k, bp|
1518
+ if condition.call(k, bp)
1519
+ bp.delete
1520
+ true
1521
+ end
1522
+ end
1523
+ end
1524
+
1525
+ def clear_line_breakpoints path
1526
+ path = resolve_path(path)
1527
+ clear_breakpoints do |k, bp|
1528
+ bp.is_a?(LineBreakpoint) && bp.path_is?(path)
1529
+ end
1530
+ rescue Errno::ENOENT
1531
+ # just ignore
1532
+ end
1533
+
1534
+ def clear_catch_breakpoints *exception_names
1535
+ clear_breakpoints do |k, bp|
1536
+ bp.is_a?(CatchBreakpoint) && exception_names.include?(k[1])
1537
+ end
1538
+ end
1539
+
1540
+ def clear_all_breakpoints
1541
+ clear_breakpoints{true}
1542
+ end
1543
+
1544
+ def add_iseq_breakpoint iseq, **kw
1545
+ bp = ISeqBreakpoint.new(iseq, [:line], **kw)
1546
+ add_bp bp
1547
+ end
1548
+
1549
+ # tracers
1550
+
1551
+ def add_tracer tracer
1552
+ if @tracers[tracer.key]&.enabled?
1553
+ tracer.disable
1554
+ @ui.puts "Duplicated tracer: #{tracer}"
1555
+ else
1556
+ @tracers[tracer.key] = tracer
1557
+ @ui.puts "Enable #{tracer}"
1558
+ end
1559
+ end
1560
+
1561
+ # threads
1562
+
1563
+ def update_thread_list
1564
+ list = Thread.list
1565
+ thcs = []
1566
+ unmanaged = []
1567
+
1568
+ list.each{|th|
1569
+ if thc = @th_clients[th]
1570
+ if !thc.management?
1571
+ thcs << thc
1572
+ end
1573
+ else
1574
+ unmanaged << th
1575
+ end
1576
+ }
1577
+
1578
+ return thcs.sort_by{|thc| thc.id}, unmanaged
1579
+ end
1580
+
1581
+ def thread_list
1582
+ thcs, unmanaged_ths = update_thread_list
1583
+ thcs.each_with_index{|thc, i|
1584
+ @ui.puts "#{@tc == thc ? "--> " : " "}\##{i} #{thc}"
1585
+ }
1586
+
1587
+ if !unmanaged_ths.empty?
1588
+ @ui.puts "The following threads are not managed yet by the debugger:"
1589
+ unmanaged_ths.each{|th|
1590
+ @ui.puts " " + th.to_s
1591
+ }
1592
+ end
1593
+ end
1594
+
1595
+ def managed_thread_clients
1596
+ thcs, _unmanaged_ths = update_thread_list
1597
+ thcs
1598
+ end
1599
+
1600
+ def switch_thread n
1601
+ thcs, _unmanaged_ths = update_thread_list
1602
+
1603
+ if tc = thcs[n]
1604
+ if tc.waiting?
1605
+ @tc = tc
1606
+ else
1607
+ @ui.puts "#{tc.thread} is not controllable yet."
1608
+ end
1609
+ end
1610
+ thread_list
1611
+ end
1612
+
1613
+ def setup_threads
1614
+ prev_clients = @th_clients
1615
+ @th_clients = {}
1616
+
1617
+ Thread.list.each{|th|
1618
+ if tc = prev_clients[th]
1619
+ @th_clients[th] = tc
1620
+ else
1621
+ create_thread_client(th)
1622
+ end
1623
+ }
1624
+ end
1625
+
1626
+ def on_thread_begin th
1627
+ if @th_clients.has_key? th
1628
+ # TODO: NG?
1629
+ else
1630
+ create_thread_client th
1631
+ end
1632
+ end
1633
+
1634
+ private def create_thread_client th
1635
+ # TODO: Ractor support
1636
+ raise "Only session_server can create thread_client" unless Thread.current == @session_server
1637
+ @th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th)
1638
+ end
1639
+
1640
+ private def ask_thread_client th
1641
+ # TODO: Ractor support
1642
+ q2 = Queue.new
1643
+ # tc, output, ev, @internal_info, *ev_args = evt
1644
+ @q_evt << [nil, [], :thread_begin, nil, th, q2]
1645
+ q2.pop
1646
+
1647
+ @th_clients[th] or raise "unexpected error"
1648
+ end
1649
+
1650
+ # can be called by other threads
1651
+ def get_thread_client th = Thread.current
1652
+ if @th_clients.has_key? th
1653
+ @th_clients[th]
1654
+ else
1655
+ if Thread.current == @session_server
1656
+ create_thread_client th
1657
+ else
1658
+ ask_thread_client th
1659
+ end
1660
+ end
1661
+ end
1662
+
1663
+ private def running_thread_clients_count
1664
+ @th_clients.count{|th, tc|
1665
+ next if tc.management?
1666
+ next unless tc.running?
1667
+ true
1668
+ }
1669
+ end
1670
+
1671
+ private def waiting_thread_clients
1672
+ @th_clients.map{|th, tc|
1673
+ next if tc.management?
1674
+ next unless tc.waiting?
1675
+ tc
1676
+ }.compact
1677
+ end
1678
+
1679
+ private def thread_stopper
1680
+ TracePoint.new(:line) do
1681
+ # run on each thread
1682
+ tc = ThreadClient.current
1683
+ next if tc.management?
1684
+ next unless tc.running?
1685
+ next if tc == @tc
1686
+
1687
+ tc.on_pause
1688
+ end
1689
+ end
1690
+
1691
+ private def stop_all_threads
1692
+ return if running_thread_clients_count == 0
1693
+
1694
+ stopper = @thread_stopper
1695
+ stopper.enable unless stopper.enabled?
1696
+ end
1697
+
1698
+ private def restart_all_threads
1699
+ stopper = @thread_stopper
1700
+ stopper.disable if stopper.enabled?
1701
+
1702
+ waiting_thread_clients.each{|tc|
1703
+ next if @tc == tc
1704
+ tc << :continue
1705
+ }
1706
+ end
1707
+
1708
+ private def enter_subsession
1709
+ @subsession_id += 1
1710
+ if !@subsession_stack.empty?
1711
+ DEBUGGER__.debug{ "Enter subsession (nested #{@subsession_stack.size})" }
1712
+ else
1713
+ DEBUGGER__.debug{ "Enter subsession" }
1714
+ stop_all_threads
1715
+ @process_group.lock
1716
+ end
1717
+
1718
+ @subsession_stack << true
1719
+ end
1720
+
1721
+ private def leave_subsession type
1722
+ raise '[BUG] leave_subsession: not entered' if @subsession_stack.empty?
1723
+ @subsession_stack.pop
1724
+
1725
+ if @subsession_stack.empty?
1726
+ DEBUGGER__.debug{ "Leave subsession" }
1727
+ @process_group.unlock
1728
+ restart_all_threads
1729
+ else
1730
+ DEBUGGER__.debug{ "Leave subsession (nested #{@subsession_stack.size})" }
1731
+ end
1732
+
1733
+ request_tc type if type
1734
+ @tc = nil
1735
+ rescue Exception => e
1736
+ STDERR.puts PP.pp([e, e.backtrace], ''.dup)
1737
+ raise
1738
+ end
1739
+
1740
+ def in_subsession?
1741
+ !@subsession_stack.empty?
1742
+ end
1743
+
1744
+ ## event
1745
+
1746
+ def on_load iseq, src
1747
+ DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}"
1748
+
1749
+ file_path, reloaded = @sr.add(iseq, src)
1750
+ @ui.event :load, file_path, reloaded
1751
+
1752
+ # check breakpoints
1753
+ if file_path
1754
+ @bps.find_all do |_key, bp|
1755
+ LineBreakpoint === bp && bp.path_is?(file_path) && (iseq.first_lineno..iseq.last_line).cover?(bp.line)
1756
+ end.each do |_key, bp|
1757
+ if !bp.iseq
1758
+ bp.try_activate iseq
1759
+ elsif reloaded
1760
+ @bps.delete bp.key # to allow duplicate
1761
+
1762
+ # When we delete a breakpoint from the @bps hash, we also need to deactivate it or else its tracepoint event
1763
+ # will continue to be enabled and we'll suspend on ghost breakpoints
1764
+ bp.delete
1765
+
1766
+ nbp = LineBreakpoint.copy(bp, iseq)
1767
+ add_bp nbp
1768
+ end
1769
+ end
1770
+ else # !file_path => file_path is not existing
1771
+ @bps.find_all do |_key, bp|
1772
+ LineBreakpoint === bp && !bp.iseq && DEBUGGER__.compare_path(bp.path, (iseq.absolute_path || iseq.path))
1773
+ end.each do |_key, bp|
1774
+ bp.try_activate iseq
1775
+ end
1776
+ end
1777
+ end
1778
+
1779
+ def resolve_path file
1780
+ File.realpath(File.expand_path(file))
1781
+ rescue Errno::ENOENT
1782
+ case file
1783
+ when '-e', '-'
1784
+ return file
1785
+ else
1786
+ $LOAD_PATH.each do |lp|
1787
+ libpath = File.join(lp, file)
1788
+ return File.realpath(libpath)
1789
+ rescue Errno::ENOENT
1790
+ # next
1791
+ end
1792
+ end
1793
+
1794
+ raise
1795
+ end
1796
+
1797
+ def method_added tp
1798
+ b = tp.binding
1799
+
1800
+ if var_name = b.local_variables.first
1801
+ mid = b.local_variable_get(var_name)
1802
+ resolved = true
1803
+
1804
+ @bps.each{|k, bp|
1805
+ case bp
1806
+ when MethodBreakpoint
1807
+ if bp.method.nil?
1808
+ if bp.sig_method_name == mid.to_s
1809
+ bp.try_enable(added: true)
1810
+ end
1811
+ end
1812
+
1813
+ resolved = false if !bp.enabled?
1814
+ end
1815
+ }
1816
+
1817
+ if resolved
1818
+ Session.deactivate_method_added_trackers
1819
+ end
1820
+
1821
+ case mid
1822
+ when :method_added, :singleton_method_added
1823
+ Session.create_method_added_tracker(tp.self, mid)
1824
+ Session.activate_method_added_trackers unless resolved
1825
+ end
1826
+ end
1827
+ end
1828
+
1829
+ class ::Module
1830
+ undef method_added
1831
+ def method_added mid; end
1832
+ end
1833
+
1834
+ class ::BasicObject
1835
+ undef singleton_method_added
1836
+ def singleton_method_added mid; end
1837
+ end
1838
+
1839
+ def self.create_method_added_tracker mod, method_added_id, method_accessor = :method
1840
+ m = mod.__send__(method_accessor, method_added_id)
1841
+ METHOD_ADDED_TRACKERS[m] = TracePoint.new(:call) do |tp|
1842
+ SESSION.method_added tp
1843
+ end
1844
+ end
1845
+
1846
+ def self.activate_method_added_trackers
1847
+ METHOD_ADDED_TRACKERS.each do |m, tp|
1848
+ tp.enable(target: m) unless tp.enabled?
1849
+ rescue ArgumentError
1850
+ DEBUGGER__.warn "Methods defined under #{m.owner} can not track by the debugger."
1851
+ end
1852
+ end
1853
+
1854
+ def self.deactivate_method_added_trackers
1855
+ METHOD_ADDED_TRACKERS.each do |m, tp|
1856
+ tp.disable if tp.enabled?
1857
+ end
1858
+ end
1859
+
1860
+ METHOD_ADDED_TRACKERS = Hash.new
1861
+ create_method_added_tracker Module, :method_added, :instance_method
1862
+ create_method_added_tracker BasicObject, :singleton_method_added, :instance_method
1863
+
1864
+ def width
1865
+ @ui.width
1866
+ end
1867
+
1868
+ def check_postmortem
1869
+ if @postmortem
1870
+ raise PostmortemError, "Can not use this command on postmortem mode."
1871
+ end
1872
+ end
1873
+
1874
+ def check_unsafe
1875
+ if @unsafe_context
1876
+ raise RuntimeError, "#{@repl_prev_line.dump} is not allowed on unsafe context."
1877
+ end
1878
+ end
1879
+
1880
+ def activate_irb_integration
1881
+ require_relative "irb_integration"
1882
+ thc = get_thread_client(@session_server)
1883
+ thc.activate_irb_integration
1884
+ end
1885
+
1886
+ def enter_postmortem_session exc
1887
+ return unless exc.instance_variable_defined? :@__debugger_postmortem_frames
1888
+
1889
+ frames = exc.instance_variable_get(:@__debugger_postmortem_frames)
1890
+ @postmortem = true
1891
+ ThreadClient.current.suspend :postmortem, postmortem_frames: frames, postmortem_exc: exc
1892
+ ensure
1893
+ @postmortem = false
1894
+ end
1895
+
1896
+ def capture_exception_frames *exclude_path
1897
+ postmortem_hook = TracePoint.new(:raise){|tp|
1898
+ exc = tp.raised_exception
1899
+ frames = DEBUGGER__.capture_frames(__dir__)
1900
+
1901
+ exclude_path.each{|ex|
1902
+ if Regexp === ex
1903
+ frames.delete_if{|e| ex =~ e.path}
1904
+ else
1905
+ frames.delete_if{|e| e.path.start_with? ex.to_s}
1906
+ end
1907
+ }
1908
+ exc.instance_variable_set(:@__debugger_postmortem_frames, frames)
1909
+ }
1910
+ postmortem_hook.enable
1911
+
1912
+ begin
1913
+ yield
1914
+ nil
1915
+ rescue Exception => e
1916
+ if e.instance_variable_defined? :@__debugger_postmortem_frames
1917
+ e
1918
+ else
1919
+ raise
1920
+ end
1921
+ ensure
1922
+ postmortem_hook.disable
1923
+ end
1924
+ end
1925
+
1926
+ def postmortem=(is_enable)
1927
+ if is_enable
1928
+ unless @postmortem_hook
1929
+ @postmortem_hook = TracePoint.new(:raise){|tp|
1930
+ exc = tp.raised_exception
1931
+ frames = DEBUGGER__.capture_frames(__dir__)
1932
+ exc.instance_variable_set(:@__debugger_postmortem_frames, frames)
1933
+ }
1934
+ at_exit{
1935
+ @postmortem_hook.disable
1936
+ if CONFIG[:postmortem] && (exc = $!) != nil
1937
+ exc = exc.cause while exc.cause
1938
+
1939
+ begin
1940
+ @ui.puts "Enter postmortem mode with #{exc.inspect}"
1941
+ @ui.puts exc.backtrace.map{|e| ' ' + e}
1942
+ @ui.puts "\n"
1943
+
1944
+ enter_postmortem_session exc
1945
+ rescue SystemExit
1946
+ exit!
1947
+ rescue Exception => e
1948
+ @ui = STDERR unless @ui
1949
+ @ui.puts "Error while postmortem console: #{e.inspect}"
1950
+ end
1951
+ end
1952
+ }
1953
+ end
1954
+
1955
+ if !@postmortem_hook.enabled?
1956
+ @postmortem_hook.enable
1957
+ end
1958
+ else
1959
+ if @postmortem_hook && @postmortem_hook.enabled?
1960
+ @postmortem_hook.disable
1961
+ end
1962
+ end
1963
+ end
1964
+
1965
+ def set_no_sigint_hook old, new
1966
+ return unless old != new
1967
+ return unless @ui.respond_to? :activate_sigint
1968
+
1969
+ if old # no -> yes
1970
+ @ui.activate_sigint
1971
+ else
1972
+ @ui.deactivate_sigint
1973
+ end
1974
+ end
1975
+
1976
+ def save_int_trap cmd
1977
+ prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, cmd
1978
+ prev
1979
+ end
1980
+
1981
+ def intercept_trap_sigint?
1982
+ @intercept_trap_sigint
1983
+ end
1984
+
1985
+ def intercept_trap_sigint flag, &b
1986
+ prev = @intercept_trap_sigint
1987
+ @intercept_trap_sigint = flag
1988
+ yield
1989
+ ensure
1990
+ @intercept_trap_sigint = prev
1991
+ end
1992
+
1993
+ def intercept_trap_sigint_start prev
1994
+ @intercept_trap_sigint = true
1995
+ @intercepted_sigint_cmd = prev
1996
+ end
1997
+
1998
+ def intercept_trap_sigint_end
1999
+ @intercept_trap_sigint = false
2000
+ prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, nil
2001
+ prev
2002
+ end
2003
+
2004
+ def process_info
2005
+ if @process_group.multi?
2006
+ "#{$0}\##{Process.pid}"
2007
+ end
2008
+ end
2009
+
2010
+ def before_fork need_lock = true
2011
+ if need_lock
2012
+ @process_group.multi_process!
2013
+ end
2014
+ end
2015
+
2016
+ def after_fork_parent
2017
+ @ui.after_fork_parent
2018
+ end
2019
+
2020
+ # experimental API
2021
+ def extend_feature session: nil, thread_client: nil, ui: nil
2022
+ Session.include session if session
2023
+ ThreadClient.include thread_client if thread_client
2024
+ @ui.extend ui if ui
2025
+ end
2026
+ end
2027
+
2028
+ class ProcessGroup
2029
+ def initialize
2030
+ @lock_file = nil
2031
+ end
2032
+
2033
+ def locked?
2034
+ true
2035
+ end
2036
+
2037
+ def trylock
2038
+ true
2039
+ end
2040
+
2041
+ def lock
2042
+ true
2043
+ end
2044
+
2045
+ def unlock
2046
+ true
2047
+ end
2048
+
2049
+ def sync
2050
+ yield
2051
+ end
2052
+
2053
+ def after_fork
2054
+ end
2055
+
2056
+ def multi?
2057
+ @lock_file
2058
+ end
2059
+
2060
+ def multi_process!
2061
+ require 'tempfile'
2062
+ @lock_tempfile = Tempfile.open("ruby-debug-lock-")
2063
+ @lock_tempfile.close
2064
+ extend MultiProcessGroup
2065
+ end
2066
+ end
2067
+
2068
+ module MultiProcessGroup
2069
+ def multi_process!
2070
+ end
2071
+
2072
+ def after_fork child: true
2073
+ if child || !@lock_file
2074
+ @m = Mutex.new unless @m
2075
+ @m.synchronize do
2076
+ @lock_level = 0
2077
+ @lock_file = open(@lock_tempfile.path, 'w')
2078
+ end
2079
+ end
2080
+ end
2081
+
2082
+ def info msg
2083
+ DEBUGGER__.info "#{msg} (#{@lock_level})" # #{caller.first(1).map{|bt| bt.sub(__dir__, '')}}"
2084
+ end
2085
+
2086
+ def locked?
2087
+ # DEBUGGER__.debug{ "locked? #{@lock_level}" }
2088
+ @lock_level > 0
2089
+ end
2090
+
2091
+ private def lock_level_up
2092
+ raise unless @m.owned?
2093
+ @lock_level += 1
2094
+ end
2095
+
2096
+ private def lock_level_down
2097
+ raise unless @m.owned?
2098
+ raise "@lock_level underflow: #{@lock_level}" if @lock_level < 1
2099
+ @lock_level -= 1
2100
+ end
2101
+
2102
+ private def trylock
2103
+ @m.synchronize do
2104
+ if locked?
2105
+ lock_level_up
2106
+ info "Try lock, already locked"
2107
+ true
2108
+ else
2109
+ case r = @lock_file.flock(File::LOCK_EX | File::LOCK_NB)
2110
+ when 0
2111
+ lock_level_up
2112
+ info "Try lock with file: success"
2113
+ true
2114
+ when false
2115
+ info "Try lock with file: failed"
2116
+ false
2117
+ else
2118
+ raise "unknown flock result: #{r.inspect}"
2119
+ end
2120
+ end
2121
+ end
2122
+ end
2123
+
2124
+ def lock
2125
+ unless trylock
2126
+ @m.synchronize do
2127
+ if locked?
2128
+ lock_level_up
2129
+ else
2130
+ info "Lock: block"
2131
+ @lock_file.flock(File::LOCK_EX)
2132
+ lock_level_up
2133
+ end
2134
+ end
2135
+
2136
+ info "Lock: success"
2137
+ end
2138
+ end
2139
+
2140
+ def unlock
2141
+ @m.synchronize do
2142
+ raise "lock file is not opened (#{@lock_file.inspect})" if @lock_file.closed?
2143
+ lock_level_down
2144
+ @lock_file.flock(File::LOCK_UN) unless locked?
2145
+ info "Unlocked"
2146
+ end
2147
+ end
2148
+
2149
+ def sync &b
2150
+ info "sync"
2151
+
2152
+ lock
2153
+ begin
2154
+ b.call if b
2155
+ ensure
2156
+ unlock
2157
+ end
2158
+ end
2159
+ end
2160
+
2161
+ class UI_Base
2162
+ def event type, *args
2163
+ case type
2164
+ when :suspend_bp
2165
+ i, bp = *args
2166
+ puts "\nStop by \##{i} #{bp}" if bp
2167
+ when :suspend_trap
2168
+ puts "\nStop by #{args.first}"
2169
+ end
2170
+ end
2171
+
2172
+ def ignore_output_on_suspend?
2173
+ false
2174
+ end
2175
+
2176
+ def flush
2177
+ end
2178
+ end
2179
+
2180
+ # manual configuration methods
2181
+
2182
+ def self.add_line_breakpoint file, line, **kw
2183
+ ::DEBUGGER__::SESSION.add_line_breakpoint file, line, **kw
2184
+ end
2185
+
2186
+ def self.add_catch_breakpoint pat
2187
+ ::DEBUGGER__::SESSION.add_catch_breakpoint pat
2188
+ end
2189
+
2190
+ # String for requiring location
2191
+ # nil for -r
2192
+ def self.require_location
2193
+ locs = caller_locations
2194
+ dir_prefix = /#{Regexp.escape(__dir__)}/
2195
+
2196
+ locs.each do |loc|
2197
+ case loc.absolute_path
2198
+ when dir_prefix
2199
+ when %r{rubygems/core_ext/kernel_require\.rb}
2200
+ when %r{bundled_gems\.rb}
2201
+ else
2202
+ return loc if loc.absolute_path
2203
+ end
2204
+ end
2205
+ nil
2206
+ end
2207
+
2208
+ # start methods
2209
+
2210
+ def self.start nonstop: false, **kw
2211
+ CONFIG.set_config(**kw)
2212
+
2213
+ if CONFIG[:open]
2214
+ open nonstop: nonstop, **kw
2215
+ else
2216
+ unless defined? SESSION
2217
+ require_relative 'local'
2218
+ initialize_session{ UI_LocalConsole.new }
2219
+ end
2220
+ setup_initial_suspend unless nonstop
2221
+ end
2222
+ end
2223
+
2224
+ def self.open host: nil, port: CONFIG[:port], sock_path: nil, sock_dir: nil, nonstop: false, **kw
2225
+ CONFIG.set_config(**kw)
2226
+ require_relative 'server'
2227
+
2228
+ if port || CONFIG[:open] == 'chrome' || (!::Addrinfo.respond_to?(:unix))
2229
+ open_tcp host: host, port: (port || 0), nonstop: nonstop
2230
+ else
2231
+ open_unix sock_path: sock_path, sock_dir: sock_dir, nonstop: nonstop
2232
+ end
2233
+ end
2234
+
2235
+ def self.open_tcp host: nil, port:, nonstop: false, **kw
2236
+ CONFIG.set_config(**kw)
2237
+ require_relative 'server'
2238
+
2239
+ if defined? SESSION
2240
+ SESSION.reset_ui UI_TcpServer.new(host: host, port: port)
2241
+ else
2242
+ initialize_session{ UI_TcpServer.new(host: host, port: port) }
2243
+ end
2244
+
2245
+ setup_initial_suspend unless nonstop
2246
+ end
2247
+
2248
+ def self.open_unix sock_path: nil, sock_dir: nil, nonstop: false, **kw
2249
+ CONFIG.set_config(**kw)
2250
+ require_relative 'server'
2251
+
2252
+ if defined? SESSION
2253
+ SESSION.reset_ui UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path)
2254
+ else
2255
+ initialize_session{ UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path) }
2256
+ end
2257
+
2258
+ setup_initial_suspend unless nonstop
2259
+ end
2260
+
2261
+ # boot utilities
2262
+
2263
+ def self.setup_initial_suspend
2264
+ if !CONFIG[:nonstop]
2265
+ case
2266
+ when CONFIG[:stop_at_load]
2267
+ add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true, hook_call: false
2268
+ nil # stop here
2269
+ when path = ENV['RUBY_DEBUG_INITIAL_SUSPEND_PATH']
2270
+ add_line_breakpoint path, 0, oneshot: true, hook_call: false
2271
+ when loc = ::DEBUGGER__.require_location
2272
+ # require 'debug/start' or 'debug'
2273
+ add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
2274
+ else
2275
+ # -r
2276
+ add_line_breakpoint $0, 0, oneshot: true, hook_call: false
2277
+ end
2278
+ end
2279
+ end
2280
+
2281
+ class << self
2282
+ define_method :initialize_session do |&init_ui|
2283
+ DEBUGGER__.info "Session start (pid: #{Process.pid})"
2284
+ ::DEBUGGER__.const_set(:SESSION, Session.new)
2285
+ SESSION.activate init_ui.call
2286
+ load_rc
2287
+ end
2288
+ end
2289
+
2290
+ # Exiting control
2291
+
2292
+ class << self
2293
+ def skip_all
2294
+ @skip_all = true
2295
+ end
2296
+
2297
+ def skip?
2298
+ @skip_all
2299
+ end
2300
+ end
2301
+
2302
+ def self.load_rc
2303
+ rc_file_paths = [
2304
+ [File.expand_path('~/.rdbgrc'), true],
2305
+ [File.expand_path('~/.rdbgrc.rb'), true],
2306
+ # ['./.rdbgrc', true], # disable because of security concern
2307
+ ]
2308
+
2309
+ if (xdg_home = ENV["XDG_CONFIG_HOME"])
2310
+ rc_file_paths << [File.expand_path(File.join(xdg_home, "rdbg", "config")), true]
2311
+ rc_file_paths << [File.expand_path(File.join(xdg_home, "rdbg", "config.rb")), true]
2312
+ end
2313
+
2314
+ rc_file_paths << [CONFIG[:init_script], false]
2315
+
2316
+ rc_file_paths.each{|(path, rc)|
2317
+ next unless path
2318
+ next if rc && CONFIG[:no_rc] # ignore rc
2319
+
2320
+ if File.file? path
2321
+ if path.end_with?('.rb')
2322
+ load path
2323
+ else
2324
+ ::DEBUGGER__::SESSION.add_preset_commands path, File.readlines(path)
2325
+ end
2326
+ elsif !rc
2327
+ warn "Not found: #{path}"
2328
+ end
2329
+ }
2330
+
2331
+ # given debug commands
2332
+ if CONFIG[:commands]
2333
+ cmds = CONFIG[:commands].split(';;')
2334
+ ::DEBUGGER__::SESSION.add_preset_commands "commands", cmds, kick: false, continue: false
2335
+ end
2336
+ end
2337
+
2338
+ # Inspector
2339
+
2340
+ SHORT_INSPECT_LENGTH = 40
2341
+
2342
+ class LimitedPP
2343
+ def self.pp(obj, max=80)
2344
+ out = self.new(max)
2345
+ catch out do
2346
+ PP.singleline_pp(obj, out)
2347
+ end
2348
+ out.buf
2349
+ end
2350
+
2351
+ attr_reader :buf
2352
+
2353
+ def initialize max
2354
+ @max = max
2355
+ @cnt = 0
2356
+ @buf = String.new
2357
+ end
2358
+
2359
+ def <<(other)
2360
+ @buf << other
2361
+
2362
+ if @buf.size >= @max
2363
+ @buf = @buf[0..@max] + '...'
2364
+ throw self
2365
+ end
2366
+ end
2367
+ end
2368
+
2369
+ def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false
2370
+ if short
2371
+ LimitedPP.pp(obj, max_length)
2372
+ else
2373
+ obj.inspect
2374
+ end
2375
+ rescue NoMethodError => e
2376
+ klass, oid = M_CLASS.bind_call(obj), M_OBJECT_ID.bind_call(obj)
2377
+ if obj == (r = e.receiver)
2378
+ "<\##{klass.name}#{oid} does not have \#inspect>"
2379
+ else
2380
+ rklass, roid = M_CLASS.bind_call(r), M_OBJECT_ID.bind_call(r)
2381
+ "<\##{klass.name}:#{roid} contains <\##{rklass}:#{roid} and it does not have #inspect>"
2382
+ end
2383
+ rescue Exception => e
2384
+ "<#inspect raises #{e.inspect}>"
2385
+ end
2386
+
2387
+ def self.warn msg
2388
+ log :WARN, msg
2389
+ end
2390
+
2391
+ def self.info msg
2392
+ log :INFO, msg
2393
+ end
2394
+
2395
+ def self.check_loglevel level
2396
+ lv = LOG_LEVELS[level]
2397
+ config_lv = LOG_LEVELS[CONFIG[:log_level]]
2398
+ lv <= config_lv
2399
+ end
2400
+
2401
+ def self.debug(&b)
2402
+ if check_loglevel :DEBUG
2403
+ log :DEBUG, b.call
2404
+ end
2405
+ end
2406
+
2407
+ def self.log level, msg
2408
+ if check_loglevel level
2409
+ @logfile = STDERR unless defined? @logfile
2410
+ return if @logfile.closed?
2411
+
2412
+ if defined? SESSION
2413
+ pi = SESSION.process_info
2414
+ process_info = pi ? "[#{pi}]" : nil
2415
+ end
2416
+
2417
+ if level == :WARN
2418
+ # :WARN on debugger is general information
2419
+ @logfile.puts "DEBUGGER#{process_info}: #{msg}"
2420
+ @logfile.flush
2421
+ else
2422
+ @logfile.puts "DEBUGGER#{process_info} (#{level}): #{msg}"
2423
+ @logfile.flush
2424
+ end
2425
+ end
2426
+ end
2427
+
2428
+ def self.step_in &b
2429
+ if defined?(SESSION) && SESSION.active?
2430
+ SESSION.add_iseq_breakpoint RubyVM::InstructionSequence.of(b), oneshot: true
2431
+ end
2432
+
2433
+ yield
2434
+ end
2435
+
2436
+ if File.identical?(__FILE__.upcase, __FILE__.downcase)
2437
+ # For case insensitive file system (like Windows)
2438
+ # Note that this check is not enough because case sensitive/insensitive is
2439
+ # depend on the file system. So this check is only roughly estimation.
2440
+
2441
+ def self.compare_path(a, b)
2442
+ a&.downcase == b&.downcase
2443
+ end
2444
+ else
2445
+ def self.compare_path(a, b)
2446
+ a == b
2447
+ end
2448
+ end
2449
+
2450
+ module ForkInterceptor
2451
+ if Process.respond_to? :_fork
2452
+ def _fork
2453
+ return super unless defined?(SESSION) && SESSION.active?
2454
+
2455
+ parent_hook, child_hook = __fork_setup_for_debugger
2456
+
2457
+ super.tap do |pid|
2458
+ if pid != 0
2459
+ # after fork: parent
2460
+ parent_hook.call pid
2461
+ else
2462
+ # after fork: child
2463
+ child_hook.call
2464
+ end
2465
+ end
2466
+ end
2467
+ else
2468
+ def fork(&given_block)
2469
+ return super unless defined?(SESSION) && SESSION.active?
2470
+ parent_hook, child_hook = __fork_setup_for_debugger
2471
+
2472
+ if given_block
2473
+ new_block = proc {
2474
+ # after fork: child
2475
+ child_hook.call
2476
+ given_block.call
2477
+ }
2478
+ super(&new_block).tap{|pid| parent_hook.call(pid)}
2479
+ else
2480
+ super.tap do |pid|
2481
+ if pid
2482
+ # after fork: parent
2483
+ parent_hook.call pid
2484
+ else
2485
+ # after fork: child
2486
+ child_hook.call
2487
+ end
2488
+ end
2489
+ end
2490
+ end
2491
+ end
2492
+
2493
+ module DaemonInterceptor
2494
+ def daemon(*args)
2495
+ return super unless defined?(SESSION) && SESSION.active?
2496
+
2497
+ _, child_hook = __fork_setup_for_debugger(:child)
2498
+
2499
+ unless SESSION.remote?
2500
+ DEBUGGER__.warn "Can't debug the code after Process.daemon locally. Use the remote debugging feature."
2501
+ end
2502
+
2503
+ super.tap do
2504
+ child_hook.call
2505
+ end
2506
+ end
2507
+ end
2508
+
2509
+ private def __fork_setup_for_debugger fork_mode = nil
2510
+ fork_mode ||= CONFIG[:fork_mode]
2511
+
2512
+ if fork_mode == :both && CONFIG[:parent_on_fork]
2513
+ fork_mode = :parent
2514
+ end
2515
+
2516
+ parent_pid = Process.pid
2517
+
2518
+ # before fork
2519
+ case fork_mode
2520
+ when :parent
2521
+ parent_hook = -> child_pid {
2522
+ # Do nothing
2523
+ }
2524
+ child_hook = -> {
2525
+ DEBUGGER__.info "Detaching after fork from child process #{Process.pid}"
2526
+ SESSION.deactivate
2527
+ }
2528
+ when :child
2529
+ SESSION.before_fork false
2530
+
2531
+ parent_hook = -> child_pid {
2532
+ DEBUGGER__.info "Detaching after fork from parent process #{Process.pid}"
2533
+ SESSION.after_fork_parent
2534
+ SESSION.deactivate
2535
+ }
2536
+ child_hook = -> {
2537
+ DEBUGGER__.info "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2538
+ SESSION.activate on_fork: true
2539
+ }
2540
+ when :both
2541
+ SESSION.before_fork
2542
+
2543
+ parent_hook = -> child_pid {
2544
+ SESSION.process_group.after_fork
2545
+ SESSION.after_fork_parent
2546
+ }
2547
+ child_hook = -> {
2548
+ DEBUGGER__.info "Attaching after process #{parent_pid} fork to child process #{Process.pid}"
2549
+ SESSION.process_group.after_fork child: true
2550
+ SESSION.activate on_fork: true
2551
+ }
2552
+ end
2553
+
2554
+ return parent_hook, child_hook
2555
+ end
2556
+ end
2557
+
2558
+ module TrapInterceptor
2559
+ def trap sig, *command, &command_proc
2560
+ sym =
2561
+ case sig
2562
+ when String
2563
+ sig.to_sym
2564
+ when Integer
2565
+ Signal.signame(sig)&.to_sym
2566
+ else
2567
+ sig
2568
+ end
2569
+
2570
+ case sym
2571
+ when :INT, :SIGINT
2572
+ if defined?(SESSION) && SESSION.active? && SESSION.intercept_trap_sigint?
2573
+ return SESSION.save_int_trap(command.empty? ? command_proc : command.first)
2574
+ end
2575
+ end
2576
+
2577
+ super
2578
+ end
2579
+ end
2580
+
2581
+ if Process.respond_to? :_fork
2582
+ module ::Process
2583
+ class << self
2584
+ prepend ForkInterceptor
2585
+ prepend DaemonInterceptor
2586
+ end
2587
+ end
2588
+
2589
+ # trap
2590
+ module ::Kernel
2591
+ prepend TrapInterceptor
2592
+ end
2593
+ module ::Signal
2594
+ class << self
2595
+ prepend TrapInterceptor
2596
+ end
2597
+ end
2598
+ else
2599
+ if RUBY_VERSION >= '3.0.0'
2600
+ module ::Kernel
2601
+ prepend ForkInterceptor
2602
+ prepend TrapInterceptor
2603
+ end
2604
+ else
2605
+ class ::Object
2606
+ include ForkInterceptor
2607
+ include TrapInterceptor
2608
+ end
2609
+ end
2610
+
2611
+ module ::Kernel
2612
+ class << self
2613
+ prepend ForkInterceptor
2614
+ prepend TrapInterceptor
2615
+ end
2616
+ end
2617
+
2618
+ module ::Process
2619
+ class << self
2620
+ prepend ForkInterceptor
2621
+ prepend DaemonInterceptor
2622
+ end
2623
+ end
2624
+ end
2625
+
2626
+ module ::Signal
2627
+ class << self
2628
+ prepend TrapInterceptor
2629
+ end
2630
+ end
2631
+ end
2632
+
2633
+ module Kernel
2634
+ def debugger pre: nil, do: nil, up_level: 0
2635
+ return if !defined?(::DEBUGGER__::SESSION) || !::DEBUGGER__::SESSION.active?
2636
+
2637
+ if pre || (do_expr = binding.local_variable_get(:do))
2638
+ cmds = ['#debugger', pre, do_expr]
2639
+ end
2640
+
2641
+ if ::DEBUGGER__::SESSION.in_subsession?
2642
+ if cmds
2643
+ commands = [*cmds[1], *cmds[2]].map{|c| c.split(';;').join("\n")}
2644
+ ::DEBUGGER__::SESSION.add_preset_commands cmds[0], commands, kick: false, continue: false
2645
+ end
2646
+ else
2647
+ loc = caller_locations(up_level, 1).first; ::DEBUGGER__.add_line_breakpoint loc.path, loc.lineno + 1, oneshot: true, command: cmds
2648
+ end
2649
+ self
2650
+ end
2651
+
2652
+ alias bb debugger if ENV['RUBY_DEBUG_BB']
2653
+ end
2654
+
2655
+ class Binding
2656
+ alias break debugger
2657
+ alias b debugger
2658
+ end
2659
+
2660
+ # for Ruby 2.6 compatibility
2661
+ unless method(:p).unbind.respond_to? :bind_call
2662
+ class UnboundMethod
2663
+ def bind_call(obj, *args)
2664
+ self.bind(obj).call(*args)
2665
+ end
2666
+ end
2667
+ end