5c9b454291
Change how the resolver is initialized so the instance can be reset to sane default values loaded from the operating system.
728 lines
20 KiB
Ruby
728 lines
20 KiB
Ruby
# -*- coding: binary -*-
|
|
require 'find'
|
|
require 'erb'
|
|
require 'rexml/document'
|
|
require 'fileutils'
|
|
require 'digest/md5'
|
|
|
|
module Msf
|
|
module Ui
|
|
module Console
|
|
|
|
#
|
|
# A user interface driver on a console interface.
|
|
#
|
|
class Driver < Msf::Ui::Driver
|
|
|
|
ConfigCore = "framework/core"
|
|
ConfigGroup = "framework/ui/console"
|
|
|
|
DefaultPrompt = "%undmsf#{Metasploit::Framework::Version::MAJOR}%clr"
|
|
DefaultPromptChar = "%clr>"
|
|
|
|
#
|
|
# Console Command Dispatchers to be loaded after the Core dispatcher.
|
|
#
|
|
CommandDispatchers = [
|
|
CommandDispatcher::Modules,
|
|
CommandDispatcher::Jobs,
|
|
CommandDispatcher::Resource,
|
|
CommandDispatcher::Db,
|
|
CommandDispatcher::Creds,
|
|
CommandDispatcher::Developer,
|
|
CommandDispatcher::DNS
|
|
]
|
|
|
|
#
|
|
# The console driver processes various framework notified events.
|
|
#
|
|
include FrameworkEventManager
|
|
|
|
#
|
|
# The console driver is a command shell.
|
|
#
|
|
include Rex::Ui::Text::DispatcherShell
|
|
|
|
include Rex::Ui::Text::Resource
|
|
|
|
#
|
|
# Initializes a console driver instance with the supplied prompt string and
|
|
# prompt character. The optional hash can take extra values that will
|
|
# serve to initialize the console driver.
|
|
#
|
|
# @option opts [Boolean] 'AllowCommandPassthru' (true) Whether to allow
|
|
# unrecognized commands to be executed by the system shell
|
|
# @option opts [Boolean] 'Readline' (true) Whether to use the readline or not
|
|
# @option opts [Boolean] 'RealReadline' (false) Whether to use the system's
|
|
# readline library instead of RBReadline
|
|
# @option opts [String] 'HistFile' (Msf::Config.history_file) Path to a file
|
|
# where we can store command history
|
|
# @option opts [Array<String>] 'Resources' ([]) A list of resource files to
|
|
# load. If no resources are given, will load the default resource script,
|
|
# 'msfconsole.rc' in the user's {Msf::Config.config_directory config
|
|
# directory}
|
|
# @option opts [Boolean] 'SkipDatabaseInit' (false) Whether to skip
|
|
# connecting to the database and running migrations
|
|
def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = {})
|
|
choose_readline(opts)
|
|
|
|
histfile = opts['HistFile'] || Msf::Config.history_file
|
|
|
|
begin
|
|
FeatureManager.instance.load_config
|
|
rescue StandardException => e
|
|
elog(e)
|
|
end
|
|
|
|
if opts['DeferModuleLoads'].nil?
|
|
opts['DeferModuleLoads'] = Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DEFER_MODULE_LOADS)
|
|
end
|
|
|
|
# Initialize attributes
|
|
|
|
framework_create_options = opts.merge({ 'DeferModuleLoads' => true })
|
|
|
|
if Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DNS_FEATURE)
|
|
dns_resolver = Rex::Proto::DNS::CachedResolver.new
|
|
dns_resolver.extend(Rex::Proto::DNS::CustomNameserverProvider)
|
|
dns_resolver.load_config if dns_resolver.has_config?
|
|
|
|
# Defer loading of modules until paths from opts can be added below
|
|
framework_create_options = framework_create_options.merge({ 'CustomDnsResolver' => dns_resolver })
|
|
end
|
|
self.framework = opts['Framework'] || Msf::Simple::Framework.create(framework_create_options)
|
|
|
|
if self.framework.datastore['Prompt']
|
|
prompt = self.framework.datastore['Prompt']
|
|
prompt_char = self.framework.datastore['PromptChar'] || DefaultPromptChar
|
|
end
|
|
|
|
# Call the parent
|
|
super(prompt, prompt_char, histfile, framework, :msfconsole)
|
|
|
|
# Temporarily disable output
|
|
self.disable_output = true
|
|
|
|
# Load pre-configuration
|
|
load_preconfig
|
|
|
|
# Initialize the user interface to use a different input and output
|
|
# handle if one is supplied
|
|
input = opts['LocalInput']
|
|
input ||= Rex::Ui::Text::Input::Stdio.new
|
|
|
|
if !opts['Readline']
|
|
input.disable_readline
|
|
end
|
|
|
|
if (opts['LocalOutput'])
|
|
if (opts['LocalOutput'].kind_of?(String))
|
|
output = Rex::Ui::Text::Output::File.new(opts['LocalOutput'])
|
|
else
|
|
output = opts['LocalOutput']
|
|
end
|
|
else
|
|
output = Rex::Ui::Text::Output::Stdio.new
|
|
end
|
|
|
|
init_ui(input, output)
|
|
init_tab_complete
|
|
|
|
# Add the core command dispatcher as the root of the dispatcher
|
|
# stack
|
|
enstack_dispatcher(CommandDispatcher::Core)
|
|
|
|
# Report readline error if there was one..
|
|
if !@rl_err.nil?
|
|
print_error("***")
|
|
print_error("* Unable to load readline: #{@rl_err}")
|
|
print_error("* Falling back to RbReadLine")
|
|
print_error("***")
|
|
end
|
|
|
|
# Load the other "core" command dispatchers
|
|
CommandDispatchers.each do |dispatcher_class|
|
|
dispatcher = enstack_dispatcher(dispatcher_class)
|
|
dispatcher.load_config(opts['Config'])
|
|
end
|
|
|
|
if !framework.db || !framework.db.active
|
|
if framework.db.error == "disabled"
|
|
print_warning("Database support has been disabled")
|
|
else
|
|
error_msg = "#{framework.db.error.class.is_a?(String) ? "#{framework.db.error.class} " : nil}#{framework.db.error}"
|
|
print_warning("No database support: #{error_msg}")
|
|
end
|
|
end
|
|
|
|
# Register event handlers
|
|
register_event_handlers
|
|
|
|
# Re-enable output
|
|
self.disable_output = false
|
|
|
|
# Whether or not command passthru should be allowed
|
|
self.command_passthru = opts.fetch('AllowCommandPassthru', true)
|
|
|
|
# Whether or not to confirm before exiting
|
|
self.confirm_exit = opts['ConfirmExit']
|
|
|
|
# Initialize the module paths only if we didn't get passed a Framework instance and 'DeferModuleLoads' is false
|
|
unless opts['Framework']
|
|
# Configure the framework module paths
|
|
self.framework.init_module_paths(module_paths: opts['ModulePath'], defer_module_loads: opts['DeferModuleLoads'])
|
|
end
|
|
|
|
unless opts['DeferModuleLoads']
|
|
framework.threads.spawn("ModuleCacheRebuild", true) do
|
|
framework.modules.refresh_cache_from_module_files
|
|
end
|
|
end
|
|
|
|
# Load console-specific configuration (after module paths are added)
|
|
load_config(opts['Config'])
|
|
|
|
# Process things before we actually display the prompt and get rocking
|
|
on_startup(opts)
|
|
|
|
# Process any resource scripts
|
|
if opts['Resource'].blank?
|
|
# None given, load the default
|
|
default_resource = ::File.join(Msf::Config.config_directory, 'msfconsole.rc')
|
|
load_resource(default_resource) if ::File.exist?(default_resource)
|
|
else
|
|
opts['Resource'].each { |r|
|
|
load_resource(r)
|
|
}
|
|
end
|
|
|
|
# Process persistent job handler
|
|
begin
|
|
restore_handlers = JSON.parse(File.read(Msf::Config.persist_file))
|
|
rescue Errno::ENOENT, JSON::ParserError
|
|
restore_handlers = nil
|
|
end
|
|
|
|
if restore_handlers
|
|
print_status("Starting persistent handler(s)...")
|
|
|
|
restore_handlers.each do |handler_opts|
|
|
handler = framework.modules.create(handler_opts['mod_name'])
|
|
handler.exploit_simple(handler_opts['mod_options'])
|
|
end
|
|
end
|
|
|
|
# Process any additional startup commands
|
|
if opts['XCommands'] and opts['XCommands'].kind_of? Array
|
|
opts['XCommands'].each { |c|
|
|
run_single(c)
|
|
}
|
|
end
|
|
end
|
|
|
|
#
|
|
# Loads configuration that needs to be analyzed before the framework
|
|
# instance is created.
|
|
#
|
|
def load_preconfig
|
|
begin
|
|
conf = Msf::Config.load
|
|
rescue
|
|
wlog("Failed to load configuration: #{$!}")
|
|
return
|
|
end
|
|
|
|
if (conf.group?(ConfigCore))
|
|
conf[ConfigCore].each_pair { |k, v|
|
|
on_variable_set(true, k, v)
|
|
}
|
|
end
|
|
end
|
|
|
|
#
|
|
# Loads configuration for the console.
|
|
#
|
|
def load_config(path=nil)
|
|
begin
|
|
conf = Msf::Config.load(path)
|
|
rescue
|
|
wlog("Failed to load configuration: #{$!}")
|
|
return
|
|
end
|
|
|
|
# If we have configuration, process it
|
|
if (conf.group?(ConfigGroup))
|
|
conf[ConfigGroup].each_pair { |k, v|
|
|
case k.downcase
|
|
when 'activemodule'
|
|
run_single("use #{v}")
|
|
when 'activeworkspace'
|
|
if framework.db.active
|
|
workspace = framework.db.find_workspace(v)
|
|
framework.db.workspace = workspace if workspace
|
|
end
|
|
end
|
|
}
|
|
end
|
|
end
|
|
|
|
#
|
|
# Generate configuration for the console.
|
|
#
|
|
def get_config
|
|
# Build out the console config group
|
|
group = {}
|
|
|
|
if (active_module)
|
|
group['ActiveModule'] = active_module.fullname
|
|
end
|
|
|
|
if framework.db.active
|
|
unless framework.db.workspace.default?
|
|
group['ActiveWorkspace'] = framework.db.workspace.name
|
|
end
|
|
end
|
|
|
|
group
|
|
end
|
|
|
|
def get_config_core
|
|
ConfigCore
|
|
end
|
|
|
|
def get_config_group
|
|
ConfigGroup
|
|
end
|
|
|
|
#
|
|
# Saves configuration for the console.
|
|
#
|
|
def save_config
|
|
begin
|
|
Msf::Config.save(ConfigGroup => get_config)
|
|
rescue ::Exception
|
|
print_error("Failed to save console config: #{$!}")
|
|
end
|
|
end
|
|
|
|
#
|
|
# Saves the recent history to the specified file
|
|
#
|
|
def save_recent_history(path)
|
|
num = Readline::HISTORY.length - hist_last_saved - 1
|
|
|
|
tmprc = ""
|
|
num.times { |x|
|
|
tmprc << Readline::HISTORY[hist_last_saved + x] + "\n"
|
|
}
|
|
|
|
if tmprc.length > 0
|
|
print_status("Saving last #{num} commands to #{path} ...")
|
|
save_resource(tmprc, path)
|
|
else
|
|
print_error("No commands to save!")
|
|
end
|
|
|
|
# Always update this, even if we didn't save anything. We do this
|
|
# so that we don't end up saving the "makerc" command itself.
|
|
self.hist_last_saved = Readline::HISTORY.length
|
|
end
|
|
|
|
#
|
|
# Creates the resource script file for the console.
|
|
#
|
|
def save_resource(data, path=nil)
|
|
path ||= File.join(Msf::Config.config_directory, 'msfconsole.rc')
|
|
|
|
begin
|
|
rcfd = File.open(path, 'w')
|
|
rcfd.write(data)
|
|
rcfd.close
|
|
rescue ::Exception
|
|
end
|
|
end
|
|
|
|
#
|
|
# Called before things actually get rolling such that banners can be
|
|
# displayed, scripts can be processed, and other fun can be had.
|
|
#
|
|
def on_startup(opts = {})
|
|
# Check for modules that failed to load
|
|
if framework.modules.module_load_error_by_path.length > 0
|
|
wlog("The following modules could not be loaded!")
|
|
|
|
framework.modules.module_load_error_by_path.each do |path, _error|
|
|
wlog("\t#{path}")
|
|
end
|
|
end
|
|
|
|
if framework.modules.module_load_warnings.length > 0
|
|
print_warning("The following modules were loaded with warnings:")
|
|
|
|
framework.modules.module_load_warnings.each do |path, _error|
|
|
wlog("\t#{path}")
|
|
end
|
|
end
|
|
|
|
if framework.db&.active
|
|
framework.db.workspace = framework.db.default_workspace unless framework.db.workspace
|
|
end
|
|
|
|
framework.events.on_ui_start(Msf::Framework::Revision)
|
|
|
|
if $msf_spinner_thread
|
|
$msf_spinner_thread.kill
|
|
$stderr.print "\r" + (" " * 50) + "\n"
|
|
end
|
|
|
|
run_single("banner") unless opts['DisableBanner']
|
|
|
|
payloads_manifest_errors = []
|
|
begin
|
|
payloads_manifest_errors = ::MetasploitPayloads.manifest_errors if framework.features.enabled?(::Msf::FeatureManager::METASPLOIT_PAYLOAD_WARNINGS)
|
|
rescue ::StandardError => e
|
|
$stderr.print('Could not verify the integrity of the Metasploit Payloads manifest')
|
|
elog(e)
|
|
end
|
|
|
|
av_warning_message if (framework.eicar_corrupted? || payloads_manifest_errors.any?)
|
|
|
|
if framework.features.enabled?(::Msf::FeatureManager::METASPLOIT_PAYLOAD_WARNINGS)
|
|
if payloads_manifest_errors.any?
|
|
warn_msg = "Metasploit Payloads manifest errors:\n"
|
|
payloads_manifest_errors.each do |file|
|
|
warn_msg << "\t#{file[:path]} : #{file[:error]}\n"
|
|
end
|
|
$stderr.print(warn_msg)
|
|
end
|
|
end
|
|
|
|
opts["Plugins"].each do |plug|
|
|
run_single("load '#{plug}'")
|
|
end if opts["Plugins"]
|
|
|
|
self.on_command_proc = Proc.new { |command| framework.events.on_ui_command(command) }
|
|
end
|
|
|
|
def av_warning_message
|
|
avdwarn = "\e[31m"\
|
|
"Warning: This copy of the Metasploit Framework has been corrupted by an installed anti-virus program."\
|
|
" We recommend that you disable your anti-virus or exclude your Metasploit installation path, "\
|
|
"then restore the removed files from quarantine or reinstall the framework.\e[0m"\
|
|
"\n\n"
|
|
|
|
$stderr.puts(Rex::Text.wordwrap(avdwarn, 0, 80))
|
|
end
|
|
|
|
#
|
|
# Called when a variable is set to a specific value. This allows the
|
|
# console to do extra processing, such as enabling logging or doing
|
|
# some other kind of task. If this routine returns false it will indicate
|
|
# that the variable is not being set to a valid value.
|
|
#
|
|
def on_variable_set(glob, var, val)
|
|
case var.downcase
|
|
when 'sessionlogging'
|
|
handle_session_logging(val) if glob
|
|
when 'sessiontlvlogging'
|
|
handle_session_tlv_logging(val) if glob
|
|
when 'consolelogging'
|
|
handle_console_logging(val) if glob
|
|
when 'loglevel'
|
|
handle_loglevel(val) if glob
|
|
when 'payload'
|
|
handle_payload(val)
|
|
when 'ssh_ident'
|
|
handle_ssh_ident(val)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Called when a variable is unset. If this routine returns false it is an
|
|
# indication that the variable should not be allowed to be unset.
|
|
#
|
|
def on_variable_unset(glob, var)
|
|
case var.downcase
|
|
when 'sessionlogging'
|
|
handle_session_logging('0') if glob
|
|
when 'sessiontlvlogging'
|
|
handle_session_tlv_logging('false') if glob
|
|
when 'consolelogging'
|
|
handle_console_logging('0') if glob
|
|
when 'loglevel'
|
|
handle_loglevel(nil) if glob
|
|
end
|
|
end
|
|
|
|
#
|
|
# Proxies to shell.rb's update prompt with our own extras
|
|
#
|
|
def update_prompt(*args)
|
|
if args.empty?
|
|
pchar = framework.datastore['PromptChar'] || DefaultPromptChar
|
|
p = framework.datastore['Prompt'] || DefaultPrompt
|
|
p = "#{p} #{active_module.type}(%bld%red#{active_module.promptname}%clr)" if active_module
|
|
super(p, pchar)
|
|
else
|
|
# Don't squash calls from within lib/rex/ui/text/shell.rb
|
|
super(*args)
|
|
end
|
|
end
|
|
|
|
#
|
|
# The framework instance associated with this driver.
|
|
#
|
|
attr_reader :framework
|
|
#
|
|
# Whether or not to confirm before exiting
|
|
#
|
|
attr_reader :confirm_exit
|
|
#
|
|
# Whether or not commands can be passed through.
|
|
#
|
|
attr_reader :command_passthru
|
|
#
|
|
# The active module associated with the driver.
|
|
#
|
|
attr_accessor :active_module
|
|
#
|
|
# The active session associated with the driver.
|
|
#
|
|
attr_accessor :active_session
|
|
|
|
def stop
|
|
framework.events.on_ui_stop()
|
|
super
|
|
end
|
|
|
|
protected
|
|
|
|
attr_writer :framework # :nodoc:
|
|
attr_writer :confirm_exit # :nodoc:
|
|
attr_writer :command_passthru # :nodoc:
|
|
|
|
#
|
|
# If an unknown command was passed, try to see if it's a valid local
|
|
# executable. This is only allowed if command passthru has been permitted
|
|
#
|
|
def unknown_command(method, line)
|
|
if File.basename(method) == 'msfconsole'
|
|
print_error('msfconsole cannot be run inside msfconsole')
|
|
return
|
|
end
|
|
|
|
[method, method+".exe"].each do |cmd|
|
|
if command_passthru && Rex::FileUtils.find_full_path(cmd)
|
|
|
|
self.busy = true
|
|
begin
|
|
run_unknown_command(line)
|
|
rescue ::Errno::EACCES, ::Errno::ENOENT
|
|
print_error("Permission denied exec: #{line}")
|
|
end
|
|
self.busy = false
|
|
return
|
|
end
|
|
end
|
|
|
|
if framework.modules.create(method)
|
|
super
|
|
if prompt_yesno "This is a module we can load. Do you want to use #{method}?"
|
|
run_single "use #{method}"
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
super
|
|
end
|
|
|
|
def run_unknown_command(command)
|
|
print_status("exec: #{command}")
|
|
print_line('')
|
|
system(command)
|
|
end
|
|
|
|
##
|
|
#
|
|
# Handlers for various global configuration values
|
|
#
|
|
##
|
|
|
|
#
|
|
# SessionLogging.
|
|
#
|
|
def handle_session_logging(val)
|
|
if (val =~ /^(y|t|1)/i)
|
|
Msf::Logging.enable_session_logging(true)
|
|
print_line("Session logging will be enabled for future sessions.")
|
|
else
|
|
Msf::Logging.enable_session_logging(false)
|
|
print_line("Session logging will be disabled for future sessions.")
|
|
end
|
|
end
|
|
|
|
#
|
|
# ConsoleLogging.
|
|
#
|
|
def handle_console_logging(val)
|
|
if (val =~ /^(y|t|1)/i)
|
|
Msf::Logging.enable_log_source('console')
|
|
print_line("Console logging is now enabled.")
|
|
|
|
set_log_source('console')
|
|
|
|
rlog("\n[*] Console logging started: #{Time.now}\n\n", 'console')
|
|
else
|
|
rlog("\n[*] Console logging stopped: #{Time.now}\n\n", 'console')
|
|
|
|
unset_log_source
|
|
|
|
Msf::Logging.disable_log_source('console')
|
|
print_line("Console logging is now disabled.")
|
|
end
|
|
end
|
|
|
|
#
|
|
# This method handles adjusting the global log level threshold.
|
|
#
|
|
def handle_loglevel(val)
|
|
set_log_level(Rex::LogSource, val)
|
|
set_log_level(Msf::LogSource, val)
|
|
end
|
|
|
|
#
|
|
# This method handles setting a desired payload
|
|
#
|
|
# TODO: Move this out of the console driver!
|
|
#
|
|
def handle_payload(val)
|
|
if framework && !framework.payloads.valid?(val)
|
|
return false
|
|
elsif active_module && (active_module.exploit? || active_module.evasion?)
|
|
return false unless active_module.is_payload_compatible?(val)
|
|
elsif active_module && !framework.features.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS)
|
|
active_module.datastore.clear_non_user_defined
|
|
elsif framework && !framework.features.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS)
|
|
framework.datastore.clear_non_user_defined
|
|
end
|
|
end
|
|
|
|
#
|
|
# This method monkeypatches Net::SSH's client identification string
|
|
#
|
|
# TODO: Move this out of the console driver!
|
|
#
|
|
def handle_ssh_ident(val)
|
|
# HACK: Suppress already initialized constant warning
|
|
verbose, $VERBOSE = $VERBOSE, nil
|
|
|
|
return false unless val.is_a?(String) && !val.empty?
|
|
|
|
require 'net/ssh'
|
|
|
|
# HACK: Bypass dynamic constant assignment error
|
|
::Net::SSH::Transport::ServerVersion.const_set(:PROTO_VERSION, val)
|
|
|
|
true
|
|
rescue LoadError
|
|
print_error('Net::SSH could not be loaded')
|
|
false
|
|
rescue NameError
|
|
print_error('Invalid constant Net::SSH::Transport::ServerVersion::PROTO_VERSION')
|
|
false
|
|
ensure
|
|
# Restore warning
|
|
$VERBOSE = verbose
|
|
end
|
|
|
|
def handle_session_tlv_logging(val)
|
|
return false if val.nil?
|
|
|
|
if val.casecmp?('console') || val.casecmp?('true') || val.casecmp?('false')
|
|
return true
|
|
elsif val.start_with?('file:') && !val.split('file:').empty?
|
|
pathname = ::Pathname.new(val.split('file:').last)
|
|
|
|
# Check if we want to write the log to file
|
|
if ::File.file?(pathname)
|
|
if ::File.writable?(pathname)
|
|
return true
|
|
else
|
|
print_status "No write permissions for log output file: #{pathname}"
|
|
return false
|
|
end
|
|
# Check if we want to write the log file to a directory
|
|
elsif ::File.directory?(pathname)
|
|
if ::File.writable?(pathname)
|
|
return true
|
|
else
|
|
print_status "No write permissions for log output directory: #{pathname}"
|
|
return false
|
|
end
|
|
# Check if the subdirectory exists
|
|
elsif ::File.directory?(pathname.dirname)
|
|
if ::File.writable?(pathname.dirname)
|
|
return true
|
|
else
|
|
print_status "No write permissions for log output directory: #{pathname.dirname}"
|
|
return false
|
|
end
|
|
else
|
|
# Else the directory doesn't exist. Check if we can create it.
|
|
begin
|
|
::FileUtils.mkdir_p(pathname.dirname)
|
|
return true
|
|
rescue ::StandardError => e
|
|
print_status "Error when trying to create directory #{pathname.dirname}: #{e.message}"
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
|
|
false
|
|
end
|
|
|
|
# Require the appropriate readline library based on the user's preference.
|
|
#
|
|
# @return [void]
|
|
def choose_readline(opts)
|
|
# Choose a readline library before calling the parent
|
|
@rl_err = nil
|
|
if opts['RealReadline']
|
|
# Remove the gem version from load path to be sure we're getting the
|
|
# stdlib readline.
|
|
gem_dir = Gem::Specification.find_all_by_name('rb-readline').first.gem_dir
|
|
rb_readline_path = File.join(gem_dir, "lib")
|
|
index = $LOAD_PATH.index(rb_readline_path)
|
|
# Bundler guarantees that the gem will be there, so it should be safe to
|
|
# assume we found it in the load path, but check to be on the safe side.
|
|
if index
|
|
$LOAD_PATH.delete_at(index)
|
|
end
|
|
end
|
|
|
|
begin
|
|
require 'readline'
|
|
rescue ::LoadError => e
|
|
if @rl_err.nil? && index
|
|
# Then this is the first time the require failed and we have an index
|
|
# for the gem version as a fallback.
|
|
@rl_err = e
|
|
# Put the gem back and see if that works
|
|
$LOAD_PATH.insert(index, rb_readline_path)
|
|
index = rb_readline_path = nil
|
|
retry
|
|
else
|
|
# Either we didn't have the gem to fall back on, or we failed twice.
|
|
# Nothing more we can do here.
|
|
raise e
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|