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