debug 0.2.0 → 1.0.0.beta3

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,1071 @@
1
+ module DEBUGGER__
2
+ # used in thread_client.c
3
+ FrameInfo = Struct.new(:location, :self, :binding, :iseq, :class, :frame_depth,
4
+ :has_return_value, :return_value, :show_line)
5
+ end
6
+
7
+ if File.exist? File.join(__dir__, 'debug.so')
8
+ require_relative 'debug.so'
9
+ else
10
+ require "debug/debug"
11
+ end
12
+
13
+ require_relative 'source_repository'
14
+ require_relative 'breakpoint'
15
+ require_relative 'thread_client'
16
+ require_relative 'config'
17
+
18
+ class RubyVM::InstructionSequence
19
+ def traceable_lines_norec lines
20
+ code = self.to_a[13]
21
+ line = 0
22
+ code.each{|e|
23
+ case e
24
+ when Integer
25
+ line = e
26
+ when Symbol
27
+ if /\ARUBY_EVENT_/ =~ e.to_s
28
+ lines[line] = [e, *lines[line]]
29
+ end
30
+ end
31
+ }
32
+ end
33
+
34
+ def traceable_lines_rec lines
35
+ self.each_child{|ci| ci.traceable_lines_rec(lines)}
36
+ traceable_lines_norec lines
37
+ end
38
+
39
+ def type
40
+ self.to_a[9]
41
+ end
42
+
43
+ def argc
44
+ self.to_a[4][:arg_size]
45
+ end
46
+
47
+ def locals
48
+ self.to_a[10]
49
+ end
50
+
51
+ def last_line
52
+ self.to_a[4][:code_location][2]
53
+ end
54
+ end
55
+
56
+ module DEBUGGER__
57
+ class Session
58
+ def initialize ui
59
+ @ui = ui
60
+ @sr = SourceRepository.new
61
+ @bps = {} # bp.key => bp
62
+ # [file, line] => LineBreakpoint
63
+ # "Error" => CatchBreakpoint
64
+ # Method => MethodBreakpoint
65
+ # [:watch, expr] => WatchExprBreakpoint
66
+ # [:check, expr] => CheckBreakpoint
67
+ @th_clients = {} # {Thread => ThreadClient}
68
+ @q_evt = Queue.new
69
+ @displays = []
70
+ @tc = nil
71
+ @tc_id = 0
72
+ @initial_commands = []
73
+
74
+ @tp_load_script = TracePoint.new(:script_compiled){|tp|
75
+ ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
76
+ }.enable
77
+
78
+ @session_server = Thread.new do
79
+ Thread.current.abort_on_exception = true
80
+
81
+ while evt = @q_evt.pop
82
+ tc, output, ev, *ev_args = evt
83
+ output.each{|str| @ui.puts str}
84
+
85
+ case ev
86
+ when :load
87
+ iseq, src = ev_args
88
+ on_load iseq, src
89
+ tc << :continue
90
+ when :thread_begin
91
+ th = ev_args.shift
92
+ on_thread_begin th
93
+ tc << :continue
94
+ when :suspend
95
+ case ev_args.first
96
+ when :breakpoint
97
+ bp, i = bp_index ev_args[1]
98
+ if bp
99
+ @ui.puts "\nStop by \##{i} #{bp}"
100
+ end
101
+ when :trap
102
+ @ui.puts ''
103
+ @ui.puts "\nStop by #{ev_args[1]}"
104
+ end
105
+
106
+ if @displays.empty?
107
+ wait_command_loop tc
108
+ else
109
+ tc << [:eval, :display, @displays]
110
+ end
111
+ when :result
112
+ case ev_args.first
113
+ when :watch
114
+ bp = ev_args[1]
115
+ @bps[bp.key] = bp
116
+ show_bps bp
117
+ when :try_display
118
+ failed_results = ev_args[1]
119
+ if failed_results.size > 0
120
+ i, msg = failed_results.last
121
+ if i+1 == @displays.size
122
+ @ui.puts "canceled: #{@displays.pop}"
123
+ end
124
+ end
125
+ when :method_breakpoint
126
+ bp = ev_args[1]
127
+ if bp
128
+ @bps[bp.key] = bp
129
+ show_bps bp
130
+ else
131
+ # can't make a bp
132
+ end
133
+ else
134
+ # ignore
135
+ end
136
+
137
+ wait_command_loop tc
138
+ end
139
+ end
140
+ ensure
141
+ @bps.each{|k, bp| bp.disable}
142
+ @th_clients.each{|th, thc| thc.close}
143
+ end
144
+
145
+ @management_threads = [@session_server]
146
+ @management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread
147
+
148
+ setup_threads
149
+
150
+ @tp_thread_begin = TracePoint.new(:thread_begin){|tp|
151
+ ThreadClient.current.on_thread_begin Thread.current
152
+ }.enable
153
+ end
154
+
155
+ def add_initial_commands cmds
156
+ cmds.each{|c|
157
+ c.gsub('#.*', '').strip!
158
+ @initial_commands << c unless c.empty?
159
+ }
160
+ end
161
+
162
+ def source path
163
+ @sr.get(path)
164
+ end
165
+
166
+ def inspect
167
+ "DEBUGGER__::SESSION"
168
+ end
169
+
170
+ def wait_command_loop tc
171
+ @tc = tc
172
+ stop_all_threads do
173
+ loop do
174
+ case wait_command
175
+ when :retry
176
+ # nothing
177
+ else
178
+ break
179
+ end
180
+ rescue Interrupt
181
+ retry
182
+ end
183
+ end
184
+ ensure
185
+ @tc = nil
186
+ end
187
+
188
+ def wait_command
189
+ if @initial_commands.empty?
190
+ line = @ui.readline
191
+ else
192
+ line = @initial_commands.shift.strip
193
+ @ui.puts "(rdbg:init) #{line}"
194
+ end
195
+
196
+ if line.empty?
197
+ if @repl_prev_line
198
+ line = @repl_prev_line
199
+ else
200
+ return :retry
201
+ end
202
+ else
203
+ @repl_prev_line = line
204
+ end
205
+
206
+ /([^\s]+)(?:\s+(.+))?/ =~ line
207
+ cmd, arg = $1, $2
208
+
209
+ # p cmd: [cmd, *arg]
210
+
211
+ case cmd
212
+ ### Control flow
213
+
214
+ # * `s[tep]`
215
+ # * Step in. Resume the program until next breakable point.
216
+ when 's', 'step'
217
+ @tc << [:step, :in]
218
+
219
+ # * `n[ext]`
220
+ # * Step over. Resume the program until next line.
221
+ when 'n', 'next'
222
+ @tc << [:step, :next]
223
+
224
+ # * `fin[ish]`
225
+ # * Finish this frame. Resume the program until the current frame is finished.
226
+ when 'fin', 'finish'
227
+ @tc << [:step, :finish]
228
+
229
+ # * `c[ontinue]`
230
+ # * Resume the program.
231
+ when 'c', 'continue'
232
+ @tc << :continue
233
+
234
+ # * `q[uit]` or `Ctrl-D`
235
+ # * Finish debugger (with the debuggee process on non-remote debugging).
236
+ when 'q', 'quit'
237
+ if ask 'Really quit?'
238
+ @ui.quit arg.to_i
239
+ @tc << :continue
240
+ else
241
+ return :retry
242
+ end
243
+
244
+ # * `q[uit]!`
245
+ # * Same as q[uit] but without the confirmation prompt.
246
+ when 'q!', 'quit!'
247
+ @ui.quit arg.to_i
248
+ @tc << :continue
249
+
250
+ # * `kill`
251
+ # * Stop the debuggee process with `Kernal#exit!`.
252
+ when 'kill'
253
+ if ask 'Really kill?'
254
+ exit! (arg || 1).to_i
255
+ else
256
+ return :retry
257
+ end
258
+
259
+ # * `kill!`
260
+ # * Same as kill but without the confirmation prompt.
261
+ when 'kill!'
262
+ exit! (arg || 1).to_i
263
+
264
+ ### Breakpoint
265
+
266
+ # * `b[reak]`
267
+ # * Show all breakpoints.
268
+ # * `b[reak] <line>`
269
+ # * Set breakpoint on `<line>` at the current frame's file.
270
+ # * `b[reak] <file>:<line>` or `<file> <line>`
271
+ # * Set breakpoint on `<file>:<line>`.
272
+ # * `b[reak] <class>#<name>`
273
+ # * Set breakpoint on the method `<class>#<name>`.
274
+ # * `b[reak] <expr>.<name>`
275
+ # * Set breakpoint on the method `<expr>.<name>`.
276
+ # * `b[reak] ... if <expr>`
277
+ # * break if `<expr>` is true at specified location.
278
+ # * `b[reak] if <expr>`
279
+ # * break if `<expr>` is true at any lines.
280
+ # * Note that this feature is super slow.
281
+ when 'b', 'break'
282
+ if arg == nil
283
+ show_bps
284
+ return :retry
285
+ else
286
+ case bp = repl_add_breakpoint(arg)
287
+ when :noretry
288
+ when nil
289
+ return :retry
290
+ else
291
+ show_bps bp
292
+ return :retry
293
+ end
294
+ end
295
+
296
+ # skip
297
+ when 'bv'
298
+ require 'json'
299
+
300
+ h = Hash.new{|h, k| h[k] = []}
301
+ @bps.each{|key, bp|
302
+ if LineBreakpoint === bp
303
+ h[bp.path] << {lnum: bp.line}
304
+ end
305
+ }
306
+ if h.empty?
307
+ # TODO: clean?
308
+ else
309
+ open(".rdb_breakpoints.json", 'w'){|f| JSON.dump(h, f)}
310
+ end
311
+
312
+ vimsrc = File.join(__dir__, 'bp.vim')
313
+ system("vim -R -S #{vimsrc} #{@tc.location.path}")
314
+
315
+ if File.exist?(".rdb_breakpoints.json")
316
+ pp JSON.load(File.read(".rdb_breakpoints.json"))
317
+ end
318
+
319
+ return :retry
320
+
321
+ # * `catch <Error>`
322
+ # * Set breakpoint on raising `<Error>`.
323
+ when 'catch'
324
+ if arg
325
+ bp = add_catch_breakpoint arg
326
+ show_bps bp if bp
327
+ else
328
+ show_bps
329
+ end
330
+ return :retry
331
+
332
+ # * `watch <expr>`
333
+ # * Stop the execution when the result of `<expr>` is changed.
334
+ # * Note that this feature is super slow.
335
+ when 'wat', 'watch'
336
+ if arg
337
+ @tc << [:eval, :watch, arg]
338
+ else
339
+ show_bps
340
+ return :retry
341
+ end
342
+
343
+ # * `del[ete]`
344
+ # * delete all breakpoints.
345
+ # * `del[ete] <bpnum>`
346
+ # * delete specified breakpoint.
347
+ when 'del', 'delete'
348
+ bp =
349
+ case arg
350
+ when nil
351
+ show_bps
352
+ if ask "Remove all breakpoints?", 'N'
353
+ delete_breakpoint
354
+ end
355
+ when /\d+/
356
+ delete_breakpoint arg.to_i
357
+ else
358
+ nil
359
+ end
360
+ @ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp
361
+ return :retry
362
+
363
+ ### Information
364
+
365
+ # * `bt` or `backtrace`
366
+ # * Show backtrace (frame) information.
367
+ when 'bt', 'backtrace'
368
+ @tc << [:show, :backtrace]
369
+
370
+ # * `l[ist]`
371
+ # * Show current frame's source code.
372
+ # * Next `list` command shows the successor lines.
373
+ # * `l[ist] -`
374
+ # * Show predecessor lines as opposed to the `list` command.
375
+ # * `l[ist] <start>` or `l[ist] <start>-<end>`
376
+ # * Show current frame's source code from the line <start> to <end> if given.
377
+ when 'l', 'list'
378
+ case arg ? arg.strip : nil
379
+ when /\A(\d+)\z/
380
+ @tc << [:show, :list, {start_line: arg.to_i - 1}]
381
+ when /\A-\z/
382
+ @tc << [:show, :list, {dir: -1}]
383
+ when /\A(\d+)-(\d+)\z/
384
+ @tc << [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}]
385
+ when nil
386
+ @tc << [:show, :list]
387
+ else
388
+ @ui.puts "Can not handle list argument: #{arg}"
389
+ return :retry
390
+ end
391
+
392
+ # * `edit`
393
+ # * Open the current file on the editor (use `EDITOR` environment variable).
394
+ # * Note that edited file will not be reloaded.
395
+ # * `edit <file>`
396
+ # * Open <file> on the editor.
397
+ when 'edit'
398
+ if @ui.remote?
399
+ @ui.puts "not supported on the remote console."
400
+ return :retry
401
+ end
402
+
403
+ begin
404
+ arg = resolve_path(arg) if arg
405
+ rescue Errno::ENOENT
406
+ @ui.puts "not found: #{arg}"
407
+ return :retry
408
+ end
409
+
410
+ @tc << [:show, :edit, arg]
411
+
412
+ # * `i[nfo]`
413
+ # * Show information about the current frame (local variables)
414
+ # * It includes `self` as `%self` and a return value as `%return`.
415
+ # * `i[nfo] <expr>`
416
+ # * Show information about the result of <expr>.
417
+ when 'i', 'info'
418
+ case arg
419
+ when nil
420
+ @tc << [:show, :local]
421
+ else
422
+ @tc << [:show, :object_info, arg]
423
+ end
424
+
425
+ # * `display`
426
+ # * Show display setting.
427
+ # * `display <expr>`
428
+ # * Show the result of `<expr>` at every suspended timing.
429
+ when 'display'
430
+ if arg && !arg.empty?
431
+ @displays << arg
432
+ @tc << [:eval, :try_display, @displays]
433
+ else
434
+ @tc << [:eval, :display, @displays]
435
+ end
436
+
437
+ # * `undisplay`
438
+ # * Remove all display settings.
439
+ # * `undisplay <displaynum>`
440
+ # * Remove a specified display setting.
441
+ when 'undisplay'
442
+ case arg
443
+ when /(\d+)/
444
+ if @displays[n = $1.to_i]
445
+ if ask "clear \##{n} #{@displays[n]}?"
446
+ @displays.delete_at n
447
+ end
448
+ end
449
+ @tc << [:eval, :display, @displays]
450
+ when nil
451
+ if ask "clear all?", 'N'
452
+ @displays.clear
453
+ end
454
+ end
455
+ return :retry
456
+
457
+ # * `trace [on|off]`
458
+ # * enable or disable line tracer.
459
+ when 'trace'
460
+ case arg
461
+ when 'on'
462
+ dir = __dir__
463
+ @tracer ||= TracePoint.new(){|tp|
464
+ next if File.dirname(tp.path) == dir
465
+ next if tp.path == '<internal:trace_point>'
466
+ # next if tp.event != :line
467
+ @ui.puts pretty_tp(tp)
468
+ }
469
+ @tracer.enable
470
+ when 'off'
471
+ @tracer && @tracer.disable
472
+ end
473
+ enabled = (@tracer && @tracer.enabled?) ? true : false
474
+ @ui.puts "Trace #{enabled ? 'on' : 'off'}"
475
+ return :retry
476
+
477
+ ### Frame control
478
+
479
+ # * `f[rame]`
480
+ # * Show the current frame.
481
+ # * `f[rame] <framenum>`
482
+ # * Specify a current frame. Evaluation are run on specified frame.
483
+ when 'frame', 'f'
484
+ @tc << [:frame, :set, arg]
485
+
486
+ # * `up`
487
+ # * Specify the upper frame.
488
+ when 'up'
489
+ @tc << [:frame, :up]
490
+
491
+ # * `down`
492
+ # * Specify the lower frame.
493
+ when 'down'
494
+ @tc << [:frame, :down]
495
+
496
+ ### Evaluate
497
+
498
+ # * `p <expr>`
499
+ # * Evaluate like `p <expr>` on the current frame.
500
+ when 'p'
501
+ @tc << [:eval, :p, arg.to_s]
502
+
503
+ # * `pp <expr>`
504
+ # * Evaluate like `pp <expr>` on the current frame.
505
+ when 'pp'
506
+ @tc << [:eval, :pp, arg.to_s]
507
+
508
+ # * `e[val] <expr>`
509
+ # * Evaluate `<expr>` on the current frame.
510
+ when 'e', 'eval', 'call'
511
+ @tc << [:eval, :call, arg]
512
+
513
+ # * `irb`
514
+ # * Invoke `irb` on the current frame.
515
+ when 'irb'
516
+ if @ui.remote?
517
+ @ui.puts "not supported on the remote console."
518
+ return :retry
519
+ end
520
+ @tc << [:eval, :call, 'binding.irb']
521
+
522
+ ### Thread control
523
+
524
+ # * `th[read]`
525
+ # * Show all threads.
526
+ # * `th[read] <thnum>`
527
+ # * Switch thread specified by `<thnum>`.
528
+ when 'th', 'thread'
529
+ case arg
530
+ when nil, 'list', 'l'
531
+ thread_list
532
+ when /(\d+)/
533
+ thread_switch $1.to_i
534
+ else
535
+ @ui.puts "unknown thread command: #{arg}"
536
+ end
537
+ return :retry
538
+
539
+ ### Help
540
+
541
+ # * `h[elp]`
542
+ # * Show help for all commands.
543
+ # * `h[elp] <command>`
544
+ # * Show help for the given command.
545
+ when 'h', 'help'
546
+ if arg
547
+ show_help arg
548
+ else
549
+ @ui.puts DEBUGGER__.help
550
+ end
551
+ return :retry
552
+
553
+ ### END
554
+ else
555
+ @ui.puts "unknown command: #{line}"
556
+ @repl_prev_line = nil
557
+ return :retry
558
+ end
559
+
560
+ rescue Interrupt
561
+ return :retry
562
+ rescue SystemExit
563
+ raise
564
+ rescue Exception => e
565
+ @ui.puts "[REPL ERROR] #{e.inspect}"
566
+ @ui.puts e.backtrace.map{|e| ' ' + e}
567
+ return :retry
568
+ end
569
+
570
+ def show_help arg
571
+ DEBUGGER__.helps.each{|cat, cs|
572
+ cs.each{|ws, desc|
573
+ if ws.include? arg
574
+ @ui.puts desc
575
+ return
576
+ end
577
+ }
578
+ }
579
+ @ui.puts "not found: #{arg}"
580
+ end
581
+
582
+ def ask msg, default = 'Y'
583
+ opts = '[y/n]'.tr(default.downcase, default)
584
+ input = @ui.ask("#{msg} #{opts} ")
585
+ input = default if input.empty?
586
+ case input
587
+ when 'y', 'Y'
588
+ true
589
+ else
590
+ false
591
+ end
592
+ end
593
+
594
+ def msig klass, receiver
595
+ if klass.singleton_class?
596
+ "#{receiver}."
597
+ else
598
+ "#{klass}#"
599
+ end
600
+ end
601
+
602
+ def pretty_tp tp
603
+ loc = "#{tp.path}:#{tp.lineno}"
604
+ level = caller.size
605
+
606
+ info =
607
+ case tp.event
608
+ when :line
609
+ "line at #{loc}"
610
+ when :call, :c_call
611
+ klass = tp.defined_class
612
+ "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} at #{loc}"
613
+ when :return, :c_return
614
+ klass = tp.defined_class
615
+ "#{tp.event} #{msig(klass, tp.self)}#{tp.method_id} => #{tp.return_value.inspect} at #{loc}"
616
+ when :b_call
617
+ "b_call at #{loc}"
618
+ when :b_return
619
+ "b_return => #{tp.return_value} at #{loc}"
620
+ when :class
621
+ "class #{tp.self} at #{loc}"
622
+ when :end
623
+ "class #{tp.self} end at #{loc}"
624
+ else
625
+ "#{tp.event} at #{loc}"
626
+ end
627
+
628
+ case tp.event
629
+ when :call, :b_call, :return, :b_return, :class, :end
630
+ level -= 1
631
+ end
632
+
633
+ "Tracing:#{' ' * level} #{info}"
634
+ rescue => e
635
+ p e
636
+ pp e.backtrace
637
+ exit!
638
+ end
639
+
640
+ def iterate_bps
641
+ deleted_bps = []
642
+ i = 0
643
+ @bps.each{|key, bp|
644
+ if !bp.deleted?
645
+ yield key, bp, i
646
+ i += 1
647
+ else
648
+ deleted_bps << bp
649
+ end
650
+ }
651
+ ensure
652
+ deleted_bps.each{|bp| @bps.delete bp}
653
+ end
654
+
655
+ def show_bps specific_bp = nil
656
+ iterate_bps do |key, bp, i|
657
+ @ui.puts "#%d %s" % [i, bp.to_s] if !specific_bp || bp == specific_bp
658
+ end
659
+ end
660
+
661
+ def bp_index specific_bp_key
662
+ iterate_bps do |key, bp, i|
663
+ if key == specific_bp_key
664
+ return [bp, i]
665
+ end
666
+ end
667
+ nil
668
+ end
669
+
670
+ def delete_breakpoint arg = nil
671
+ case arg
672
+ when nil
673
+ @bps.each{|key, bp| bp.delete}
674
+ @bps.clear
675
+ else
676
+ del_bp = nil
677
+ iterate_bps{|key, bp, i| del_bp = bp if i == arg}
678
+ if del_bp
679
+ del_bp.delete
680
+ @bps.delete del_bp.key
681
+ return [arg, del_bp]
682
+ end
683
+ end
684
+ end
685
+
686
+ def repl_add_breakpoint arg
687
+ arg.strip!
688
+
689
+ case arg
690
+ when /\Aif\s+(.+)\z/
691
+ cond = $1
692
+ when /(.+?)\s+if\s+(.+)\z/
693
+ sig = $1
694
+ cond = $2
695
+ else
696
+ sig = arg
697
+ end
698
+
699
+ case sig
700
+ when /\A(\d+)\z/
701
+ add_line_breakpoint @tc.location.path, $1.to_i, cond: cond
702
+ when /\A(.+)[:\s+](\d+)\z/
703
+ add_line_breakpoint $1, $2.to_i, cond: cond
704
+ when /\A(.+)([\.\#])(.+)\z/
705
+ @tc << [:breakpoint, :method, $1, $2, $3, cond]
706
+ return :noretry
707
+ when nil
708
+ add_check_breakpoint cond
709
+ else
710
+ @ui.puts "Unknown breakpoint format: #{arg}"
711
+ @ui.puts
712
+ show_help 'b'
713
+ end
714
+ end
715
+
716
+ # threads
717
+
718
+ def update_thread_list
719
+ list = Thread.list
720
+ thcs = []
721
+ unmanaged = []
722
+
723
+ list.each{|th|
724
+ case
725
+ when th == Thread.current
726
+ # ignore
727
+ when @th_clients.has_key?(th)
728
+ thcs << @th_clients[th]
729
+ else
730
+ unmanaged << th
731
+ end
732
+ }
733
+ return thcs.sort_by{|thc| thc.id}, unmanaged
734
+ end
735
+
736
+ def thread_list
737
+ thcs, unmanaged_ths = update_thread_list
738
+ thcs.each_with_index{|thc, i|
739
+ @ui.puts "#{@tc == thc ? "--> " : " "}\##{i} #{thc}"
740
+ }
741
+
742
+ if !unmanaged_ths.empty?
743
+ @ui.puts "The following threads are not managed yet by the debugger:"
744
+ unmanaged_ths.each{|th|
745
+ @ui.puts " " + th.to_s
746
+ }
747
+ end
748
+ end
749
+
750
+ def thread_switch n
751
+ thcs, unmanaged_ths = update_thread_list
752
+
753
+ if tc = thcs[n]
754
+ if tc.mode
755
+ @tc = tc
756
+ else
757
+ @ui.puts "#{tc.thread} is not controllable yet."
758
+ end
759
+ end
760
+ thread_list
761
+ end
762
+
763
+ def thread_client_create th
764
+ @th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th)
765
+ end
766
+
767
+ def setup_threads
768
+ stop_all_threads do
769
+ Thread.list.each{|th|
770
+ thread_client_create(th)
771
+ }
772
+ end
773
+ end
774
+
775
+ def on_thread_begin th
776
+ if @th_clients.has_key? th
777
+ # OK
778
+ else
779
+ # TODO: NG?
780
+ thread_client_create th
781
+ end
782
+ end
783
+
784
+ def thread_client
785
+ thr = Thread.current
786
+ if @th_clients.has_key? thr
787
+ @th_clients[thr]
788
+ else
789
+ @th_clients[thr] = thread_client_create(thr)
790
+ end
791
+ end
792
+
793
+ def stop_all_threads
794
+ current = Thread.current
795
+
796
+ if Thread.list.size > 1
797
+ TracePoint.new(:line) do
798
+ th = Thread.current
799
+ if current == th || @management_threads.include?(th)
800
+ next
801
+ else
802
+ tc = ThreadClient.current
803
+ tc.on_pause
804
+ end
805
+ end.enable do
806
+ yield
807
+ ensure
808
+ @th_clients.each{|thr, tc|
809
+ case thr
810
+ when current, (@tc && @tc.thread)
811
+ next
812
+ else
813
+ tc << :continue if thr != Thread.current
814
+ end
815
+ }
816
+ end
817
+ else
818
+ yield
819
+ end
820
+ end
821
+
822
+ ## event
823
+
824
+ def on_load iseq, src
825
+ @sr.add iseq, src
826
+
827
+ pending_line_breakpoints do |bp|
828
+ if bp.path == (iseq.absolute_path || iseq.path)
829
+ bp.try_activate
830
+ end
831
+ end
832
+ end
833
+
834
+ # breakpoint management
835
+
836
+ def add_breakpoint bp
837
+ if @bps.has_key? bp.key
838
+ @ui.puts "duplicated breakpoint: #{bp}"
839
+ else
840
+ @bps[bp.key] = bp
841
+ end
842
+ end
843
+
844
+ def rehash_bps
845
+ bps = @bps.values
846
+ @bps.clear
847
+ bps.each{|bp|
848
+ add_breakpoint bp
849
+ }
850
+ end
851
+
852
+ def break? file, line
853
+ @bps.has_key? [file, line]
854
+ end
855
+
856
+ def add_catch_breakpoint arg
857
+ bp = CatchBreakpoint.new(arg)
858
+ add_braekpoint bp
859
+ end
860
+
861
+ def add_check_breakpoint expr
862
+ bp = CheckBreakpoint.new(expr)
863
+ add_breakpoint bp
864
+ end
865
+
866
+ def resolve_path file
867
+ File.realpath(File.expand_path(file))
868
+ rescue Errno::ENOENT
869
+ return file if file == '-e'
870
+ $LOAD_PATH.each do |lp|
871
+ libpath = File.join(lp, file)
872
+ return File.realpath(libpath)
873
+ rescue Errno::ENOENT
874
+ # next
875
+ end
876
+
877
+ raise
878
+ end
879
+
880
+ def add_line_breakpoint file, line, **kw
881
+ file = resolve_path(file)
882
+ bp = LineBreakpoint.new(file, line, **kw)
883
+ add_breakpoint bp
884
+ rescue Errno::ENOENT => e
885
+ @ui.puts e.message
886
+ end
887
+
888
+ def pending_line_breakpoints
889
+ @bps.find_all do |key, bp|
890
+ LineBreakpoint === bp && !bp.iseq
891
+ end.each do |key, bp|
892
+ yield bp
893
+ end
894
+ end
895
+
896
+ def method_added tp
897
+ b = tp.binding
898
+ if var_name = b.local_variables.first
899
+ mid = b.local_variable_get(var_name)
900
+ found = false
901
+
902
+ @bps.each{|k, bp|
903
+ case bp
904
+ when MethodBreakpoint
905
+ if bp.method.nil?
906
+ found = true
907
+ if bp.sig_method_name == mid.to_s
908
+ bp.enable(quiet: true)
909
+ end
910
+ end
911
+ end
912
+ }
913
+ unless found
914
+ METHOD_ADDED_TRACKER.disable
915
+ end
916
+ end
917
+ end
918
+ end
919
+
920
+ # String for requring location
921
+ # nil for -r
922
+ def self.require_location
923
+ locs = caller_locations
924
+ dir_prefix = /#{__dir__}/
925
+
926
+ locs.each do |loc|
927
+ case loc.absolute_path
928
+ when dir_prefix
929
+ when %r{rubygems/core_ext/kernel_require\.rb}
930
+ else
931
+ return loc
932
+ end
933
+ end
934
+ nil
935
+ end
936
+
937
+ def self.console
938
+ initialize_session UI_Console.new
939
+
940
+ @prev_handler = trap(:SIGINT){
941
+ ThreadClient.current.on_trap :SIGINT
942
+ }
943
+ end
944
+
945
+ def self.add_line_breakpoint file, line, **kw
946
+ ::DEBUGGER__::SESSION.add_line_breakpoint file, line, **kw
947
+ end
948
+
949
+ def self.add_catch_breakpoint pat
950
+ ::DEBUGGER__::SESSION.add_catch_breakpoint pat
951
+ end
952
+
953
+ class << self
954
+ define_method :initialize_session do |ui|
955
+ ::DEBUGGER__.const_set(:SESSION, Session.new(ui))
956
+
957
+ # default breakpoints
958
+
959
+ # ::DEBUGGER__.add_catch_breakpoint 'RuntimeError'
960
+
961
+ Binding.module_eval do
962
+ ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1
963
+ def bp; nil; end
964
+ end
965
+
966
+ if ::DEBUGGER__::CONFIG[:nonstop] != '1'
967
+ if loc = ::DEBUGGER__.require_location
968
+ # require 'debug/console' or 'debug'
969
+ add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
970
+ else
971
+ # -r
972
+ add_line_breakpoint $0, 1, oneshot: true, hook_call: false
973
+ end
974
+ end
975
+
976
+ load_rc
977
+ end
978
+ end
979
+
980
+ def self.load_rc
981
+ ['./rdbgrc.rb', File.expand_path('~/.rdbgrc.rb')].each{|path|
982
+ if File.file? path
983
+ load path
984
+ end
985
+ }
986
+
987
+ # debug commands file
988
+ [::DEBUGGER__::CONFIG[:init_script],
989
+ './.rdbgrc',
990
+ File.expand_path('~/.rdbgrc')].each{|path|
991
+ next unless path
992
+
993
+ if File.file? path
994
+ ::DEBUGGER__::SESSION.add_initial_commands File.readlines(path)
995
+ end
996
+ }
997
+
998
+ # given debug commands
999
+ if ::DEBUGGER__::CONFIG[:commands]
1000
+ cmds = ::DEBUGGER__::CONFIG[:commands].split(';;')
1001
+ ::DEBUGGER__::SESSION.add_initial_commands cmds
1002
+ end
1003
+ end
1004
+
1005
+ def self.parse_help
1006
+ helps = Hash.new{|h, k| h[k] = []}
1007
+ desc = cat = nil
1008
+ cmds = []
1009
+
1010
+ File.read(__FILE__).each_line do |line|
1011
+ case line
1012
+ when /\A\s*### (.+)/
1013
+ cat = $1
1014
+ break if $1 == 'END'
1015
+ when /\A when (.+)/
1016
+ next unless cat
1017
+ next unless desc
1018
+ ws = $1.split(/,\s*/).map{|e| e.gsub('\'', '')}
1019
+ helps[cat] << [ws, desc]
1020
+ desc = nil
1021
+ cmds.concat ws
1022
+ when /\A\s+# (\s*\*.+)/
1023
+ if desc
1024
+ desc << "\n" + $1
1025
+ else
1026
+ desc = $1
1027
+ end
1028
+ end
1029
+ end
1030
+ @commands = cmds
1031
+ @helps = helps
1032
+ end
1033
+
1034
+ def self.helps
1035
+ (defined?(@helps) && @helps) || parse_help
1036
+ end
1037
+
1038
+ def self.commands
1039
+ (defined?(@commands) && @commands) || (parse_help; @commands)
1040
+ end
1041
+
1042
+ def self.help
1043
+ r = []
1044
+ self.helps.each{|cat, cmds|
1045
+ r << "### #{cat}"
1046
+ r << ''
1047
+ cmds.each{|ws, desc|
1048
+ r << desc
1049
+ }
1050
+ r << ''
1051
+ }
1052
+ r.join("\n")
1053
+ end
1054
+
1055
+ CONFIG = ::DEBUGGER__.parse_argv(ENV['RUBY_DEBUG_OPT'])
1056
+
1057
+ class ::Module
1058
+ def method_added mid; end
1059
+ def singleton_method_added mid; end
1060
+ end
1061
+
1062
+ def self.method_added tp
1063
+ begin
1064
+ SESSION.method_added tp
1065
+ rescue Exception => e
1066
+ p e
1067
+ end
1068
+ end
1069
+
1070
+ METHOD_ADDED_TRACKER = self.create_method_added_tracker
1071
+ end