debug 1.0.0.alpha1 → 1.0.0.beta5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/debug/session.rb CHANGED
@@ -1,11 +1,13 @@
1
- require 'json'
2
- require 'pp'
3
- require 'debug_inspector'
4
- require 'iseq_collector'
1
+ 
2
+ # skip to load debugger for bundle exec
3
+ return if $0.end_with?('bin/bundle') && ARGV.first == 'exec'
5
4
 
5
+ require_relative 'config'
6
+ require_relative 'thread_client'
6
7
  require_relative 'source_repository'
7
8
  require_relative 'breakpoint'
8
- require_relative 'thread_client'
9
+
10
+ require 'json' if ENV['RUBY_DEBUG_TEST_MODE']
9
11
 
10
12
  class RubyVM::InstructionSequence
11
13
  def traceable_lines_norec lines
@@ -39,6 +41,14 @@ class RubyVM::InstructionSequence
39
41
  def locals
40
42
  self.to_a[10]
41
43
  end
44
+
45
+ def last_line
46
+ self.to_a[4][:code_location][2]
47
+ end
48
+
49
+ def first_line
50
+ self.to_a[4][:code_location][0]
51
+ end
42
52
  end
43
53
 
44
54
  module DEBUGGER__
@@ -46,49 +56,132 @@ module DEBUGGER__
46
56
  def initialize ui
47
57
  @ui = ui
48
58
  @sr = SourceRepository.new
49
- @reserved_bps = []
50
- @bps = {} # [file, line] => LineBreakpoint || "Error" => CatchBreakpoint
59
+ @bps = {} # bp.key => bp
60
+ # [file, line] => LineBreakpoint
61
+ # "Error" => CatchBreakpoint
62
+ # "Foo#bar" => MethodBreakpoint
63
+ # [:watch, ivar] => WatchIVarBreakpoint
64
+ # [:check, expr] => CheckBreakpoint
51
65
  @th_clients = {} # {Thread => ThreadClient}
52
66
  @q_evt = Queue.new
53
67
  @displays = []
54
68
  @tc = nil
69
+ @tc_id = 0
70
+ @initial_commands = []
71
+
72
+ @frame_map = {} # {id => [threadId, frame_depth]} for DAP
73
+ @var_map = {1 => [:globals], } # {id => ...} for DAP
74
+ @src_map = {} # {id => src}
55
75
 
56
76
  @tp_load_script = TracePoint.new(:script_compiled){|tp|
57
- ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
58
- }.enable
77
+ unless @management_threads.include? Thread.current
78
+ ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
79
+ end
80
+ }
81
+ @tp_load_script.enable
59
82
 
60
83
  @session_server = Thread.new do
61
84
  Thread.current.abort_on_exception = true
85
+ session_server_main
86
+ end
62
87
 
63
- while evt = @q_evt.pop
64
- tc, output, ev, *ev_args = evt
65
- output.each{|str| @ui.puts str}
66
-
67
- case ev
68
- when :load
69
- iseq, src = ev_args
70
- on_load iseq, src
71
- tc << :continue
72
- when :suspend
73
- if @displays.empty?
74
- wait_command_loop tc
88
+ @management_threads = [@session_server]
89
+ @management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread
90
+
91
+ setup_threads
92
+
93
+ @tp_thread_begin = TracePoint.new(:thread_begin){|tp|
94
+ unless @management_threads.include?(th = Thread.current)
95
+ ThreadClient.current.on_thread_begin th
96
+ end
97
+ }
98
+ @tp_thread_begin.enable
99
+ end
100
+
101
+ def session_server_main
102
+ while evt = @q_evt.pop
103
+ # varible `@internal_info` is only used for test
104
+ tc, output, ev, @internal_info, *ev_args = evt
105
+ output.each{|str| @ui.puts str}
106
+
107
+ case ev
108
+ when :load
109
+ iseq, src = ev_args
110
+ on_load iseq, src
111
+ @ui.event :load
112
+ tc << :continue
113
+ when :thread_begin
114
+ th = ev_args.shift
115
+ on_thread_begin th
116
+ @ui.event :thread_begin, th
117
+ tc << :continue
118
+ when :suspend
119
+ case ev_args.first
120
+ when :breakpoint
121
+ bp, i = bp_index ev_args[1]
122
+ @ui.event :suspend_bp, i, bp
123
+ when :trap
124
+ @ui.event :suspend_trap, ev_args[1]
125
+ else
126
+ @ui.event :suspended
127
+ end
128
+
129
+ if @displays.empty?
130
+ wait_command_loop tc
131
+ else
132
+ tc << [:eval, :display, @displays]
133
+ end
134
+ when :result
135
+ case ev_args.first
136
+ when :watch
137
+ bp = ev_args[1]
138
+ @bps[bp.key] = bp
139
+ show_bps bp
140
+ when :try_display
141
+ failed_results = ev_args[1]
142
+ if failed_results.size > 0
143
+ i, _msg = failed_results.last
144
+ if i+1 == @displays.size
145
+ @ui.puts "canceled: #{@displays.pop}"
146
+ end
147
+ end
148
+ when :method_breakpoint
149
+ bp = ev_args[1]
150
+ if bp
151
+ @bps[bp.key] = bp
152
+ show_bps bp
75
153
  else
76
- tc << [:eval, :display, @displays]
154
+ # can't make a bp
77
155
  end
78
- when :result
79
- wait_command_loop tc
156
+ else
157
+ # ignore
80
158
  end
159
+
160
+ wait_command_loop tc
161
+
162
+ when :dap_result
163
+ dap_event ev_args # server.rb
164
+ wait_command_loop tc
81
165
  end
82
166
  end
167
+ ensure
168
+ @bps.each{|k, bp| bp.disable}
169
+ @th_clients.each{|th, thc| thc.close}
170
+ end
83
171
 
84
- @management_threads = [@session_server]
85
- @management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread
86
-
87
- setup_threads
172
+ def add_initial_commands cmds
173
+ cmds.each{|c|
174
+ c.gsub('#.*', '').strip!
175
+ @initial_commands << c unless c.empty?
176
+ }
88
177
  end
89
178
 
90
- def source path
91
- @sr.get(path)
179
+ def source iseq
180
+ if CONFIG[:use_colorize]
181
+ @sr.get_colored(iseq)
182
+ else
183
+ @sr.get(iseq)
184
+ end
92
185
  end
93
186
 
94
187
  def inspect
@@ -109,11 +202,30 @@ module DEBUGGER__
109
202
  retry
110
203
  end
111
204
  end
205
+ ensure
206
+ @tc = nil
112
207
  end
113
208
 
114
209
  def wait_command
115
- line = @ui.readline
210
+ if @initial_commands.empty?
211
+ @ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE']
212
+ line = @ui.readline
213
+ else
214
+ line = @initial_commands.shift.strip
215
+ @ui.puts "(rdbg:init) #{line}"
216
+ end
116
217
 
218
+ case line
219
+ when String
220
+ process_command line
221
+ when Hash
222
+ process_dap_request line # defined in server.rb
223
+ else
224
+ raise "unexpected input: #{line.inspect}"
225
+ end
226
+ end
227
+
228
+ def process_command line
117
229
  if line.empty?
118
230
  if @repl_prev_line
119
231
  line = @repl_prev_line
@@ -130,16 +242,30 @@ module DEBUGGER__
130
242
  # p cmd: [cmd, *arg]
131
243
 
132
244
  case cmd
245
+ ### Control flow
133
246
 
134
- # control
247
+ # * `s[tep]`
248
+ # * Step in. Resume the program until next breakable point.
135
249
  when 's', 'step'
136
250
  @tc << [:step, :in]
251
+
252
+ # * `n[ext]`
253
+ # * Step over. Resume the program until next line.
137
254
  when 'n', 'next'
138
255
  @tc << [:step, :next]
256
+
257
+ # * `fin[ish]`
258
+ # * Finish this frame. Resume the program until the current frame is finished.
139
259
  when 'fin', 'finish'
140
260
  @tc << [:step, :finish]
261
+
262
+ # * `c[ontinue]`
263
+ # * Resume the program.
141
264
  when 'c', 'continue'
142
265
  @tc << :continue
266
+
267
+ # * `q[uit]` or `Ctrl-D`
268
+ # * Finish debugger (with the debuggee process on non-remote debugging).
143
269
  when 'q', 'quit'
144
270
  if ask 'Really quit?'
145
271
  @ui.quit arg.to_i
@@ -147,23 +273,63 @@ module DEBUGGER__
147
273
  else
148
274
  return :retry
149
275
  end
276
+
277
+ # * `q[uit]!`
278
+ # * Same as q[uit] but without the confirmation prompt.
279
+ when 'q!', 'quit!'
280
+ @ui.quit arg.to_i
281
+ @tc << :continue
282
+
283
+ # * `kill`
284
+ # * Stop the debuggee process with `Kernal#exit!`.
150
285
  when 'kill'
151
- if ask 'Really quit?'
286
+ if ask 'Really kill?'
152
287
  exit! (arg || 1).to_i
153
288
  else
154
289
  return :retry
155
290
  end
156
291
 
157
- # breakpoints
292
+ # * `kill!`
293
+ # * Same as kill but without the confirmation prompt.
294
+ when 'kill!'
295
+ exit! (arg || 1).to_i
296
+
297
+ ### Breakpoint
298
+
299
+ # * `b[reak]`
300
+ # * Show all breakpoints.
301
+ # * `b[reak] <line>`
302
+ # * Set breakpoint on `<line>` at the current frame's file.
303
+ # * `b[reak] <file>:<line>` or `<file> <line>`
304
+ # * Set breakpoint on `<file>:<line>`.
305
+ # * `b[reak] <class>#<name>`
306
+ # * Set breakpoint on the method `<class>#<name>`.
307
+ # * `b[reak] <expr>.<name>`
308
+ # * Set breakpoint on the method `<expr>.<name>`.
309
+ # * `b[reak] ... if <expr>`
310
+ # * break if `<expr>` is true at specified location.
311
+ # * `b[reak] if <expr>`
312
+ # * break if `<expr>` is true at any lines.
313
+ # * Note that this feature is super slow.
158
314
  when 'b', 'break'
159
315
  if arg == nil
160
316
  show_bps
317
+ return :retry
161
318
  else
162
- bp = repl_add_breakpoint arg
163
- show_bps bp if bp
319
+ case bp = repl_add_breakpoint(arg)
320
+ when :noretry
321
+ when nil
322
+ return :retry
323
+ else
324
+ show_bps bp
325
+ return :retry
326
+ end
164
327
  end
165
- return :retry
328
+
329
+ # skip
166
330
  when 'bv'
331
+ require 'json'
332
+
167
333
  h = Hash.new{|h, k| h[k] = []}
168
334
  @bps.each{|key, bp|
169
335
  if LineBreakpoint === bp
@@ -184,12 +350,33 @@ module DEBUGGER__
184
350
  end
185
351
 
186
352
  return :retry
353
+
354
+ # * `catch <Error>`
355
+ # * Set breakpoint on raising `<Error>`.
187
356
  when 'catch'
188
357
  if arg
189
358
  bp = add_catch_breakpoint arg
190
359
  show_bps bp if bp
360
+ else
361
+ show_bps
191
362
  end
192
363
  return :retry
364
+
365
+ # * `watch @ivar`
366
+ # * Stop the execution when the result of current scope's `@ivar` is changed.
367
+ # * Note that this feature is super slow.
368
+ when 'wat', 'watch'
369
+ if arg
370
+ @tc << [:eval, :watch, arg]
371
+ else
372
+ show_bps
373
+ return :retry
374
+ end
375
+
376
+ # * `del[ete]`
377
+ # * delete all breakpoints.
378
+ # * `del[ete] <bpnum>`
379
+ # * delete specified breakpoint.
193
380
  when 'del', 'delete'
194
381
  bp =
195
382
  case arg
@@ -206,42 +393,90 @@ module DEBUGGER__
206
393
  @ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp
207
394
  return :retry
208
395
 
209
- # evaluate
210
- when 'p'
211
- @tc << [:eval, :p, arg.to_s]
212
- when 'pp'
213
- @tc << [:eval, :pp, arg.to_s]
214
- when 'e', 'eval', 'call'
215
- @tc << [:eval, :call, arg]
216
- when 'irb'
217
- @tc << [:eval, :call, 'binding.irb']
396
+ ### Information
218
397
 
219
- # evaluate/frame selector
220
- when 'up'
221
- @tc << [:frame, :up]
222
- when 'down'
223
- @tc << [:frame, :down]
224
- when 'frame', 'f'
225
- @tc << [:frame, :set, arg]
226
-
227
- # information
398
+ # * `bt` or `backtrace`
399
+ # * Show backtrace (frame) information.
228
400
  when 'bt', 'backtrace'
229
401
  @tc << [:show, :backtrace]
230
- when 'list'
231
- @tc << [:show, :list]
232
- when 'info'
402
+
403
+ # * `l[ist]`
404
+ # * Show current frame's source code.
405
+ # * Next `list` command shows the successor lines.
406
+ # * `l[ist] -`
407
+ # * Show predecessor lines as opposed to the `list` command.
408
+ # * `l[ist] <start>` or `l[ist] <start>-<end>`
409
+ # * Show current frame's source code from the line <start> to <end> if given.
410
+ when 'l', 'list'
411
+ case arg ? arg.strip : nil
412
+ when /\A(\d+)\z/
413
+ @tc << [:show, :list, {start_line: arg.to_i - 1}]
414
+ when /\A-\z/
415
+ @tc << [:show, :list, {dir: -1}]
416
+ when /\A(\d+)-(\d+)\z/
417
+ @tc << [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}]
418
+ when nil
419
+ @tc << [:show, :list]
420
+ else
421
+ @ui.puts "Can not handle list argument: #{arg}"
422
+ return :retry
423
+ end
424
+
425
+ # * `edit`
426
+ # * Open the current file on the editor (use `EDITOR` environment variable).
427
+ # * Note that edited file will not be reloaded.
428
+ # * `edit <file>`
429
+ # * Open <file> on the editor.
430
+ when 'edit'
431
+ if @ui.remote?
432
+ @ui.puts "not supported on the remote console."
433
+ return :retry
434
+ end
435
+
436
+ begin
437
+ arg = resolve_path(arg) if arg
438
+ rescue Errno::ENOENT
439
+ @ui.puts "not found: #{arg}"
440
+ return :retry
441
+ end
442
+
443
+ @tc << [:show, :edit, arg]
444
+
445
+ # * `i[nfo]`, `i[nfo] l[ocal[s]]`
446
+ # * Show information about the current frame (local variables)
447
+ # * It includes `self` as `%self` and a return value as `%return`.
448
+ # * `i[nfo] th[read[s]]`
449
+ # * Show all threads (same as `th[read]`).
450
+ when 'i', 'info'
233
451
  case arg
234
- when 'l', 'local', 'locals'
235
- @tc << [:show, :locals]
236
- when 'i', 'instance', 'ivars'
237
- @tc << [:show, :ivars]
452
+ when nil
453
+ @tc << [:show, :local]
454
+ when 'l', /locals?/
455
+ @tc << [:show, :local]
456
+ when 'th', /threads?/
457
+ thread_list
458
+ return :retry
238
459
  else
239
- @ui.puts "unknown info argument: #{arg}"
460
+ show_help 'info'
240
461
  return :retry
241
462
  end
463
+
464
+ # * `display`
465
+ # * Show display setting.
466
+ # * `display <expr>`
467
+ # * Show the result of `<expr>` at every suspended timing.
242
468
  when 'display'
243
- @displays << arg if arg && !arg.empty?
244
- @tc << [:eval, :display, @displays]
469
+ if arg && !arg.empty?
470
+ @displays << arg
471
+ @tc << [:eval, :try_display, @displays]
472
+ else
473
+ @tc << [:eval, :display, @displays]
474
+ end
475
+
476
+ # * `undisplay`
477
+ # * Remove all display settings.
478
+ # * `undisplay <displaynum>`
479
+ # * Remove a specified display setting.
245
480
  when 'undisplay'
246
481
  case arg
247
482
  when /(\d+)/
@@ -258,26 +493,79 @@ module DEBUGGER__
258
493
  end
259
494
  return :retry
260
495
 
261
- # trace
496
+ # * `trace [on|off]`
497
+ # * enable or disable line tracer.
262
498
  when 'trace'
263
499
  case arg
264
500
  when 'on'
265
- @tracer ||= TracePoint.new(){|tp|
266
- next if tp.path == __FILE__
501
+ dir = __dir__
502
+ @tracer ||= TracePoint.new(:call, :return, :b_call, :b_return, :line, :class, :end){|tp|
503
+ next if File.dirname(tp.path) == dir
267
504
  next if tp.path == '<internal:trace_point>'
505
+ # Skip when `JSON.generate` is called during tests
506
+ next if tp.binding.eval('self').to_s == 'JSON' and ENV['RUBY_DEBUG_TEST_MODE']
268
507
  # next if tp.event != :line
269
508
  @ui.puts pretty_tp(tp)
270
509
  }
271
510
  @tracer.enable
272
511
  when 'off'
273
512
  @tracer && @tracer.disable
274
- else
275
- enabled = (@tracer && @tracer.enabled?) ? true : false
276
- @ui.puts "Trace #{enabled ? 'on' : 'off'}"
277
513
  end
514
+ enabled = (@tracer && @tracer.enabled?) ? true : false
515
+ @ui.puts "Trace #{enabled ? 'on' : 'off'}"
278
516
  return :retry
279
517
 
280
- # threads
518
+ ### Frame control
519
+
520
+ # * `f[rame]`
521
+ # * Show the current frame.
522
+ # * `f[rame] <framenum>`
523
+ # * Specify a current frame. Evaluation are run on specified frame.
524
+ when 'frame', 'f'
525
+ @tc << [:frame, :set, arg]
526
+
527
+ # * `up`
528
+ # * Specify the upper frame.
529
+ when 'up'
530
+ @tc << [:frame, :up]
531
+
532
+ # * `down`
533
+ # * Specify the lower frame.
534
+ when 'down'
535
+ @tc << [:frame, :down]
536
+
537
+ ### Evaluate
538
+
539
+ # * `p <expr>`
540
+ # * Evaluate like `p <expr>` on the current frame.
541
+ when 'p'
542
+ @tc << [:eval, :p, arg.to_s]
543
+
544
+ # * `pp <expr>`
545
+ # * Evaluate like `pp <expr>` on the current frame.
546
+ when 'pp'
547
+ @tc << [:eval, :pp, arg.to_s]
548
+
549
+ # * `e[val] <expr>`
550
+ # * Evaluate `<expr>` on the current frame.
551
+ when 'e', 'eval', 'call'
552
+ @tc << [:eval, :call, arg]
553
+
554
+ # * `irb`
555
+ # * Invoke `irb` on the current frame.
556
+ when 'irb'
557
+ if @ui.remote?
558
+ @ui.puts "not supported on the remote console."
559
+ return :retry
560
+ end
561
+ @tc << [:eval, :call, 'binding.irb']
562
+
563
+ ### Thread control
564
+
565
+ # * `th[read]`
566
+ # * Show all threads.
567
+ # * `th[read] <thnum>`
568
+ # * Switch thread specified by `<thnum>`.
281
569
  when 'th', 'thread'
282
570
  case arg
283
571
  when nil, 'list', 'l'
@@ -289,6 +577,21 @@ module DEBUGGER__
289
577
  end
290
578
  return :retry
291
579
 
580
+ ### Help
581
+
582
+ # * `h[elp]`
583
+ # * Show help for all commands.
584
+ # * `h[elp] <command>`
585
+ # * Show help for the given command.
586
+ when 'h', 'help'
587
+ if arg
588
+ show_help arg
589
+ else
590
+ @ui.puts DEBUGGER__.help
591
+ end
592
+ return :retry
593
+
594
+ ### END
292
595
  else
293
596
  @ui.puts "unknown command: #{line}"
294
597
  @repl_prev_line = nil
@@ -305,6 +608,18 @@ module DEBUGGER__
305
608
  return :retry
306
609
  end
307
610
 
611
+ def show_help arg
612
+ DEBUGGER__.helps.each{|cat, cs|
613
+ cs.each{|ws, desc|
614
+ if ws.include? arg
615
+ @ui.puts desc
616
+ return
617
+ end
618
+ }
619
+ }
620
+ @ui.puts "not found: #{arg}"
621
+ end
622
+
308
623
  def ask msg, default = 'Y'
309
624
  opts = '[y/n]'.tr(default.downcase, default)
310
625
  input = @ui.ask("#{msg} #{opts} ")
@@ -363,35 +678,84 @@ module DEBUGGER__
363
678
  exit!
364
679
  end
365
680
 
366
- def show_bps specified_bp = nil
367
- @bps.each_with_index{|(key, bp), i|
368
- if !specified_bp || bp == specified_bp
369
- @ui.puts "#%d %s" % [i, bp.to_s]
681
+ def iterate_bps
682
+ deleted_bps = []
683
+ i = 0
684
+ @bps.each{|key, bp|
685
+ if !bp.deleted?
686
+ yield key, bp, i
687
+ i += 1
688
+ else
689
+ deleted_bps << bp
370
690
  end
371
691
  }
692
+ ensure
693
+ deleted_bps.each{|bp| @bps.delete bp}
372
694
  end
373
695
 
374
- def thread_list
375
- thcs, unmanaged_ths = update_thread_list
376
- thcs.each_with_index{|thc, i|
377
- @ui.puts "#{@tc == thc ? "--> " : " "}\##{i} #{thc}"
378
- }
696
+ def show_bps specific_bp = nil
697
+ iterate_bps do |key, bp, i|
698
+ @ui.puts "#%d %s" % [i, bp.to_s] if !specific_bp || bp == specific_bp
699
+ end
700
+ end
379
701
 
380
- if !unmanaged_ths.empty?
381
- @ui.puts "The following threads are not managed yet by the debugger:"
382
- unmanaged_ths.each{|th|
383
- @ui.puts " " + th.to_s
384
- }
702
+ def bp_index specific_bp_key
703
+ iterate_bps do |key, bp, i|
704
+ if key == specific_bp_key
705
+ return [bp, i]
706
+ end
385
707
  end
708
+ nil
386
709
  end
387
710
 
388
- def thread_switch n
389
- if th = @th_clients.keys[n]
390
- @tc = @th_clients[th]
711
+ def delete_breakpoint arg = nil
712
+ case arg
713
+ when nil
714
+ @bps.each{|key, bp| bp.delete}
715
+ @bps.clear
716
+ else
717
+ del_bp = nil
718
+ iterate_bps{|key, bp, i| del_bp = bp if i == arg}
719
+ if del_bp
720
+ del_bp.delete
721
+ @bps.delete del_bp.key
722
+ return [arg, del_bp]
723
+ end
391
724
  end
392
- thread_list
393
725
  end
394
726
 
727
+ def repl_add_breakpoint arg
728
+ arg.strip!
729
+
730
+ case arg
731
+ when /\Aif\s+(.+)\z/
732
+ cond = $1
733
+ when /(.+?)\s+if\s+(.+)\z/
734
+ sig = $1
735
+ cond = $2
736
+ else
737
+ sig = arg
738
+ end
739
+
740
+ case sig
741
+ when /\A(\d+)\z/
742
+ add_line_breakpoint @tc.location.path, $1.to_i, cond: cond
743
+ when /\A(.+)[:\s+](\d+)\z/
744
+ add_line_breakpoint $1, $2.to_i, cond: cond
745
+ when /\A(.+)([\.\#])(.+)\z/
746
+ @tc << [:breakpoint, :method, $1, $2, $3, cond]
747
+ return :noretry
748
+ when nil
749
+ add_check_breakpoint cond
750
+ else
751
+ @ui.puts "Unknown breakpoint format: #{arg}"
752
+ @ui.puts
753
+ show_help 'b'
754
+ end
755
+ end
756
+
757
+ # threads
758
+
395
759
  def update_thread_list
396
760
  list = Thread.list
397
761
  thcs = []
@@ -401,66 +765,77 @@ module DEBUGGER__
401
765
  case
402
766
  when th == Thread.current
403
767
  # ignore
768
+ when @management_threads.include?(th)
769
+ # ignore
404
770
  when @th_clients.has_key?(th)
405
771
  thcs << @th_clients[th]
406
772
  else
407
773
  unmanaged << th
408
774
  end
409
775
  }
410
- return thcs, unmanaged
776
+ return thcs.sort_by{|thc| thc.id}, unmanaged
411
777
  end
412
778
 
413
- def delete_breakpoint arg = nil
414
- case arg
415
- when nil
416
- @bps.each{|key, bp| bp.disable}
417
- @bps.clear
418
- else
419
- if bp = @bps[key = @bps.keys[arg]]
420
- bp.disable
421
- @bps.delete key
422
- return [arg, bp]
423
- end
779
+ def thread_list
780
+ thcs, unmanaged_ths = update_thread_list
781
+ thcs.each_with_index{|thc, i|
782
+ @ui.puts "#{@tc == thc ? "--> " : " "}\##{i} #{thc}"
783
+ }
784
+
785
+ if !unmanaged_ths.empty?
786
+ @ui.puts "The following threads are not managed yet by the debugger:"
787
+ unmanaged_ths.each{|th|
788
+ @ui.puts " " + th.to_s
789
+ }
424
790
  end
425
791
  end
426
792
 
427
- def repl_add_breakpoint arg
428
- arg.strip!
793
+ def managed_thread_clients
794
+ thcs, _unmanaged_ths = update_thread_list
795
+ thcs
796
+ end
429
797
 
430
- if /(.+?)\s+if\s+(.+)\z/ =~ arg
431
- sig = $1
432
- cond = $2
433
- else
434
- sig = arg
435
- end
798
+ def thread_switch n
799
+ thcs, _unmanaged_ths = update_thread_list
436
800
 
437
- case sig
438
- when /\A(\d+)\z/
439
- add_line_breakpoint @tc.location.path, $1.to_i, cond
440
- when /\A(.+):(\d+)\z/
441
- add_line_breakpoint $1, $2.to_i, cond
442
- when /\A(.+)[\.\#](.+)\z/
443
- add_method_breakpoint arg, cond
444
- else
445
- raise "unknown breakpoint format: #{arg}"
801
+ if tc = thcs[n]
802
+ if tc.mode
803
+ @tc = tc
804
+ else
805
+ @ui.puts "#{tc.thread} is not controllable yet."
806
+ end
446
807
  end
808
+ thread_list
447
809
  end
448
810
 
449
- def break? file, line
450
- @bps.has_key? [file, line]
811
+ def thread_client_create th
812
+ @th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th)
451
813
  end
452
814
 
453
815
  def setup_threads
454
816
  stop_all_threads do
455
817
  Thread.list.each{|th|
456
- @th_clients[th] = ThreadClient.new(@q_evt, Queue.new, th)
818
+ thread_client_create(th)
457
819
  }
458
820
  end
459
821
  end
460
822
 
823
+ def on_thread_begin th
824
+ if @th_clients.has_key? th
825
+ # OK
826
+ else
827
+ # TODO: NG?
828
+ thread_client_create th
829
+ end
830
+ end
831
+
461
832
  def thread_client
462
833
  thr = Thread.current
463
- @th_clients[thr] ||= ThreadClient.new(@q_evt, Queue.new)
834
+ if @th_clients.has_key? thr
835
+ @th_clients[thr]
836
+ else
837
+ @th_clients[thr] = thread_client_create(thr)
838
+ end
464
839
  end
465
840
 
466
841
  def stop_all_threads
@@ -492,120 +867,331 @@ module DEBUGGER__
492
867
  end
493
868
  end
494
869
 
495
- ## event
870
+ ## event
496
871
 
497
872
  def on_load iseq, src
498
873
  @sr.add iseq, src
499
- @reserved_bps.each{|(path, line, cond)|
500
- if path == iseq.absolute_path
501
- add_line_breakpoint(path, line, cond)
874
+
875
+ pending_line_breakpoints do |bp|
876
+ if bp.path == (iseq.absolute_path || iseq.path)
877
+ bp.try_activate
502
878
  end
879
+ end
880
+ end
881
+
882
+ # breakpoint management
883
+
884
+ def add_breakpoint bp
885
+ if @bps.has_key? bp.key
886
+ @ui.puts "duplicated breakpoint: #{bp}"
887
+ else
888
+ @bps[bp.key] = bp
889
+ end
890
+ end
891
+
892
+ def rehash_bps
893
+ bps = @bps.values
894
+ @bps.clear
895
+ bps.each{|bp|
896
+ add_breakpoint bp
503
897
  }
504
898
  end
505
899
 
506
- # configuration
900
+ def break? file, line
901
+ @bps.has_key? [file, line]
902
+ end
507
903
 
508
904
  def add_catch_breakpoint arg
509
905
  bp = CatchBreakpoint.new(arg)
510
- @bps[bp.key] = bp
511
- bp
512
- end
513
-
514
- def add_line_breakpoint_exact iseq, events, file, line, cond
515
- if @bps[[file, line]]
516
- return nil # duplicated
517
- end
518
-
519
- bp = case
520
- when events.include?(:RUBY_EVENT_CALL)
521
- # "def foo" line set bp on the beggining of method foo
522
- LineBreakpoint.new(:call, iseq, line, cond)
523
- when events.include?(:RUBY_EVENT_LINE)
524
- LineBreakpoint.new(:line, iseq, line, cond)
525
- when events.include?(:RUBY_EVENT_RETURN)
526
- LineBreakpoint.new(:return, iseq, line, cond)
527
- when events.include?(:RUBY_EVENT_B_RETURN)
528
- LineBreakpoint.new(:b_return, iseq, line, cond)
529
- when events.include?(:RUBY_EVENT_END)
530
- LineBreakpoint.new(:end, iseq, line, cond)
531
- else
532
- nil
533
- end
534
- @bps[bp.key] = bp if bp
906
+ add_breakpoint bp
535
907
  end
536
908
 
537
- NearestISeq = Struct.new(:iseq, :line, :events)
909
+ def add_check_breakpoint expr
910
+ bp = CheckBreakpoint.new(expr)
911
+ add_breakpoint bp
912
+ end
538
913
 
539
- def add_line_breakpoint_nearest file, line, cond
540
- nearest = nil # NearestISeq
914
+ def resolve_path file
915
+ File.realpath(File.expand_path(file))
916
+ rescue Errno::ENOENT
917
+ case file
918
+ when '-e', '-'
919
+ return file
920
+ else
921
+ $LOAD_PATH.each do |lp|
922
+ libpath = File.join(lp, file)
923
+ return File.realpath(libpath)
924
+ rescue Errno::ENOENT
925
+ # next
926
+ end
927
+ end
541
928
 
542
- ObjectSpace.each_iseq{|iseq|
543
- if iseq.absolute_path == file && iseq.first_lineno <= line
544
- iseq.traceable_lines_norec(line_events = {})
545
- lines = line_events.keys.sort
929
+ raise
930
+ end
931
+
932
+ def add_line_breakpoint file, line, **kw
933
+ file = resolve_path(file)
934
+ bp = LineBreakpoint.new(file, line, **kw)
935
+ add_breakpoint bp
936
+ rescue Errno::ENOENT => e
937
+ @ui.puts e.message
938
+ end
546
939
 
547
- if !lines.empty? && lines.last >= line
548
- nline = lines.bsearch{|l| line <= l}
549
- events = line_events[nline]
940
+ def pending_line_breakpoints
941
+ @bps.find_all do |key, bp|
942
+ LineBreakpoint === bp && !bp.iseq
943
+ end.each do |key, bp|
944
+ yield bp
945
+ end
946
+ end
550
947
 
551
- if !nearest
552
- nearest = NearestISeq.new(iseq, nline, events)
553
- else
554
- if nearest.iseq.first_lineno <= iseq.first_lineno
555
- if (nearest.line > line && !nearest.events.include?(:RUBY_EVENT_CALL)) ||
556
- events.include?(:RUBY_EVENT_CALL)
557
- nearest = NearestISeq.new(iseq, nline, events)
558
- end
948
+ def method_added tp
949
+ b = tp.binding
950
+ if var_name = b.local_variables.first
951
+ mid = b.local_variable_get(var_name)
952
+ unresolved = false
953
+
954
+ @bps.each{|k, bp|
955
+ case bp
956
+ when MethodBreakpoint
957
+ if bp.method.nil?
958
+ if bp.sig_method_name == mid.to_s
959
+ bp.try_enable(quiet: true)
559
960
  end
560
961
  end
962
+
963
+ unresolved = true unless bp.enabled?
561
964
  end
965
+ }
966
+ unless unresolved
967
+ METHOD_ADDED_TRACKER.disable
562
968
  end
563
- }
564
-
565
- if nearest
566
- add_line_breakpoint_exact nearest.iseq, nearest.events, file, nearest.line, cond
567
- else
568
- return nil
569
969
  end
570
970
  end
571
971
 
572
- def resolve_path file
573
- File.realpath(File.expand_path(file))
574
- rescue Errno::ENOENT
575
- file
972
+ def width
973
+ @ui.width
576
974
  end
577
975
 
578
- def add_line_breakpoint file, line, cond = nil
579
- file = resolve_path(file)
580
- bp = add_line_breakpoint_nearest file, line, cond
581
- @reserved_bps << [file, line, cond] unless bp
582
- bp
976
+ def check_forked
977
+ unless @session_server.status
978
+ # TODO: Support it
979
+ raise 'DEBUGGER: stop at forked process is not supported yet.'
980
+ end
583
981
  end
982
+ end
584
983
 
585
- def add_method_breakpoint signature
586
- raise
984
+ class UI_Base
985
+ def event type, *args
986
+ case type
987
+ when :suspend_bp
988
+ i, bp = *args
989
+ puts "\nStop by \##{i} #{bp}" if bp
990
+ when :suspend_trap
991
+ puts "\nStop by #{args.first}"
992
+ end
587
993
  end
588
994
  end
589
995
 
590
- def self.add_line_breakpoint file, line, if: if_not_given = true
591
- ::DEBUGGER__::SESSION.add_line_breakpoint file, line, if_not_given ? nil : binding.local_variable_get(:if)
996
+ # manual configuration methods
997
+
998
+ def self.add_line_breakpoint file, line, **kw
999
+ ::DEBUGGER__::SESSION.add_line_breakpoint file, line, **kw
592
1000
  end
593
1001
 
594
1002
  def self.add_catch_breakpoint pat
595
1003
  ::DEBUGGER__::SESSION.add_catch_breakpoint pat
596
1004
  end
597
1005
 
1006
+ # String for requring location
1007
+ # nil for -r
1008
+ def self.require_location
1009
+ locs = caller_locations
1010
+ dir_prefix = /#{__dir__}/
1011
+
1012
+ locs.each do |loc|
1013
+ case loc.absolute_path
1014
+ when dir_prefix
1015
+ when %r{rubygems/core_ext/kernel_require\.rb}
1016
+ else
1017
+ return loc if loc.absolute_path
1018
+ end
1019
+ end
1020
+ nil
1021
+ end
1022
+
1023
+ # start methods
1024
+
1025
+ def self.console **kw
1026
+ set_config(kw)
1027
+
1028
+ require_relative 'console'
1029
+
1030
+ initialize_session UI_Console.new
1031
+
1032
+ @prev_handler = trap(:SIGINT){
1033
+ ThreadClient.current.on_trap :SIGINT
1034
+ }
1035
+ end
1036
+
1037
+ def self.open host: nil, port: ::DEBUGGER__::CONFIG[:port], sock_path: nil, sock_dir: nil, **kw
1038
+ set_config(kw)
1039
+
1040
+ if port
1041
+ open_tcp host: host, port: port
1042
+ else
1043
+ open_unix sock_path: sock_path, sock_dir: sock_dir
1044
+ end
1045
+ end
1046
+
1047
+ def self.open_tcp host: nil, port:, **kw
1048
+ set_config(kw)
1049
+ require_relative 'server'
1050
+ initialize_session UI_TcpServer.new(host: host, port: port)
1051
+ end
1052
+
1053
+ def self.open_unix sock_path: nil, sock_dir: nil, **kw
1054
+ set_config(kw)
1055
+ require_relative 'server'
1056
+ initialize_session UI_UnixDomainServer.new(sock_dir: sock_dir, sock_path: sock_path)
1057
+ end
1058
+
1059
+ # boot utilities
1060
+
598
1061
  class << self
599
1062
  define_method :initialize_session do |ui|
600
1063
  ::DEBUGGER__.const_set(:SESSION, Session.new(ui))
601
1064
 
602
1065
  # default breakpoints
603
- ::DEBUGGER__.add_catch_breakpoint 'RuntimeError'
1066
+
1067
+ # ::DEBUGGER__.add_catch_breakpoint 'RuntimeError'
604
1068
 
605
1069
  Binding.module_eval do
606
- ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1
607
- def bp; nil; end
1070
+ def bp command: nil
1071
+ if command
1072
+ cmds = command.split(";;")
1073
+ SESSION.add_initial_commands cmds
1074
+ end
1075
+
1076
+ ::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1, oneshot: true
1077
+ true
1078
+ end
1079
+ end
1080
+
1081
+ if !::DEBUGGER__::CONFIG[:nonstop]
1082
+ if loc = ::DEBUGGER__.require_location
1083
+ # require 'debug/console' or 'debug'
1084
+ add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
1085
+ else
1086
+ # -r
1087
+ add_line_breakpoint $0, 1, oneshot: true, hook_call: false
1088
+ end
1089
+ end
1090
+
1091
+ load_rc
1092
+ end
1093
+ end
1094
+
1095
+ def self.load_rc
1096
+ ['./rdbgrc.rb', File.expand_path('~/.rdbgrc.rb')].each{|path|
1097
+ if File.file? path
1098
+ load path
1099
+ end
1100
+ }
1101
+
1102
+ # debug commands file
1103
+ [init_script = ::DEBUGGER__::CONFIG[:init_script],
1104
+ './.rdbgrc',
1105
+ File.expand_path('~/.rdbgrc')].each{|path|
1106
+ next unless path
1107
+
1108
+ if File.file? path
1109
+ ::DEBUGGER__::SESSION.add_initial_commands File.readlines(path)
1110
+ elsif path == init_script
1111
+ warn "Not found: #{path}"
1112
+ end
1113
+ }
1114
+
1115
+ # given debug commands
1116
+ if ::DEBUGGER__::CONFIG[:commands]
1117
+ cmds = ::DEBUGGER__::CONFIG[:commands].split(';;')
1118
+ ::DEBUGGER__::SESSION.add_initial_commands cmds
1119
+ end
1120
+ end
1121
+
1122
+ def self.parse_help
1123
+ helps = Hash.new{|h, k| h[k] = []}
1124
+ desc = cat = nil
1125
+ cmds = []
1126
+
1127
+ File.read(__FILE__).each_line do |line|
1128
+ case line
1129
+ when /\A\s*### (.+)/
1130
+ cat = $1
1131
+ break if $1 == 'END'
1132
+ when /\A when (.+)/
1133
+ next unless cat
1134
+ next unless desc
1135
+ ws = $1.split(/,\s*/).map{|e| e.gsub('\'', '')}
1136
+ helps[cat] << [ws, desc]
1137
+ desc = nil
1138
+ cmds.concat ws
1139
+ when /\A\s+# (\s*\*.+)/
1140
+ if desc
1141
+ desc << "\n" + $1
1142
+ else
1143
+ desc = $1
1144
+ end
608
1145
  end
609
1146
  end
1147
+ @commands = cmds
1148
+ @helps = helps
1149
+ end
1150
+
1151
+ def self.helps
1152
+ (defined?(@helps) && @helps) || parse_help
1153
+ end
1154
+
1155
+ def self.commands
1156
+ (defined?(@commands) && @commands) || (parse_help; @commands)
1157
+ end
1158
+
1159
+ def self.help
1160
+ r = []
1161
+ self.helps.each{|cat, cmds|
1162
+ r << "### #{cat}"
1163
+ r << ''
1164
+ cmds.each{|ws, desc|
1165
+ r << desc
1166
+ }
1167
+ r << ''
1168
+ }
1169
+ r.join("\n")
1170
+ end
1171
+
1172
+ class ::Module
1173
+ undef method_added
1174
+ def method_added mid; end
1175
+ def singleton_method_added mid; end
1176
+ end
1177
+
1178
+ def self.method_added tp
1179
+ begin
1180
+ SESSION.method_added tp
1181
+ rescue Exception => e
1182
+ p e
1183
+ end
1184
+ end
1185
+
1186
+ METHOD_ADDED_TRACKER = self.create_method_added_tracker
1187
+
1188
+ SHORT_INSPECT_LENGTH = 40
1189
+ def self.short_inspect obj, use_short = true
1190
+ str = obj.inspect
1191
+ if use_short && str.length > SHORT_INSPECT_LENGTH
1192
+ str[0...SHORT_INSPECT_LENGTH] + '...'
1193
+ else
1194
+ str
1195
+ end
610
1196
  end
611
1197
  end