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