1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
|
# frozen_string_literal: true
module IRB
module Debug
IRB_DIR = File.expand_path('..', __dir__)
class << self
def insert_debug_break(pre_cmds: nil, do_cmds: nil)
options = { oneshot: true, hook_call: false }
if pre_cmds || do_cmds
options[:command] = ['irb', pre_cmds, do_cmds]
end
if DEBUGGER__::LineBreakpoint.instance_method(:initialize).parameters.include?([:key, :skip_src])
options[:skip_src] = true
end
# To make debugger commands like `next` or `continue` work without asking
# the user to quit IRB after that, we need to exit IRB first and then hit
# a TracePoint on #debug_break.
file, lineno = IRB::Irb.instance_method(:debug_break).source_location
DEBUGGER__::SESSION.add_line_breakpoint(file, lineno + 1, **options)
end
def setup(irb)
# When debug session is not started at all
unless defined?(DEBUGGER__::SESSION)
begin
require "debug/session"
rescue LoadError # debug.gem is not written in Gemfile
return false unless load_bundled_debug_gem
end
DEBUGGER__::CONFIG.set_config
configure_irb_for_debugger(irb)
DEBUGGER__.initialize_session{ IRB::Debug::UI.new(irb) }
end
# When debug session was previously started but not by IRB
if defined?(DEBUGGER__::SESSION) && !irb.context.with_debugger
configure_irb_for_debugger(irb)
DEBUGGER__::SESSION.reset_ui(IRB::Debug::UI.new(irb))
end
# Apply patches to debug gem so it skips IRB frames
unless DEBUGGER__.respond_to?(:capture_frames_without_irb)
DEBUGGER__.singleton_class.send(:alias_method, :capture_frames_without_irb, :capture_frames)
def DEBUGGER__.capture_frames(*args)
frames = capture_frames_without_irb(*args)
frames.reject! do |frame|
frame.realpath&.start_with?(IRB_DIR) || frame.path == "<internal:prelude>"
end
frames
end
DEBUGGER__::ThreadClient.prepend(SkipPathHelperForIRB)
end
if !DEBUGGER__::CONFIG[:no_hint] && irb.context.io.is_a?(RelineInputMethod)
Reline.output_modifier_proc = proc do |input, complete:|
unless input.strip.empty?
cmd = input.split(/\s/, 2).first
if !complete && DEBUGGER__.commands.key?(cmd)
input = input.sub(/\n$/, " # debug command\n")
end
end
irb.context.colorize_input(input, complete: complete)
end
end
true
end
private
def configure_irb_for_debugger(irb)
require 'irb/debug/ui'
IRB.instance_variable_set(:@debugger_irb, irb)
irb.context.with_debugger = true
irb.context.irb_name += ":rdbg"
irb.context.io.load_history if irb.context.io.class < HistorySavingAbility
end
module SkipPathHelperForIRB
def skip_internal_path?(path)
# The latter can be removed once https://github.com/ruby/debug/issues/866 is resolved
super || path.match?(IRB_DIR) || path.match?('<internal:prelude>')
end
end
# This is used when debug.gem is not written in Gemfile. Even if it's not
# installed by `bundle install`, debug.gem is installed by default because
# it's a bundled gem. This method tries to activate and load that.
def load_bundled_debug_gem
# Discover latest debug.gem under GEM_PATH
debug_gem = Gem.paths.path.flat_map { |path| Dir.glob("#{path}/gems/debug-*") }.select do |path|
File.basename(path).match?(/\Adebug-\d+\.\d+\.\d+(\w+)?\z/)
end.sort_by do |path|
Gem::Version.new(File.basename(path).delete_prefix('debug-'))
end.last
return false unless debug_gem
# Discover debug/debug.so under extensions for Ruby 3.2+
ext_name = "/debug/debug.#{RbConfig::CONFIG['DLEXT']}"
ext_path = Gem.paths.path.flat_map do |path|
Dir.glob("#{path}/extensions/**/#{File.basename(debug_gem)}#{ext_name}")
end.first
# Attempt to forcibly load the bundled gem
if ext_path
$LOAD_PATH << ext_path.delete_suffix(ext_name)
end
$LOAD_PATH << "#{debug_gem}/lib"
begin
require "debug/session"
puts "Loaded #{File.basename(debug_gem)}"
true
rescue LoadError
false
end
end
end
end
end
|