Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/rex/ui/text/shell/history_manager.rb
Views: 11704
# -*- coding: binary -*-12require 'singleton'34module Rex5module Ui6module Text7module Shell89class HistoryManager1011MAX_HISTORY = 20001213def initialize14@contexts = []15@debug = false16# Values dequeued before work is started17@write_queue = ::Queue.new18# Values dequeued after work is completed19@remaining_work = ::Queue.new20end2122# Create a new history command context when executing the given block23#24# @param [String,nil] history_file The file to load and persist commands to25# @param [String] name Human readable history context name26# @param [Symbol] input_library The input library to provide context for. :reline, :readline27# @param [Proc] block28# @return [nil]29def with_context(history_file: nil, name: nil, input_library: nil, &block)30# Default to Readline for backwards compatibility.31push_context(history_file: history_file, name: name, input_library: input_library || :readline)3233begin34block.call35ensure36pop_context37end3839nil40end4142# Flush the contents of the write queue to disk. Blocks synchronously.43def flush44until @write_queue.empty? && @remaining_work.empty?45sleep 0.146end4748nil49end5051def inspect52"#<HistoryManager stack size: #{@contexts.length}>"53end5455def _contexts56@contexts57end5859def _debug=(value)60@debug = value61end6263def _close64event = { type: :close }65@write_queue << event66@remaining_work << event67end6869private7071def debug?72@debug73end7475# A wrapper around mapping the input library to its history; this way we can mock the return value of this method.76def map_library_to_history(input_library)77case input_library78when :readline79::Readline::HISTORY80when :reline81::Reline::HISTORY82else83$stderr.puts("Unknown input library: #{input_library}") if debug?84[]85end86end8788def clear_library(input_library)89case input_library90when :readline91clear_readline92when :reline93clear_reline94else95$stderr.puts("Unknown input library: #{input_library}") if debug?96end97end9899def push_context(history_file: nil, name: nil, input_library: nil)100$stderr.puts("Push context before\n#{JSON.pretty_generate(_contexts)}") if debug?101new_context = { history_file: history_file, name: name, input_library: input_library || :readline }102103switch_context(new_context, @contexts.last)104@contexts.push(new_context)105$stderr.puts("Push context after\n#{JSON.pretty_generate(_contexts)}") if debug?106107nil108end109110def pop_context111$stderr.puts("Pop context before\n#{JSON.pretty_generate(_contexts)}") if debug?112return if @contexts.empty?113114old_context = @contexts.pop115$stderr.puts("Pop context after\n#{JSON.pretty_generate(_contexts)}") if debug?116switch_context(@contexts.last, old_context)117118nil119end120121def readline_available?122defined?(::Readline)123end124125def reline_available?126begin127require 'reline'128defined?(::Reline)129rescue ::LoadError => _e130false131end132end133134def clear_readline135return unless readline_available?136137::Readline::HISTORY.length.times { ::Readline::HISTORY.pop }138end139140def clear_reline141return unless reline_available?142143::Reline::HISTORY.length.times { ::Reline::HISTORY.pop }144end145146def load_history_file(context)147history_file = context[:history_file]148history = map_library_to_history(context[:input_library])149150begin151File.open(history_file, 'rb') do |f|152clear_library(context[:input_library])153f.each_line(chomp: true) do |line|154if context[:input_library] == :reline && history.last&.end_with?("\\")155history.last.delete_suffix!("\\")156history.last << "\n" << line157else158history << line159end160end161end162rescue Errno::EACCES, Errno::ENOENT => e163elog "Failed to open history file: #{history_file} with error: #{e}"164end165end166167def store_history_file(context)168history_file = context[:history_file]169history = map_library_to_history(context[:input_library])170171history_diff = history.length < MAX_HISTORY ? history.length : MAX_HISTORY172173cmds = []174history_diff.times do175entry = history.pop176cmds << entry.scrub.split("\n").join("\\\n")177end178179write_history_file(history_file, cmds.reverse)180end181182def switch_context(new_context, old_context=nil)183if old_context && old_context[:history_file]184store_history_file(old_context)185end186187if new_context && new_context[:history_file]188load_history_file(new_context)189else190clear_readline191clear_reline192end193rescue SignalException => _e194clear_readline195clear_reline196end197198def write_history_file(history_file, cmds)199write_queue_ref = @write_queue200remaining_work_ref = @remaining_work201202@write_thread ||= Rex::ThreadFactory.spawn("HistoryManagerWriter", false) do203while (event = write_queue_ref.pop)204begin205break if event[:type] == :close206207history_file = event[:history_file]208cmds = event[:cmds]209210File.open(history_file, 'wb+') do |f|211f.puts(cmds)212end213214rescue => e215elog(e)216ensure217remaining_work_ref.pop218end219end220end221222event = { type: :write, history_file: history_file, cmds: cmds }223@write_queue << event224@remaining_work << event225end226end227228end229end230end231end232233234