lib/debug/session.rb in debug-1.0.0.alpha1 vs lib/debug/session.rb in debug-1.0.0.beta1
- old
+ new
@@ -1,13 +1,17 @@
-require 'json'
-require 'pp'
-require 'debug_inspector'
-require 'iseq_collector'
+module DEBUGGER__
+ # used in thread_client.c
+ FrameInfo = Struct.new(:location, :self, :binding, :iseq, :class, :frame_depth,
+ :has_return_value, :return_value, :show_line)
+end
+require_relative 'debug.so'
+
require_relative 'source_repository'
require_relative 'breakpoint'
require_relative 'thread_client'
+require_relative 'config'
class RubyVM::InstructionSequence
def traceable_lines_norec lines
code = self.to_a[13]
line = 0
@@ -37,23 +41,33 @@
end
def locals
self.to_a[10]
end
+
+ def last_line
+ self.to_a[4][:code_location][2]
+ end
end
module DEBUGGER__
class Session
def initialize ui
@ui = ui
@sr = SourceRepository.new
- @reserved_bps = []
- @bps = {} # [file, line] => LineBreakpoint || "Error" => CatchBreakpoint
+ @bps = {} # bp.key => bp
+ # [file, line] => LineBreakpoint
+ # "Error" => CatchBreakpoint
+ # Method => MethodBreakpoint
+ # [:watch, expr] => WatchExprBreakpoint
+ # [:check, expr] => CheckBreakpoint
@th_clients = {} # {Thread => ThreadClient}
@q_evt = Queue.new
@displays = []
@tc = nil
+ @tc_id = 0
+ @initial_commands = []
@tp_load_script = TracePoint.new(:script_compiled){|tp|
ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
}.enable
@@ -67,28 +81,79 @@
case ev
when :load
iseq, src = ev_args
on_load iseq, src
tc << :continue
+ when :thread_begin
+ th = ev_args.shift
+ on_thread_begin th
+ tc << :continue
when :suspend
+ case ev_args.first
+ when :breakpoint
+ bp, i = bp_index ev_args[1]
+ if bp
+ @ui.puts "\nStop by \##{i} #{bp}"
+ end
+ when :trap
+ @ui.puts ''
+ @ui.puts "\nStop by #{ev_args[1]}"
+ end
+
if @displays.empty?
wait_command_loop tc
else
tc << [:eval, :display, @displays]
end
when :result
+ case ev_args.first
+ when :watch
+ bp = ev_args[1]
+ @bps[bp.key] = bp
+ show_bps bp
+ when :try_display
+ failed_results = ev_args[1]
+ if failed_results.size > 0
+ i, msg = failed_results.last
+ if i+1 == @displays.size
+ @ui.puts "canceled: #{@displays.pop}"
+ end
+ end
+ when :method_breakpoint
+ bp = ev_args[1]
+ if bp
+ @bps[bp.key] = bp
+ show_bps bp
+ else
+ # can't make a bp
+ end
+ else
+ # ignore
+ end
+
wait_command_loop tc
end
end
end
@management_threads = [@session_server]
@management_threads << @ui.reader_thread if @ui.respond_to? :reader_thread
setup_threads
+
+ @tp_thread_begin = TracePoint.new(:thread_begin){|tp|
+ ThreadClient.current.on_thread_begin Thread.current
+ }.enable
end
+ def add_initial_commands cmds
+ cmds.each{|c|
+ c.gsub('#.*', '').strip!
+ @initial_commands << c unless c.empty?
+ }
+ end
+
def source path
@sr.get(path)
end
def inspect
@@ -107,14 +172,21 @@
end
rescue Interrupt
retry
end
end
+ ensure
+ @tc = nil
end
def wait_command
- line = @ui.readline
+ if @initial_commands.empty?
+ line = @ui.readline
+ else
+ line = @initial_commands.shift.strip
+ @ui.puts "(rdbg:init) #{line}"
+ end
if line.empty?
if @repl_prev_line
line = @repl_prev_line
else
@@ -128,44 +200,87 @@
cmd, arg = $1, $2
# p cmd: [cmd, *arg]
case cmd
+ ### Control flow
- # control
+ # * `s[tep]`
+ # * Step in. Resume the program until next breakable point.
when 's', 'step'
@tc << [:step, :in]
+
+ # * `n[ext]`
+ # * Step over. Resume the program until next line.
when 'n', 'next'
@tc << [:step, :next]
+
+ # * `fin[ish]`
+ # * Finish this frame. Resume the program until the current frame is finished.
when 'fin', 'finish'
@tc << [:step, :finish]
+
+ # * `c[ontinue]`
+ # * Resume the program.
when 'c', 'continue'
@tc << :continue
- when 'q', 'quit'
+
+ # * `q[uit]` or exit or `Ctrl-D`
+ # * Finish debugger (with the debuggee process on non-remote debugging).
+ when 'q', 'quit', 'exit'
if ask 'Really quit?'
@ui.quit arg.to_i
@tc << :continue
else
return :retry
end
- when 'kill'
- if ask 'Really quit?'
+
+ # * `kill` or `q[uit]!`
+ # * Stop the debuggee process.
+ when 'kill', 'quit!', 'q!'
+ if ask 'Really kill?'
exit! (arg || 1).to_i
else
return :retry
end
- # breakpoints
+ ### Breakpoint
+
+ # * `b[reak]`
+ # * Show all breakpoints.
+ # * `b[reak] <line>`
+ # * Set breakpoint on `<line>` at the current frame's file.
+ # * `b[reak] <file>:<line>` or `<file> <line>`
+ # * Set breakpoint on `<file>:<line>`.
+ # * `b[reak] <class>#<name>`
+ # * Set breakpoint on the method `<class>#<name>`.
+ # * `b[reak] <expr>.<name>`
+ # * Set breakpoint on the method `<expr>.<name>`.
+ # * `b[reak] ... if <expr>`
+ # * break if `<expr>` is true at specified location.
+ # * `b[reak] if <expr>`
+ # * break if `<expr>` is true at any lines.
+ # * Note that this feature is super slow.
when 'b', 'break'
if arg == nil
show_bps
+ return :retry
else
- bp = repl_add_breakpoint arg
- show_bps bp if bp
+ case bp = repl_add_breakpoint(arg)
+ when :noretry
+ when nil
+ return :retry
+ else
+ show_bps bp
+ return :retry
+ end
end
- return :retry
+
+ # skip
when 'bv'
+ require 'json'
+
h = Hash.new{|h, k| h[k] = []}
@bps.each{|key, bp|
if LineBreakpoint === bp
h[bp.path] << {lnum: bp.line}
end
@@ -182,16 +297,37 @@
if File.exist?(".rdb_breakpoints.json")
pp JSON.load(File.read(".rdb_breakpoints.json"))
end
return :retry
+
+ # * `catch <Error>`
+ # * Set breakpoint on raising `<Error>`.
when 'catch'
if arg
bp = add_catch_breakpoint arg
show_bps bp if bp
+ else
+ show_bps
end
return :retry
+
+ # * `watch <expr>`
+ # * Stop the execution when the result of <expr> is changed.
+ # * Note that this feature is super slow.
+ when 'wat', 'watch'
+ if arg
+ @tc << [:eval, :watch, arg]
+ else
+ show_bps
+ return :retry
+ end
+
+ # * `del[ete]`
+ # * delete all breakpoints.
+ # * `del[ete] <bpnum>`
+ # * delete specified breakpoint.
when 'del', 'delete'
bp =
case arg
when nil
show_bps
@@ -204,46 +340,88 @@
nil
end
@ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp
return :retry
- # evaluate
- when 'p'
- @tc << [:eval, :p, arg.to_s]
- when 'pp'
- @tc << [:eval, :pp, arg.to_s]
- when 'e', 'eval', 'call'
- @tc << [:eval, :call, arg]
- when 'irb'
- @tc << [:eval, :call, 'binding.irb']
+ ### Information
- # evaluate/frame selector
- when 'up'
- @tc << [:frame, :up]
- when 'down'
- @tc << [:frame, :down]
- when 'frame', 'f'
- @tc << [:frame, :set, arg]
-
- # information
+ # * `bt` or `backtrace`
+ # * Show backtrace (frame) information.
when 'bt', 'backtrace'
@tc << [:show, :backtrace]
- when 'list'
- @tc << [:show, :list]
- when 'info'
- case arg
- when 'l', 'local', 'locals'
- @tc << [:show, :locals]
- when 'i', 'instance', 'ivars'
- @tc << [:show, :ivars]
+
+ # * `l[ist]`
+ # * Show current frame's source code.
+ # * Next `list` command shows the successor lines.
+ # * `l[ist] -`
+ # * Show predecessor lines as opposed to the `list` command.
+ # * `l[ist] <start>` or `l[ist] <start>-<end>`
+ # * Show current frame's source code from the line <start> to <end> if given.
+ when 'l', 'list'
+ case arg ? arg.strip : nil
+ when /\A(\d+)\z/
+ @tc << [:show, :list, {start_line: arg.to_i - 1}]
+ when /\A-\z/
+ @tc << [:show, :list, {dir: -1}]
+ when /\A(\d+)-(\d+)\z/
+ @tc << [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}]
+ when nil
+ @tc << [:show, :list]
else
- @ui.puts "unknown info argument: #{arg}"
+ @ui.puts "Can not handle list argument: #{arg}"
return :retry
end
+
+ # * `edit`
+ # * Open the current file on the editor (use `EDITOR` environment variable).
+ # * Note that editted file will not be reloaded.
+ # * `edit <file>`
+ # * Open <file> on the editor.
+ when 'edit'
+ if @ui.remote?
+ @ui.puts "not supported on the remote console."
+ return :retry
+ end
+
+ begin
+ arg = resolve_path(arg) if arg
+ rescue Errno::ENOENT
+ @ui.puts "not found: #{arg}"
+ return :retry
+ end
+
+ @tc << [:show, :edit, arg]
+
+ # * `i[nfo]`
+ # * Show information about the current frame (local variables)
+ # * It includes `self` as `%self` and a return value as `%return`.
+ # * `i[nfo] <expr>`
+ # * Show information about the result of <expr>.
+ when 'i', 'info'
+ case arg
+ when nil
+ @tc << [:show, :local]
+ else
+ @tc << [:show, :object_info, arg]
+ end
+
+ # * `display`
+ # * Show display setting.
+ # * `display <expr>`
+ # * Show the result of `<expr>` at every suspended timing.
when 'display'
- @displays << arg if arg && !arg.empty?
- @tc << [:eval, :display, @displays]
+ if arg && !arg.empty?
+ @displays << arg
+ @tc << [:eval, :try_display, @displays]
+ else
+ @tc << [:eval, :display, @displays]
+ end
+
+ # * `undisplay`
+ # * Remove all display settings.
+ # * `undisplay <displaynum>`
+ # * Remove a specified display setting.
when 'undisplay'
case arg
when /(\d+)/
if @displays[n = $1.to_i]
if ask "clear \##{n} #{@displays[n]}?"
@@ -256,30 +434,81 @@
@displays.clear
end
end
return :retry
- # trace
+ # * `trace [on|off]`
+ # * enable or disable line tracer.
when 'trace'
case arg
when 'on'
+ dir = __dir__
@tracer ||= TracePoint.new(){|tp|
- next if tp.path == __FILE__
+ next if File.dirname(tp.path) == dir
next if tp.path == '<internal:trace_point>'
# next if tp.event != :line
@ui.puts pretty_tp(tp)
}
@tracer.enable
when 'off'
@tracer && @tracer.disable
- else
- enabled = (@tracer && @tracer.enabled?) ? true : false
- @ui.puts "Trace #{enabled ? 'on' : 'off'}"
end
+ enabled = (@tracer && @tracer.enabled?) ? true : false
+ @ui.puts "Trace #{enabled ? 'on' : 'off'}"
return :retry
- # threads
+ ### Frame control
+
+ # * `f[rame]`
+ # * Show current frame.
+ # * `f[rame] <framenum>`
+ # * Specify frame. Evaluation are run on this frame environement.
+ when 'frame', 'f'
+ @tc << [:frame, :set, arg]
+
+ # * `up`
+ # * Specify upper frame.
+ when 'up'
+ @tc << [:frame, :up]
+
+ # * `down`
+ # * Specify down frame.
+ when 'down'
+ @tc << [:frame, :down]
+
+ ### Evaluate
+
+ # * `p <expr>`
+ # * Evaluate like `p <expr>` on the current frame.
+ when 'p'
+ @tc << [:eval, :p, arg.to_s]
+
+ # * `pp <expr>`
+ # * Evaluate like `pp <expr>` on the current frame.
+ when 'pp'
+ @tc << [:eval, :pp, arg.to_s]
+
+ # * `e[val] <expr>`
+ # * Evaluate `<expr>` on the current frame.
+ when 'e', 'eval', 'call'
+ @tc << [:eval, :call, arg]
+
+ # * `irb`
+ # * Invoke `irb` on the current frame.
+ when 'irb'
+ if @ui.remote?
+ @ui.puts "not supported on the remote console."
+ return :retry
+ end
+ @tc << [:eval, :call, 'binding.irb']
+
+ ### Thread control
+
+ # * `th[read]`
+ # * Show all threads.
+ # * `th[read] <thnum>`
+ # * Switch thread specified by `<thnum>`.
when 'th', 'thread'
case arg
when nil, 'list', 'l'
thread_list
when /(\d+)/
@@ -287,10 +516,25 @@
else
@ui.puts "unknown thread command: #{arg}"
end
return :retry
+ ### Help
+
+ # * `h[elp]`
+ # * Show help for all commands.
+ # * `h[elp] <command>`
+ # * Show help for the given command.
+ when 'h', 'help'
+ if arg
+ show_help arg
+ else
+ @ui.puts DEBUGGER__.help
+ end
+ return :retry
+
+ ### END
else
@ui.puts "unknown command: #{line}"
@repl_prev_line = nil
return :retry
end
@@ -303,10 +547,22 @@
@ui.puts "[REPL ERROR] #{e.inspect}"
@ui.puts e.backtrace.map{|e| ' ' + e}
return :retry
end
+ def show_help arg
+ DEBUGGER__.helps.each{|cat, cs|
+ cs.each{|ws, desc|
+ if ws.include? arg
+ @ui.puts desc
+ return
+ end
+ }
+ }
+ @ui.puts "not found: #{arg}"
+ end
+
def ask msg, default = 'Y'
opts = '[y/n]'.tr(default.downcase, default)
input = @ui.ask("#{msg} #{opts} ")
input = default if input.empty?
case input
@@ -361,39 +617,88 @@
p e
pp e.backtrace
exit!
end
- def show_bps specified_bp = nil
- @bps.each_with_index{|(key, bp), i|
- if !specified_bp || bp == specified_bp
- @ui.puts "#%d %s" % [i, bp.to_s]
+ def iterate_bps
+ deleted_bps = []
+ i = 0
+ @bps.each{|key, bp|
+ if !bp.deleted?
+ yield key, bp, i
+ i += 1
+ else
+ deleted_bps << bp
end
}
+ ensure
+ deleted_bps.each{|bp| @bps.delete bp}
end
- def thread_list
- thcs, unmanaged_ths = update_thread_list
- thcs.each_with_index{|thc, i|
- @ui.puts "#{@tc == thc ? "--> " : " "}\##{i} #{thc}"
- }
+ def show_bps specific_bp = nil
+ iterate_bps do |key, bp, i|
+ @ui.puts "#%d %s" % [i, bp.to_s] if !specific_bp || bp == specific_bp
+ end
+ end
- if !unmanaged_ths.empty?
- @ui.puts "The following threads are not managed yet by the debugger:"
- unmanaged_ths.each{|th|
- @ui.puts " " + th.to_s
- }
+ def bp_index specific_bp_key
+ iterate_bps do |key, bp, i|
+ if key == specific_bp_key
+ return [bp, i]
+ end
end
+ nil
end
- def thread_switch n
- if th = @th_clients.keys[n]
- @tc = @th_clients[th]
+ def delete_breakpoint arg = nil
+ case arg
+ when nil
+ @bps.each{|key, bp| bp.delete}
+ @bps.clear
+ else
+ del_bp = nil
+ iterate_bps{|key, bp, i| del_bp = bp if i == arg}
+ if del_bp
+ del_bp.delete
+ @bps.delete del_bp.key
+ return [arg, del_bp]
+ end
end
- thread_list
end
+ def repl_add_breakpoint arg
+ arg.strip!
+
+ case arg
+ when /\Aif\s+(.+)\z/
+ cond = $1
+ when /(.+?)\s+if\s+(.+)\z/
+ sig = $1
+ cond = $2
+ else
+ sig = arg
+ end
+
+ case sig
+ when /\A(\d+)\z/
+ add_line_breakpoint @tc.location.path, $1.to_i, cond: cond
+ when /\A(.+)[:\s+](\d+)\z/
+ add_line_breakpoint $1, $2.to_i, cond: cond
+ when /\A(.+)([\.\#])(.+)\z/
+ @tc << [:breakpoint, :method, $1, $2, $3, cond]
+ return :noretry
+ when nil
+ add_check_breakpoint cond
+ else
+ @ui.puts "Unknown breakpoint format: #{arg}"
+ @ui.puts
+ show_help 'b'
+ end
+ end
+
+ # threads
+
def update_thread_list
list = Thread.list
thcs = []
unmanaged = []
@@ -405,64 +710,68 @@
thcs << @th_clients[th]
else
unmanaged << th
end
}
- return thcs, unmanaged
+ return thcs.sort_by{|thc| thc.id}, unmanaged
end
- def delete_breakpoint arg = nil
- case arg
- when nil
- @bps.each{|key, bp| bp.disable}
- @bps.clear
- else
- if bp = @bps[key = @bps.keys[arg]]
- bp.disable
- @bps.delete key
- return [arg, bp]
- end
+ def thread_list
+ thcs, unmanaged_ths = update_thread_list
+ thcs.each_with_index{|thc, i|
+ @ui.puts "#{@tc == thc ? "--> " : " "}\##{i} #{thc}"
+ }
+
+ if !unmanaged_ths.empty?
+ @ui.puts "The following threads are not managed yet by the debugger:"
+ unmanaged_ths.each{|th|
+ @ui.puts " " + th.to_s
+ }
end
end
- def repl_add_breakpoint arg
- arg.strip!
+ def thread_switch n
+ thcs, unmanaged_ths = update_thread_list
- if /(.+?)\s+if\s+(.+)\z/ =~ arg
- sig = $1
- cond = $2
- else
- sig = arg
+ if tc = thcs[n]
+ if tc.mode
+ @tc = tc
+ else
+ @ui.puts "#{tc.thread} is not controllable yet."
+ end
end
-
- case sig
- when /\A(\d+)\z/
- add_line_breakpoint @tc.location.path, $1.to_i, cond
- when /\A(.+):(\d+)\z/
- add_line_breakpoint $1, $2.to_i, cond
- when /\A(.+)[\.\#](.+)\z/
- add_method_breakpoint arg, cond
- else
- raise "unknown breakpoint format: #{arg}"
- end
+ thread_list
end
- def break? file, line
- @bps.has_key? [file, line]
+ def thread_client_create th
+ @th_clients[th] = ThreadClient.new((@tc_id += 1), @q_evt, Queue.new, th)
end
def setup_threads
stop_all_threads do
Thread.list.each{|th|
- @th_clients[th] = ThreadClient.new(@q_evt, Queue.new, th)
+ thread_client_create(th)
}
end
end
+ def on_thread_begin th
+ if @th_clients.has_key? th
+ # OK
+ else
+ # TODO: NG?
+ thread_client_create th
+ end
+ end
+
def thread_client
thr = Thread.current
- @th_clients[thr] ||= ThreadClient.new(@q_evt, Queue.new)
+ if @th_clients.has_key? thr
+ @th_clients[thr]
+ else
+ @th_clients[thr] = thread_client_create(thr)
+ end
end
def stop_all_threads
current = Thread.current
@@ -494,103 +803,124 @@
## event
def on_load iseq, src
@sr.add iseq, src
- @reserved_bps.each{|(path, line, cond)|
- if path == iseq.absolute_path
- add_line_breakpoint(path, line, cond)
+
+ pending_line_breakpoints do |bp|
+ if bp.path == (iseq.absolute_path || iseq.path)
+ bp.try_activate
end
+ end
+ end
+
+ # breakpoint management
+
+ def add_breakpoint bp
+ if @bps.has_key? bp.key
+ @ui.puts "duplicated breakpoint: #{bp}"
+ else
+ @bps[bp.key] = bp
+ end
+ end
+
+ def rehash_bps
+ bps = @bps.values
+ @bps.clear
+ bps.each{|bp|
+ add_breakpoint bp
}
end
- # configuration
+ def break? file, line
+ @bps.has_key? [file, line]
+ end
def add_catch_breakpoint arg
bp = CatchBreakpoint.new(arg)
- @bps[bp.key] = bp
- bp
+ add_braekpoint bp
end
- def add_line_breakpoint_exact iseq, events, file, line, cond
- if @bps[[file, line]]
- return nil # duplicated
- end
+ def add_check_breakpoint expr
+ bp = CheckBreakpoint.new(expr)
+ add_breakpoint bp
+ end
- bp = case
- when events.include?(:RUBY_EVENT_CALL)
- # "def foo" line set bp on the beggining of method foo
- LineBreakpoint.new(:call, iseq, line, cond)
- when events.include?(:RUBY_EVENT_LINE)
- LineBreakpoint.new(:line, iseq, line, cond)
- when events.include?(:RUBY_EVENT_RETURN)
- LineBreakpoint.new(:return, iseq, line, cond)
- when events.include?(:RUBY_EVENT_B_RETURN)
- LineBreakpoint.new(:b_return, iseq, line, cond)
- when events.include?(:RUBY_EVENT_END)
- LineBreakpoint.new(:end, iseq, line, cond)
- else
- nil
- end
- @bps[bp.key] = bp if bp
+ def resolve_path file
+ File.realpath(File.expand_path(file))
+ rescue Errno::ENOENT
+ return file if file == '-e'
+ raise
end
- NearestISeq = Struct.new(:iseq, :line, :events)
+ def add_line_breakpoint file, line, **kw
+ file = resolve_path(file)
+ bp = LineBreakpoint.new(file, line, **kw)
+ add_breakpoint bp
+ rescue Errno::ENOENT => e
+ @ui.puts e.message
+ end
- def add_line_breakpoint_nearest file, line, cond
- nearest = nil # NearestISeq
+ def pending_line_breakpoints
+ @bps.find_all do |key, bp|
+ LineBreakpoint === bp && !bp.iseq
+ end.each do |key, bp|
+ yield bp
+ end
+ end
- ObjectSpace.each_iseq{|iseq|
- if iseq.absolute_path == file && iseq.first_lineno <= line
- iseq.traceable_lines_norec(line_events = {})
- lines = line_events.keys.sort
+ def method_added tp
+ b = tp.binding
+ if var_name = b.local_variables.first
+ mid = b.local_variable_get(var_name)
+ found = false
- if !lines.empty? && lines.last >= line
- nline = lines.bsearch{|l| line <= l}
- events = line_events[nline]
-
- if !nearest
- nearest = NearestISeq.new(iseq, nline, events)
- else
- if nearest.iseq.first_lineno <= iseq.first_lineno
- if (nearest.line > line && !nearest.events.include?(:RUBY_EVENT_CALL)) ||
- events.include?(:RUBY_EVENT_CALL)
- nearest = NearestISeq.new(iseq, nline, events)
- end
+ @bps.each{|k, bp|
+ case bp
+ when MethodBreakpoint
+ if bp.method.nil?
+ found = true
+ if bp.sig_method_name == mid.to_s
+ bp.enable(quiet: true)
end
end
end
+ }
+ unless found
+ METHOD_ADDED_TRACKER.disable
end
- }
-
- if nearest
- add_line_breakpoint_exact nearest.iseq, nearest.events, file, nearest.line, cond
- else
- return nil
end
end
+ end
- def resolve_path file
- File.realpath(File.expand_path(file))
- rescue Errno::ENOENT
- file
- end
+ # String for requring location
+ # nil for -r
+ def self.require_location
+ locs = caller_locations
+ dir_prefix = /#{__dir__}/
- def add_line_breakpoint file, line, cond = nil
- file = resolve_path(file)
- bp = add_line_breakpoint_nearest file, line, cond
- @reserved_bps << [file, line, cond] unless bp
- bp
+ locs.each do |loc|
+ case loc.absolute_path
+ when dir_prefix
+ when %r{rubygems/core_ext/kernel_require\.rb}
+ else
+ return loc
+ end
end
+ nil
+ end
- def add_method_breakpoint signature
- raise
- end
+ def self.console
+ initialize_session UI_Console.new
+
+ @prev_handler = trap(:SIGINT){
+ ThreadClient.current.on_trap :SIGINT
+ }
end
- def self.add_line_breakpoint file, line, if: if_not_given = true
- ::DEBUGGER__::SESSION.add_line_breakpoint file, line, if_not_given ? nil : binding.local_variable_get(:if)
+ def self.add_line_breakpoint file, line, **kw
+ ::DEBUGGER__::SESSION.add_line_breakpoint file, line, **kw
end
def self.add_catch_breakpoint pat
::DEBUGGER__::SESSION.add_catch_breakpoint pat
end
@@ -598,14 +928,119 @@
class << self
define_method :initialize_session do |ui|
::DEBUGGER__.const_set(:SESSION, Session.new(ui))
# default breakpoints
- ::DEBUGGER__.add_catch_breakpoint 'RuntimeError'
+ # ::DEBUGGER__.add_catch_breakpoint 'RuntimeError'
+
Binding.module_eval do
::DEBUGGER__.add_line_breakpoint __FILE__, __LINE__ + 1
def bp; nil; end
end
+
+ if ::DEBUGGER__::CONFIG[:nonstop] != '1'
+ if loc = ::DEBUGGER__.require_location
+ # require 'debug/console' or 'debug'
+ add_line_breakpoint loc.absolute_path, loc.lineno + 1, oneshot: true, hook_call: false
+ else
+ # -r
+ add_line_breakpoint $0, 1, oneshot: true, hook_call: false
+ end
+ end
+
+ load_rc
end
end
+
+ def self.load_rc
+ ['./rdbgrc.rb', File.expand_path('~/.rdbgrc.rb')].each{|path|
+ if File.file? path
+ load path
+ end
+ }
+
+ # debug commands file
+ [::DEBUGGER__::CONFIG[:init_script],
+ './.rdbgrc',
+ File.expand_path('~/.rdbgrc')].each{|path|
+ next unless path
+
+ if File.file? path
+ ::DEBUGGER__::SESSION.add_initial_commands File.readlines(path)
+ end
+ }
+
+ # given debug commands
+ if ::DEBUGGER__::CONFIG[:commands]
+ cmds = ::DEBUGGER__::CONFIG[:commands].split(';;')
+ ::DEBUGGER__::SESSION.add_initial_commands cmds
+ end
+ end
+
+ def self.parse_help
+ helps = Hash.new{|h, k| h[k] = []}
+ desc = cat = nil
+ cmds = []
+
+ File.read(__FILE__).each_line do |line|
+ case line
+ when /\A\s*### (.+)/
+ cat = $1
+ break if $1 == 'END'
+ when /\A when (.+)/
+ next unless cat
+ next unless desc
+ ws = $1.split(/,\s*/).map{|e| e.gsub('\'', '')}
+ helps[cat] << [ws, desc]
+ desc = nil
+ cmds.concat ws
+ when /\A\s+# (\s*\*.+)/
+ if desc
+ desc << "\n" + $1
+ else
+ desc = $1
+ end
+ end
+ end
+ @commands = cmds
+ @helps = helps
+ end
+
+ def self.helps
+ (defined?(@helps) && @helps) || parse_help
+ end
+
+ def self.commands
+ (defined?(@commands) && @commands) || (parse_help; @commands)
+ end
+
+ def self.help
+ r = []
+ self.helps.each{|cat, cmds|
+ r << "### #{cat}"
+ r << ''
+ cmds.each{|ws, desc|
+ r << desc
+ }
+ r << ''
+ }
+ r.join("\n")
+ end
+
+ CONFIG = ::DEBUGGER__.parse_argv(ENV['RUBY_DEBUG_OPT'])
+
+ class ::Module
+ def method_added mid; end
+ def singleton_method_added mid; end
+ end
+
+ def self.method_added tp
+ begin
+ SESSION.method_added tp
+ rescue Exception => e
+ p e
+ end
+ end
+
+ METHOD_ADDED_TRACKER = self.create_method_added_tracker
end