Skip to content

Commit

Permalink
WIP: Replace Readline with Reline
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanusz-r7 committed Aug 14, 2024
1 parent 233f6dc commit 0b48387
Show file tree
Hide file tree
Showing 20 changed files with 79 additions and 202 deletions.
10 changes: 4 additions & 6 deletions lib/msf/ui/console/command_dispatcher/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,7 @@ def cmd_features_tabs(_str, words)
end

def cmd_history(*args)
length = Readline::HISTORY.length
length = Reline::HISTORY.length

if length < @history_limit
limit = length
Expand All @@ -780,10 +780,8 @@ def cmd_history(*args)
limit = val.to_i
end
when '-c'
if Readline::HISTORY.respond_to?(:clear)
Readline::HISTORY.clear
elsif defined?(RbReadline)
RbReadline.clear_history
if Reline::HISTORY.respond_to?(:clear)
Reline::HISTORY.clear
else
print_error('Could not clear history, skipping file')
return false
Expand All @@ -808,7 +806,7 @@ def cmd_history(*args)

(start..length-1).each do |pos|
cmd_num = (pos + 1).to_s
print_line "#{cmd_num.ljust(pad_len)} #{Readline::HISTORY[pos]}"
print_line "#{cmd_num.ljust(pad_len)} #{Reline::HISTORY[pos]}"
end
end

Expand Down
4 changes: 4 additions & 0 deletions lib/msf/ui/console/command_dispatcher/developer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,10 @@ def cmd_time_help
private

def modified_files
# Temporary work-around until Open3 gets fixed on Windows 11:
# https://github.com/ruby/open3/issues/9
return [] if Rex::Compat.is_cygwin || Rex::Compat.is_windows

# Using an array avoids shelling out, so we avoid escaping/quoting
changed_files = %w[git diff --name-only]
begin
Expand Down
25 changes: 12 additions & 13 deletions lib/msf/ui/console/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ class Driver < Msf::Ui::Driver
# @option opts [Boolean] 'SkipDatabaseInit' (false) Whether to skip
# connecting to the database and running migrations
def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = {})
@rl_err = nil
choose_readline(opts)

histfile = opts['HistFile'] || Msf::Config.history_file

begin
Expand Down Expand Up @@ -132,14 +132,6 @@ def initialize(prompt = DefaultPrompt, prompt_char = DefaultPromptChar, opts = {
# 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)
Expand Down Expand Up @@ -323,11 +315,11 @@ def save_config
# Saves the recent history to the specified file
#
def save_recent_history(path)
num = Readline::HISTORY.length - hist_last_saved - 1
num = Reline::HISTORY.length - hist_last_saved - 1

tmprc = ""
num.times { |x|
tmprc << Readline::HISTORY[hist_last_saved + x] + "\n"
tmprc << Reline::HISTORY[hist_last_saved + x] + "\n"
}

if tmprc.length > 0
Expand All @@ -339,7 +331,7 @@ def save_recent_history(path)

# 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
self.hist_last_saved = Reline::HISTORY.length
end

#
Expand Down Expand Up @@ -708,7 +700,6 @@ def handle_session_tlv_logging(val)
# @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.
Expand All @@ -723,7 +714,15 @@ def choose_readline(opts)
end

begin
# Loading this gem can fail if we do not have the required C dynamic libraries on the system
require 'readline'

if !@rl_err.nil?
print_error('***')
print_error("* Unable to load readline: #{@rl_err}")
print_error('* Falling back to RbReadLine')
print_error('***')
end
rescue ::LoadError => e
if @rl_err.nil? && index
# Then this is the first time the require failed and we have an index
Expand Down
6 changes: 3 additions & 3 deletions lib/msf/ui/console/module_option_tab_completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,18 @@ def tab_complete_option(mod, str, words)
option_name = str.chop
option_value = ''

::Readline.completion_append_character = ' '
::Reline.completion_append_character = ' '
return tab_complete_option_values(mod, option_value, words, opt: option_name).map { |value| "#{str}#{value}" }
elsif str.include?('=')
str_split = str.split('=')
option_name = str_split[0].strip
option_value = str_split[1].strip

::Readline.completion_append_character = ' '
::Reline.completion_append_character = ' '
return tab_complete_option_values(mod, option_value, words, opt: option_name).map { |value| "#{option_name}=#{value}" }
end

::Readline.completion_append_character = ''
::Reline.completion_append_character = ''
tab_complete_option_names(mod, str, words).map { |name| "#{name}=" }
end

Expand Down
4 changes: 2 additions & 2 deletions lib/msf/ui/debug.rb
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,13 @@ def self.framework_config(framework)
end

def self.history(driver)
end_pos = Readline::HISTORY.length - 1
end_pos = Reline::HISTORY.length - 1
start_pos = end_pos - COMMAND_HISTORY_TOTAL > driver.hist_last_saved ? end_pos - (COMMAND_HISTORY_TOTAL - 1) : driver.hist_last_saved

commands = ''
while start_pos <= end_pos
# Formats command position in history to 6 characters in length
commands += "#{'%-6.6s' % start_pos.to_s} #{Readline::HISTORY[start_pos]}\n"
commands += "#{'%-6.6s' % start_pos.to_s} #{Reline::HISTORY[start_pos]}\n"
start_pos += 1
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -902,7 +902,7 @@ def tab_complete_cdirectory(str, words)

def tab_complete_path(str, words, dir_only)
if client.platform == 'windows'
::Readline.completion_case_fold = true
::Reline.completion_case_fold = true
end
if client.commands.include?(COMMAND_ID_STDAPI_FS_LS)
expanded = str
Expand All @@ -915,7 +915,7 @@ def tab_complete_path(str, words, dir_only)
# This is annoying if we're recursively tab-traversing our way through subdirectories -
# we may want to continue traversing, but MSF will add a space, requiring us to back up to continue
# tab-completing our way through successive subdirectories.
::Readline.completion_append_character = nil
::Reline.completion_append_character = nil
end
results
else
Expand Down
38 changes: 1 addition & 37 deletions lib/rex/post/sql/ui/console/interactive_sql_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,14 @@ def _multiline_with_fallback
history_file = Msf::Config.history_file_for_session_type(session_type: name, interactive: true)
return { status: :fail, errors: ["Unable to get history file for session type: #{name}"] } if history_file.nil?

# Multiline (Reline) and fallback (Readline) have separate history contexts as they are two different libraries.
framework.history_manager.with_context(history_file: history_file , name: name, input_library: :reline) do
framework.history_manager.with_context(history_file: history_file, name: name) do
query = _multiline
end

if query[:status] == :fail
framework.history_manager.with_context(history_file: history_file, name: name, input_library: :readline) do
query = _fallback
end
end

query
end

def _multiline
begin
require 'reline' unless defined?(::Reline)
rescue ::LoadError => e
elog('Failed to load Reline', e)
return { status: :fail, errors: [e] }
end

stop_words = %w[stop s exit e end quit q].freeze
help_words = %w[help h].freeze

Expand Down Expand Up @@ -152,28 +138,6 @@ def _multiline
{ status: :success, result: raw_query }
end

def _fallback
stop_words = %w[stop s exit e end quit q].freeze
line_buffer = []
while (line = ::Readline.readline(prompt = line_buffer.empty? ? 'SQL >> ' : 'SQL *> ', add_history = true))
return { status: :exit, result: nil } unless self.interacting

if stop_words.include? line.chomp.downcase
self.interacting = false
print_status 'Exiting Interactive mode.'
return { status: :exit, result: nil }
end

next if line.empty?

line_buffer.append line

break if line.end_with? ';'
end

{ status: :success, result: line_buffer.join(' ') }
end

attr_accessor :on_log_proc, :client_dispatcher

private
Expand Down
10 changes: 5 additions & 5 deletions lib/rex/ui/text/dispatcher_shell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,11 @@ def docs_dir
# and continues completions with filenames contained within.
#
def tab_complete_filenames(str, words)
matches = ::Readline::FILENAME_COMPLETION_PROC.call(str)
matches = ::Reline.completion_proc.call(str)
if matches and matches.length == 1 and File.directory?(matches[0])
dir = matches[0]
dir += File::SEPARATOR if dir[-1,1] != File::SEPARATOR
matches = ::Readline::FILENAME_COMPLETION_PROC.call(dir)
matches = ::Reline.completion_proc.call(dir)
end
matches.nil? ? [] : matches
end
Expand All @@ -311,7 +311,7 @@ def tab_complete_directory(str, words)
# This is annoying if we're recursively tab-traversing our way through subdirectories -
# we may want to continue traversing, but MSF will add a space, requiring us to back up to continue
# tab-completing our way through successive subdirectories.
::Readline.completion_append_character = nil
::Reline.completion_append_character = nil
end

if dirs.length == 0 && File.directory?(str)
Expand Down Expand Up @@ -409,8 +409,8 @@ def initialize(prompt, prompt_char = '>', histfile = nil, framework = nil, name
# Readline.basic_word_break_characters variable being set to \x00
#
def tab_complete(str)
::Readline.completion_append_character = ' '
::Readline.completion_case_fold = false
::Reline.completion_append_character = ' '
::Reline.completion_case_fold = false

# Check trailing whitespace so we can tell 'x' from 'x '
str_match = str.match(/[^\\]([\\]{2})*\s+$/)
Expand Down
75 changes: 21 additions & 54 deletions lib/rex/ui/text/input/readline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module Text

###
#
# This class implements standard input using readline against
# This class implements standard input using Reline against
# standard input. It supports tab completion.
#
###
Expand All @@ -18,37 +18,29 @@ class Input::Readline < Rex::Ui::Text::Input
# Initializes the readline-aware Input instance for text.
#
def initialize(tab_complete_proc = nil)
if(not Object.const_defined?('Readline'))
require 'readline'
end

self.extend(::Readline)
self.extend(::Reline)

if tab_complete_proc
::Readline.basic_word_break_characters = ""
::Reline.basic_word_break_characters = ""
@rl_saved_proc = with_error_handling(tab_complete_proc)
::Readline.completion_proc = @rl_saved_proc
::Reline.completion_proc = @rl_saved_proc
end
end

#
# Reattach the original completion proc
#
def reset_tab_completion(tab_complete_proc = nil)
::Readline.basic_word_break_characters = "\x00"
::Readline.completion_proc = tab_complete_proc ? with_error_handling(tab_complete_proc) : @rl_saved_proc
::Reline.basic_word_break_characters = "\x00"
::Reline.completion_proc = tab_complete_proc ? with_error_handling(tab_complete_proc) : @rl_saved_proc
end


#
# Retrieve the line buffer
#
def line_buffer
if defined? RbReadline
RbReadline.rl_line_buffer
else
::Readline.line_buffer
end
::Reline.line_buffer
end

attr_accessor :prompt
Expand Down Expand Up @@ -97,7 +89,6 @@ def pgets

output.prompting
line = readline_with_output(prompt, true)
::Readline::HISTORY.pop if (line and line.empty?)
ensure
Thread.current.priority = orig || 0
end
Expand Down Expand Up @@ -132,13 +123,8 @@ def intrinsic_shell?
private

def readline_with_output(prompt, add_history=false)
# rb-readlines's Readline.readline hardcodes the input and output to
# $stdin and $stdout, which means setting `Readline.input` or
# `Readline.ouput` has no effect when running `Readline.readline` with
# rb-readline, so need to reimplement
# []`Readline.readline`](https://github.com/luislavena/rb-readline/blob/ce4908dae45dbcae90a6e42e3710b8c3a1f2cd64/lib/readline.rb#L36-L58)
# for rb-readline to support setting input and output. Output needs to
# be set so that colorization works for the prompt on Windows.
# Output needs to be set so that colorization works for the prompt on Windows.

self.prompt = prompt

# TODO: there are unhandled quirks in async output buffering that
Expand All @@ -153,38 +139,19 @@ def readline_with_output(prompt, add_history=false)
=end
reset_sequence = ""

if defined? RbReadline
RbReadline.rl_instream = fd
RbReadline.rl_outstream = output

begin
line = RbReadline.readline(reset_sequence + prompt)
rescue ::Exception => exception
RbReadline.rl_cleanup_after_signal()
RbReadline.rl_deprep_terminal()

raise exception
end

if add_history && line && !line.start_with?(' ')
# Don't add duplicate lines to history
if ::Readline::HISTORY.empty? || line.strip != ::Readline::HISTORY[-1]
RbReadline.add_history(line.strip)
end
end

line.try(:dup)
else
# The line that's read is immediately added to history
line = ::Readline.readline(reset_sequence + prompt, true)

# Don't add duplicate lines to history
if ::Readline::HISTORY.length > 1 && line == ::Readline::HISTORY[-2]
::Readline::HISTORY.pop
end

line
# ::Reline.core.input = fd
# ::Reline.core.output = output
::Reline.input = fd
::Reline.output = output

line = ::Reline.readline(reset_sequence + prompt, true)

# Don't add duplicate lines to history
if ::Reline::HISTORY.length > 1 && line == ::Reline::HISTORY[-2]
::Reline::HISTORY.pop
end

line
end

private
Expand Down
1 change: 1 addition & 0 deletions lib/rex/ui/text/output/stdio.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def print_raw(msg = '')
msg
end
alias_method :write, :print_raw
alias_method :<< ,:write

def supports_color?
case config[:color]
Expand Down
Loading

0 comments on commit 0b48387

Please sign in to comment.