lib/debug/session.rb in debug-1.5.0 vs lib/debug/session.rb in debug-1.6.0

- old
+ new

@@ -17,10 +17,18 @@ end return end +# restore RUBYOPT +if (added_opt = ENV['RUBY_DEBUG_ADDED_RUBYOPT']) && + (rubyopt = ENV['RUBYOPT']) && + rubyopt.start_with?(added_opt) + ENV['RUBYOPT'] = rubyopt.delete_prefix(rubyopt) + ENV['RUBY_DEBUG_ADDED_RUBYOPT'] = nil +end + require_relative 'frame_info' require_relative 'config' require_relative 'thread_client' require_relative 'source_repository' require_relative 'breakpoint' @@ -30,10 +38,11 @@ $LOADED_FEATURES << 'debug.rb' $LOADED_FEATURES << File.expand_path(File.join(__dir__, '..', 'debug.rb')) require 'debug' # invalidate the $LOADED_FEATURE cache require 'json' if ENV['RUBY_DEBUG_TEST_UI'] == 'terminal' +require 'pp' class RubyVM::InstructionSequence def traceable_lines_norec lines code = self.to_a[13] line = 0 @@ -195,13 +204,18 @@ end ensure deactivate end + def request_tc(req) + @tc << req + end + def process_event evt # variable `@internal_info` is only used for test - tc, output, ev, @internal_info, *ev_args = evt + @tc, output, ev, @internal_info, *ev_args = evt + output.each{|str| @ui.puts str} if ev != :suspend case ev when :thread_begin # special event, tc is nil @@ -210,51 +224,48 @@ on_thread_begin th q << true when :init enter_subsession - wait_command_loop tc - + wait_command_loop when :load iseq, src = ev_args on_load iseq, src - @ui.event :load - tc << :continue + request_tc :continue when :trace trace_id, msg = ev_args if t = @tracers.values.find{|t| t.object_id == trace_id} t.puts msg end - tc << :continue + request_tc :continue when :suspend enter_subsession if ev_args.first != :replay output.each{|str| @ui.puts str} case ev_args.first when :breakpoint bp, i = bp_index ev_args[1] clean_bps unless bp - @ui.event :suspend_bp, i, bp, tc.id + @ui.event :suspend_bp, i, bp, @tc.id when :trap - @ui.event :suspend_trap, sig = ev_args[1], tc.id + @ui.event :suspend_trap, sig = ev_args[1], @tc.id if sig == :SIGINT && (@intercepted_sigint_cmd.kind_of?(Proc) || @intercepted_sigint_cmd.kind_of?(String)) @ui.puts "#{@intercepted_sigint_cmd.inspect} is registered as SIGINT handler." @ui.puts "`sigint` command execute it." end else - @ui.event :suspended, tc.id + @ui.event :suspended, @tc.id end if @displays.empty? - wait_command_loop tc + wait_command_loop else - tc << [:eval, :display, @displays] + request_tc [:eval, :display, @displays] end - when :result raise "[BUG] not in subsession" if @subsession_stack.empty? case ev_args.first when :try_display @@ -281,18 +292,18 @@ add_tracer ObjectTracer.new(@ui, obj_id, obj_inspect, **opt) else # ignore end - wait_command_loop tc + wait_command_loop when :dap_result dap_event ev_args # server.rb - wait_command_loop tc + wait_command_loop when :cdp_result cdp_event ev_args - wait_command_loop tc + wait_command_loop end end def add_preset_commands name, cmds, kick: true, continue: true cs = cmds.map{|c| @@ -321,13 +332,11 @@ def inspect "DEBUGGER__::SESSION" end - def wait_command_loop tc - @tc = tc - + def wait_command_loop loop do case wait_command when :retry # nothing else @@ -527,36 +536,10 @@ show_bps bp return :retry end end - # skip - when 'bv' - check_postmortem - require 'json' - - h = Hash.new{|h, k| h[k] = []} - @bps.each_value{|bp| - if LineBreakpoint === bp - h[bp.path] << {lnum: bp.line} - end - } - if h.empty? - # TODO: clean? - else - open(".rdb_breakpoints.json", 'w'){|f| JSON.dump(h, f)} - end - - vimsrc = File.join(__dir__, 'bp.vim') - system("vim -R -S #{vimsrc} #{@tc.location.path}") - - if File.exist?(".rdb_breakpoints.json") - pp JSON.load(File.read(".rdb_breakpoints.json")) - end - - return :retry - # * `catch <Error>` # * Set breakpoint on raising `<Error>`. # * `catch ... if: <expr>` # * stops only if `<expr>` is true as well. # * `catch ... pre: <command>` @@ -630,19 +613,19 @@ # * `bt <num> /regexp/` or `backtrace <num> /regexp/` # * Only shows first `<num>` frames with method name or location info that matches `/regexp/`. when 'bt', 'backtrace' case arg when /\A(\d+)\z/ - @tc << [:show, :backtrace, arg.to_i, nil] + request_tc [:show, :backtrace, arg.to_i, nil] when /\A\/(.*)\/\z/ pattern = $1 - @tc << [:show, :backtrace, nil, Regexp.compile(pattern)] + request_tc [:show, :backtrace, nil, Regexp.compile(pattern)] when /\A(\d+)\s+\/(.*)\/\z/ max, pattern = $1, $2 - @tc << [:show, :backtrace, max.to_i, Regexp.compile(pattern)] + request_tc [:show, :backtrace, max.to_i, Regexp.compile(pattern)] else - @tc << [:show, :backtrace, nil, nil] + request_tc [:show, :backtrace, nil, nil] end # * `l[ist]` # * Show current frame's source code. # * Next `list` command shows the successor lines. @@ -651,17 +634,17 @@ # * `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}] + request_tc [:show, :list, {start_line: arg.to_i - 1}] when /\A-\z/ - @tc << [:show, :list, {dir: -1}] + request_tc [:show, :list, {dir: -1}] when /\A(\d+)-(\d+)\z/ - @tc << [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}] + request_tc [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}] when nil - @tc << [:show, :list] + request_tc [:show, :list] else @ui.puts "Can not handle list argument: #{arg}" return :retry end @@ -681,11 +664,11 @@ rescue Errno::ENOENT @ui.puts "not found: #{arg}" return :retry end - @tc << [:show, :edit, arg] + request_tc [:show, :edit, arg] # * `i[nfo]` # * Show information about current frame (local/instance variables and defined constants). # * `i[nfo] l[ocal[s]]` # * Show information about the current frame (local variables) @@ -708,19 +691,19 @@ sub = arg end case sub when nil - @tc << [:show, :default, pat] # something useful + request_tc [:show, :default, pat] # something useful when 'l', /^locals?/ - @tc << [:show, :locals, pat] + request_tc [:show, :locals, pat] when 'i', /^ivars?/i, /^instance[_ ]variables?/i - @tc << [:show, :ivars, pat] + request_tc [:show, :ivars, pat] when 'c', /^consts?/i, /^constants?/i - @tc << [:show, :consts, pat] + request_tc [:show, :consts, pat] when 'g', /^globals?/i, /^global[_ ]variables?/i - @tc << [:show, :globals, pat] + request_tc [:show, :globals, pat] when 'th', /threads?/ thread_list return :retry else @ui.puts "unrecognized argument for info command: #{arg}" @@ -732,22 +715,22 @@ # * Show you available methods, constants, local variables, and instance variables in the current scope. # * `o[utline] <expr>` or `ls <expr>` # * Show you available methods and instance variables of the given object. # * If the object is a class/module, it also lists its constants. when 'outline', 'o', 'ls' - @tc << [:show, :outline, arg] + request_tc [:show, :outline, arg] # * `display` # * Show display setting. # * `display <expr>` # * Show the result of `<expr>` at every suspended timing. when 'display' if arg && !arg.empty? @displays << arg - @tc << [:eval, :try_display, @displays] + request_tc [:eval, :try_display, @displays] else - @tc << [:eval, :display, @displays] + request_tc [:eval, :display, @displays] end # * `undisplay` # * Remove all display settings. # * `undisplay <displaynum>` @@ -756,11 +739,11 @@ case arg when /(\d+)/ if @displays[n = $1.to_i] @displays.delete_at n end - @tc << [:eval, :display, @displays] + request_tc [:eval, :display, @displays] when nil if ask "clear all?", 'N' @displays.clear end return :retry @@ -771,53 +754,53 @@ # * `f[rame]` # * Show the current frame. # * `f[rame] <framenum>` # * Specify a current frame. Evaluation are run on specified frame. when 'frame', 'f' - @tc << [:frame, :set, arg] + request_tc [:frame, :set, arg] # * `up` # * Specify the upper frame. when 'up' - @tc << [:frame, :up] + request_tc [:frame, :up] # * `down` # * Specify the lower frame. when 'down' - @tc << [:frame, :down] + request_tc [:frame, :down] ### Evaluate # * `p <expr>` # * Evaluate like `p <expr>` on the current frame. when 'p' - @tc << [:eval, :p, arg.to_s] + request_tc [:eval, :p, arg.to_s] # * `pp <expr>` # * Evaluate like `pp <expr>` on the current frame. when 'pp' - @tc << [:eval, :pp, arg.to_s] + request_tc [:eval, :pp, arg.to_s] # * `eval <expr>` # * Evaluate `<expr>` on the current frame. when 'eval', 'call' if arg == nil || arg.empty? show_help 'eval' @ui.puts "\nTo evaluate the variable `#{cmd}`, use `pp #{cmd}` instead." return :retry else - @tc << [:eval, :call, arg] + request_tc [:eval, :call, arg] end # * `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, :irb] + request_tc [:eval, :irb] # don't repeat irb command @repl_prev_line = nil ### Trace @@ -870,11 +853,11 @@ when /\Aexception\z/ add_tracer ExceptionTracer.new(@ui, pattern: pattern, into: into) return :retry when /\Aobject\s+(.+)/ - @tc << [:trace, :object, $1.strip, {pattern: pattern, into: into}] + request_tc [:trace, :object, $1.strip, {pattern: pattern, into: into}] when /\Aoff\s+(\d+)\z/ if t = @tracers.values[$1.to_i] t.disable @ui.puts "Disable #{t.to_s}" @@ -908,11 +891,11 @@ # * `step reset` # * Stop replay . when 'record' case arg when nil, 'on', 'off' - @tc << [:record, arg&.to_sym] + request_tc [:record, arg&.to_sym] else @ui.puts "unknown command: #{arg}" return :retry end @@ -1003,11 +986,11 @@ show_help arg return :retry ### END else - @tc << [:eval, :pp, line] + request_tc [:eval, :pp, line] =begin @repl_prev_line = nil @ui.puts "unknown command: #{line}" begin require 'did_you_mean' @@ -1060,34 +1043,41 @@ def step_command type, arg case arg when nil, /\A\d+\z/ if type == :in && @tc.recorder&.replaying? - @tc << [:step, type, arg&.to_i] + request_tc [:step, type, arg&.to_i] else leave_subsession [:step, type, arg&.to_i] end when /\Aback\z/, /\Areset\z/ if type != :in @ui.puts "only `step #{arg}` is supported." :retry else - @tc << [:step, arg.to_sym] + request_tc [:step, arg.to_sym] end else @ui.puts "Unknown option: #{arg}" :retry end end def config_show key key = key.to_sym - if CONFIG_SET[key] + config_detail = CONFIG_SET[key] + + if config_detail v = CONFIG[key] - kv = "#{key} = #{v.nil? ? '(default)' : v.inspect}" - desc = CONFIG_SET[key][1] - line = "%-30s \# %s" % [kv, desc] + kv = "#{key} = #{v.inspect}" + desc = config_detail[1] + + if config_default = config_detail[3] + desc += " (default: #{config_default})" + end + + line = "%-34s \# %s" % [kv, desc] if line.size > SESSION.width @ui.puts "\# #{desc}\n#{kv}" else @ui.puts line end @@ -1133,11 +1123,11 @@ when /\A(\w+)\s*<<\s*(.+)\z/ config_set $1, $2, append: true when /\A\s*append\s+(\w+)\s+(.+)\z/ - config_set $1, $2 + config_set $1, $2, append: true when /\A(\w+)\z/ config_show $1 else @@ -1259,13 +1249,16 @@ def add_bp bp # don't repeat commands that add breakpoints @repl_prev_line = nil if @bps.has_key? bp.key - unless bp.duplicable? + if bp.duplicable? + bp + else @ui.puts "duplicated breakpoint: #{bp}" bp.disable + nil end else @bps[bp.key] = bp end end @@ -1318,11 +1311,11 @@ when /\A(\d+)\z/ add_line_breakpoint @tc.location.path, $1.to_i, cond: cond, command: cmd when /\A(.+)[:\s+](\d+)\z/ add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd when /\A(.+)([\.\#])(.+)\z/ - @tc << [:breakpoint, :method, $1, $2, $3, cond, cmd, path] + request_tc [:breakpoint, :method, $1, $2, $3, cond, cmd, path] return :noretry when nil add_check_breakpoint cond, path, cmd else @ui.puts "Unknown breakpoint format: #{arg}" @@ -1345,15 +1338,15 @@ expr = parse_break arg.strip cond = expr[:if] cmd = ['watch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do] path = Regexp.compile(expr[:path]) if expr[:path] - @tc << [:breakpoint, :watch, expr[:sig], cond, cmd, path] + request_tc [:breakpoint, :watch, expr[:sig], cond, cmd, path] end - def add_catch_breakpoint pat - bp = CatchBreakpoint.new(pat) + def add_catch_breakpoint pat, cond: nil + bp = CatchBreakpoint.new(pat, cond: cond) add_bp bp end def add_check_breakpoint cond, path, command bp = CheckBreakpoint.new(cond: cond, path: path, command: command) @@ -1367,19 +1360,38 @@ add_bp bp rescue Errno::ENOENT => e @ui.puts e.message end - def clear_line_breakpoints path - path = resolve_path(path) + def clear_breakpoints(&condition) @bps.delete_if do |k, bp| - if (Array === k) && DEBUGGER__.compare_path(k.first, path) + if condition.call(k, bp) bp.delete + true end end end + def clear_line_breakpoints path + path = resolve_path(path) + clear_breakpoints do |k, bp| + bp.is_a?(LineBreakpoint) && DEBUGGER__.compare_path(k.first, path) + end + rescue Errno::ENOENT + # just ignore + end + + def clear_catch_breakpoints *exception_names + clear_breakpoints do |k, bp| + bp.is_a?(CatchBreakpoint) && exception_names.include?(k[1]) + end + end + + def clear_all_breakpoints + clear_breakpoints{true} + end + def add_iseq_breakpoint iseq, **kw bp = ISeqBreakpoint.new(iseq, [:line], **kw) add_bp bp end @@ -1566,11 +1578,11 @@ restart_all_threads else DEBUGGER__.info "Leave subsession (nested #{@subsession_stack.size})" end - @tc << type if type + request_tc type if type @tc = nil rescue Exception => e STDERR.puts PP.pp([e, e.backtrace], ''.dup) raise end @@ -1581,21 +1593,34 @@ ## event def on_load iseq, src DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}" - @sr.add iseq, src + file_path, reloaded = @sr.add(iseq, src) + @ui.event :load, file_path, reloaded + pending_line_breakpoints = @bps.find_all do |key, bp| LineBreakpoint === bp && !bp.iseq end pending_line_breakpoints.each do |_key, bp| if DEBUGGER__.compare_path(bp.path, (iseq.absolute_path || iseq.path)) - bp.try_activate + bp.try_activate iseq end end + + if reloaded + @bps.find_all do |key, bp| + LineBreakpoint === bp && DEBUGGER__.compare_path(bp.path, file_path) + end.each do |_key, bp| + @bps.delete bp.key # to allow duplicate + if nbp = LineBreakpoint.copy(bp, iseq) + add_bp nbp + end + end + end end def resolve_path file File.realpath(File.expand_path(file)) rescue Errno::ENOENT @@ -1616,30 +1641,68 @@ def method_added tp b = tp.binding if var_name = b.local_variables.first mid = b.local_variable_get(var_name) - unresolved = false + resolved = true @bps.each{|k, bp| case bp when MethodBreakpoint if bp.method.nil? if bp.sig_method_name == mid.to_s bp.try_enable(added: true) end end - unresolved = true unless bp.enabled? + resolved = false if !bp.enabled? end } - unless unresolved - METHOD_ADDED_TRACKER.disable + + if resolved + Session.deactivate_method_added_trackers end + + case mid + when :method_added, :singleton_method_added + Session.create_method_added_tracker(tp.self, mid) + Session.create_method_added_tracker unless resolved + end end end + class ::Module + undef method_added + def method_added mid; end + def singleton_method_added mid; end + end + + def self.create_method_added_tracker mod, method_added_id, method_accessor = :method + m = mod.__send__(method_accessor, method_added_id) + METHOD_ADDED_TRACKERS[m] = TracePoint.new(:call) do |tp| + SESSION.method_added tp + end + end + + def self.activate_method_added_trackers + METHOD_ADDED_TRACKERS.each do |m, tp| + tp.enable(target: m) unless tp.enabled? + rescue ArgumentError + DEBUGGER__.warn "Methods defined under #{m.owner} can not track by the debugger." + end + end + + def self.deactivate_method_added_trackers + METHOD_ADDED_TRACKERS.each do |m, tp| + tp.disable if tp.enabled? + end + end + + METHOD_ADDED_TRACKERS = Hash.new + create_method_added_tracker Module, :method_added, :instance_method + create_method_added_tracker Module, :singleton_method_added, :instance_method + def width @ui.width end def check_postmortem @@ -2049,60 +2112,88 @@ cmds = CONFIG[:commands].split(';;') ::DEBUGGER__::SESSION.add_preset_commands "commands", cmds, kick: false, continue: false end end - class ::Module - undef method_added - def method_added mid; end - def singleton_method_added mid; end - end + # Inspector + + SHORT_INSPECT_LENGTH = 40 - def self.method_added tp - begin - SESSION.method_added tp - rescue Exception => e - p e + class LimitedPP + def self.pp(obj, max=80) + out = self.new(max) + catch out do + PP.singleline_pp(obj, out) + end + out.buf end - end - METHOD_ADDED_TRACKER = self.create_method_added_tracker + attr_reader :buf - SHORT_INSPECT_LENGTH = 40 + def initialize max + @max = max + @cnt = 0 + @buf = String.new + end - def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false - str = obj.inspect + def <<(other) + @buf << other - if short && str.length > max_length - str[0...max_length] + '...' + if @buf.size >= @max + @buf = @buf[0..@max] + '...' + throw self + end + end + end + + def self.safe_inspect obj, max_length: SHORT_INSPECT_LENGTH, short: false + if short + LimitedPP.pp(obj, max_length) else - str + obj.inspect end + rescue NoMethodError => e + klass, oid = M_CLASS.bind_call(obj), M_OBJECT_ID.bind_call(obj) + if obj == (r = e.receiver) + "<\##{klass.name}#{oid} does not have \#inspect>" + else + rklass, roid = M_CLASS.bind_call(r), M_OBJECT_ID.bind_call(r) + "<\##{klass.name}:#{roid} contains <\##{rklass}:#{roid} and it does not have #inspect>" + end rescue Exception => e - str = "<#inspect raises #{e.inspect}>" + "<#inspect raises #{e.inspect}>" end def self.warn msg log :WARN, msg end def self.info msg log :INFO, msg end - def self.log level, msg - @logfile = STDERR unless defined? @logfile - + def self.check_loglevel level lv = LOG_LEVELS[level] - config_lv = LOG_LEVELS[CONFIG[:log_level] || :WARN] + config_lv = LOG_LEVELS[CONFIG[:log_level]] + lv <= config_lv + end - if defined? SESSION - pi = SESSION.process_info - process_info = pi ? "[#{pi}]" : nil + def self.debug(&b) + if check_loglevel :DEBUG + log :DEBUG, b.call end + end - if lv <= config_lv + def self.log level, msg + if check_loglevel level + @logfile = STDERR unless defined? @logfile + + if defined? SESSION + pi = SESSION.process_info + process_info = pi ? "[#{pi}]" : nil + end + if level == :WARN # :WARN on debugger is general information @logfile.puts "DEBUGGER#{process_info}: #{msg}" @logfile.flush else @@ -2135,11 +2226,11 @@ end module ForkInterceptor if Process.respond_to? :_fork def _fork - return yield unless defined?(SESSION) && SESSION.active? + return super unless defined?(SESSION) && SESSION.active? parent_hook, child_hook = __fork_setup_for_debugger super.tap do |pid| if pid != 0 @@ -2151,11 +2242,11 @@ end end end else def fork(&given_block) - return yield unless defined?(SESSION) && SESSION.active? + return super unless defined?(SESSION) && SESSION.active? parent_hook, child_hook = __fork_setup_for_debugger if given_block new_block = proc { # after fork: child @@ -2176,16 +2267,14 @@ end end end private def __fork_setup_for_debugger - unless fork_mode = CONFIG[:fork_mode] - if CONFIG[:parent_on_fork] - fork_mode = :parent - else - fork_mode = :both - end + fork_mode = CONFIG[:fork_mode] + + if fork_mode == :both && CONFIG[:parent_on_fork] + fork_mode = :parent end parent_pid = Process.pid # before fork @@ -2307,6 +2396,15 @@ end class Binding alias break debugger alias b debugger +end + +# for Ruby 2.6 compatibility +unless method(:p).unbind.respond_to? :bind_call + class UnboundMethod + def bind_call(obj, *args) + self.bind(obj).call(*args) + end + end end