moved some code around for interactive channels, still not functional yet, boohoo

git-svn-id: file:///home/svn/incoming/trunk@2797 4d416f70-5f16-0410-b530-b9f4589650da
unstable
Matt Miller 2005-07-19 04:21:15 +00:00
parent 203f185ad7
commit 632a97823f
21 changed files with 752 additions and 171 deletions

View File

@ -48,3 +48,5 @@ service.shutdown
- findsock handler - findsock handler
- meterpreter - meterpreter
- more ui wrapping - more ui wrapping
- fix route addition/removal in stdapi server dll (mib structure issue)
- fix interactive stream pool channels

View File

@ -36,25 +36,7 @@ protected
# overriden by derived classes if they wish to do this another way. # overriden by derived classes if they wish to do this another way.
# #
def _interact def _interact
while self.interacting interact_stream(rstream)
# Select input and rstream
sd = Rex::ThreadSafe.select([ user_input.fd, rstream.fd ])
# Cycle through the items that have data
# From the rstream? Write to user_output.
sd[0].each { |s|
if (s == rstream.fd)
data = rstream.get
user_output.print(data)
# From user_input? Write to rstream.
elsif (s == user_input.fd)
data = user_input.gets
rstream.put(data)
end
} if (sd)
end
end end
end end

View File

@ -16,7 +16,7 @@ module Interactive
# Interactive sessions by default may interact with the local user input # Interactive sessions by default may interact with the local user input
# and output. # and output.
# #
include Rex::Ui::Subscriber include Rex::Ui::Interactive
# #
# Initialize's the session # Initialize's the session
@ -55,75 +55,43 @@ module Interactive
rstream = nil rstream = nil
end end
#
# Starts interacting with the session at the most raw level, simply
# forwarding input from user_input to rstream and forwarding input from
# rstream to user_output.
#
def interact
self.interacting = true
eof = false
# Handle suspend notifications
handle_suspend
callcc { |ctx|
# As long as we're interacting...
while (self.interacting == true)
begin
_interact
# If we get an interrupt exception, ask the user if they want to
# abort the interaction. If they do, then we return out of
# the interact function and call it a day.
rescue Interrupt
if (user_want_abort? == true)
eof = true
ctx.call
end
# If we reach EOF or the connection is reset...
rescue EOFError, Errno::ECONNRESET
dlog("Session #{name} got EOF, closing.", 'core', LEV_1)
eof = true
ctx.call
end
end
}
# Restore the suspend handler
restore_suspend
# If we hit end-of-file, then that means we should finish off this
# session and call it a day.
framework.sessions.deregister(self) if (eof == true)
# Return whether or not EOF was reached
return eof
end
# #
# The remote stream handle. Must inherit from Rex::IO::Stream. # The remote stream handle. Must inherit from Rex::IO::Stream.
# #
attr_accessor :rstream attr_accessor :rstream
#
# Whether or not the session is currently being interacted with
#
attr_reader :interacting
protected protected
attr_writer :interacting
#
# The original suspend proc.
#
attr_accessor :orig_suspend
# #
# Stub method that is meant to handler interaction # Stub method that is meant to handler interaction
# #
def _interact def _interact
end end
#
# Check to see if the user wants to abort
#
def _interrupt
user_want_abort?
end
#
# Check to see if we should suspnd
#
def _suspend
# Ask the user if they would like to background the session
if (prompt_yesno("Background session #{name}?") == true)
self.interacting = false
end
end
#
# If the session reaches EOF, deregister it.
#
def _interact_complete
framework.sessions.deregister(self)
end
# #
# Checks to see if the user wants to abort # Checks to see if the user wants to abort
# #
@ -131,49 +99,6 @@ protected
prompt_yesno("Abort session #{name}?") prompt_yesno("Abort session #{name}?")
end end
#
# Installs a signal handler to monitor suspend signal notifications.
#
def handle_suspend
if (orig_suspend == nil)
self.orig_suspend = Signal.trap("TSTP") {
# Ask the user if they would like to background the session
if (prompt_yesno("Background session #{name}?") == true)
self.interacting = false
end
}
end
end
#
# Restores the previously installed signal handler for suspend
# notifications.
#
def restore_suspend
if (orig_suspend)
Signal.trap("TSTP", orig_suspend)
self.orig_suspend = nil
end
end
#
# Prompt the user for input if possible.
#
def prompt(query)
if (user_output and user_input)
user_output.print("\n" + query)
user_input.gets
end
end
#
# Check the return value of a yes/no prompt
#
def prompt_yesno(query)
(prompt(query + " [y/N] ") =~ /^y/i) ? true : false
end
end end
end end

View File

@ -96,37 +96,7 @@ class Core
# Displays the command help banner # Displays the command help banner
# #
def cmd_help(*args) def cmd_help(*args)
driver.dispatcher_stack.reverse.each { |dispatcher| print(driver.help_to_s)
begin
commands = dispatcher.commands
rescue
commands = nil
next
end
# Display the commands
tbl = Table.new(
Table::Style::Default,
'Header' => "#{dispatcher.name} Commands",
'Columns' =>
[
'Command',
'Description'
],
'ColProps' =>
{
'Command' =>
{
'MaxWidth' => 12
}
})
dispatcher.commands.sort.each { |c|
tbl << c
}
print(tbl.to_s)
}
end end
# #

View File

@ -11,9 +11,6 @@ module Rex
# #
### ###
module Exception module Exception
def to_s
"An unknown exception occurred."
end
end end
class TimeoutError < Interrupt class TimeoutError < Interrupt
@ -35,9 +32,9 @@ end
class RuntimeError < ::RuntimeError class RuntimeError < ::RuntimeError
include Exception include Exception
def to_s # def to_s
"A runtime error occurred." # "A runtime error occurred."
end # end
end end
class ArgumentError < ::ArgumentError class ArgumentError < ::ArgumentError

View File

@ -33,24 +33,30 @@ module Stream
# Writes data to the stream. # Writes data to the stream.
# #
def write(buf, opts = {}) def write(buf, opts = {})
fd.syswrite(buf)
end end
# #
# Reads data from the stream. # Reads data from the stream.
# #
def read(length = nil, opts = {}) def read(length = nil, opts = {})
length = 16384 unless length
fd.sysread(length)
end end
# #
# Shuts down the stream for reading, writing, or both. # Shuts down the stream for reading, writing, or both.
# #
def shutdown(how = SW_BOTH) def shutdown(how = SW_BOTH)
fd.shutdown(how)
end end
# #
# Closes the stream and allows for resource cleanup # Closes the stream and allows for resource cleanup
# #
def close def close
fd.close
end end
# #
@ -58,6 +64,7 @@ module Stream
# true if data is available for reading, otherwise false is returned. # true if data is available for reading, otherwise false is returned.
# #
def has_read_data?(timeout = nil) def has_read_data?(timeout = nil)
Rex::ThreadSafe.select([ fd ], nil, nil, timeout)
end end
# #

View File

@ -16,7 +16,9 @@ module IO
### ###
module StreamAbstraction module StreamAbstraction
#
# Creates a streaming socket pair # Creates a streaming socket pair
#
def initialize_abstraction def initialize_abstraction
self.lsock, self.rsock = ::Socket.pair(::Socket::AF_UNIX, self.lsock, self.rsock = ::Socket.pair(::Socket::AF_UNIX,
::Socket::SOCK_STREAM, 0) ::Socket::SOCK_STREAM, 0)

View File

@ -49,7 +49,6 @@ class Channel
# Valid channel context? # Valid channel context?
if (channel == nil) if (channel == nil)
puts "nil wtf"
return false return false
end end
@ -243,6 +242,26 @@ class Channel
return true return true
end end
#
# Enables or disables interactive mode
#
def interactive(tf = true, addends = nil)
if (self.cid == nil)
raise IOError, "Channel has been closed.", caller
end
request = Packet.create_request('core_channel_interact')
# Populate the request
request.add_tlv(TLV_TYPE_CHANNEL_ID, self.cid)
request.add_tlv(TLV_TYPE_BOOL, tf)
request.add_tlvs(addends)
self.client.send_request(request)
return true
end
## ##
# #
# Direct I/O # Direct I/O

View File

@ -24,6 +24,8 @@ module Pools
### ###
class StreamPool < Rex::Post::Meterpreter::Channels::Pool class StreamPool < Rex::Post::Meterpreter::Channels::Pool
include Rex::IO::StreamAbstraction
## ##
# #
# Constructor # Constructor
@ -33,6 +35,8 @@ class StreamPool < Rex::Post::Meterpreter::Channels::Pool
# Initializes the file channel instance # Initializes the file channel instance
def initialize(client, cid, type, flags) def initialize(client, cid, type, flags)
super(client, cid, type, flags) super(client, cid, type, flags)
initialize_abstraction
end end
# #
@ -51,6 +55,19 @@ class StreamPool < Rex::Post::Meterpreter::Channels::Pool
return false return false
end end
def dio_write_handler(packet, data)
rsock.write(data)
return true;
end
def dio_close_handler(packet)
rsock.close
return super(packet)
end
end end
end; end; end; end; end end; end; end; end; end

View File

@ -63,6 +63,8 @@ class Config
return ifaces return ifaces
end end
alias interfaces get_interfaces
## ##
# #
# Routing # Routing
@ -92,6 +94,8 @@ class Config
return routes return routes
end end
alias routes get_routes
# Adds a route to the target machine # Adds a route to the target machine
def add_route(subnet, netmask, gateway) def add_route(subnet, netmask, gateway)
request = Packet.create_request('stdapi_net_config_add_route') request = Packet.create_request('stdapi_net_config_add_route')

View File

@ -6,6 +6,24 @@ module Rex
module Post module Post
module Meterpreter module Meterpreter
###
#
# RequestError
# ------------
#
# Exception thrown when a request fails.
#
###
class RequestError < ArgumentError
def initialize(method, result)
@method = method
@result = result
end
def to_s
"#{@method}: Operation failed: #{@result}"
end
end
### ###
# #
# PacketDispatcher # PacketDispatcher
@ -44,9 +62,13 @@ module PacketDispatcher
response = send_packet_wait_response(packet, t) response = send_packet_wait_response(packet, t)
if (response == nil) if (response == nil)
raise RuntimeError, packet.method + ": No response was received.", caller raise TimeoutError
elsif (response.result != 0) elsif (response.result != 0)
raise RuntimeError, packet.method + ": Operation failed: #{response.result}", caller e = RequestError.new(packet.method, response.result)
e.set_backtrace(caller)
raise e
end end
return response return response

View File

@ -19,6 +19,7 @@ class Console
include Rex::Ui::Text::DispatcherShell include Rex::Ui::Text::DispatcherShell
# Dispatchers # Dispatchers
require 'rex/post/meterpreter/ui/console/interactive_channel'
require 'rex/post/meterpreter/ui/console/command_dispatcher' require 'rex/post/meterpreter/ui/console/command_dispatcher'
require 'rex/post/meterpreter/ui/console/command_dispatcher/core' require 'rex/post/meterpreter/ui/console/command_dispatcher/core'
@ -55,6 +56,31 @@ class Console
} }
end end
#
# Interacts with the supplied channel
#
def interact_with_channel(channel)
channel.extend(InteractiveChannel) unless (channel.kind_of?(InteractiveChannel) == true)
channel.init_ui(input, output)
channel.interact
channel.reset_ui
end
#
# Runs the specified command wrapper in something to catch meterpreter
# exceptions.
#
def run_command(dispatcher, method, arguments)
begin
super
rescue TimeoutError
output.print_line("Operation timed out.")
rescue RequestError => info
output.print_line(info.to_s)
end
end
attr_reader :client attr_reader :client
protected protected

View File

@ -60,6 +60,7 @@ class Console::CommandDispatcher::Core
# Displays the help menu # Displays the help menu
# #
def cmd_help(*args) def cmd_help(*args)
print(shell.help_to_s)
end end
# #
@ -90,11 +91,11 @@ class Console::CommandDispatcher::Core
md = m.downcase md = m.downcase
if (extensions.include?(md)) if (extensions.include?(md))
print_error("The '#{m}' extension has already been loaded.") print_error("The '#{md}' extension has already been loaded.")
next next
end end
print("Loading extension #{m}...") print("Loading extension #{md}...")
begin begin
# Use the remote side, then load the client-side # Use the remote side, then load the client-side

View File

@ -15,16 +15,27 @@ module Ui
### ###
class Console::CommandDispatcher::Stdapi class Console::CommandDispatcher::Stdapi
require 'rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs'
require 'rex/post/meterpreter/ui/console/command_dispatcher/stdapi/net'
require 'rex/post/meterpreter/ui/console/command_dispatcher/stdapi/sys'
Klass = Console::CommandDispatcher::Stdapi Klass = Console::CommandDispatcher::Stdapi
include Console::CommandDispatcher Dispatchers =
[
Klass::Fs,
Klass::Net,
Klass::Sys,
]
require 'rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs' include Console::CommandDispatcher
def initialize(shell) def initialize(shell)
super super
shell.enstack_dispatcher(Klass::Fs) Dispatchers.each { |d|
shell.enstack_dispatcher(d)
}
end end
# #

View File

@ -7,10 +7,10 @@ module Ui
### ###
# #
# Stdapi # Fs
# ------ # --
# #
# Standard API extension. # The file system portion of the standard API extension.
# #
### ###
class Console::CommandDispatcher::Stdapi::Fs class Console::CommandDispatcher::Stdapi::Fs
@ -207,7 +207,7 @@ class Console::CommandDispatcher::Stdapi::Fs
# Uploads a file or directory to the remote machine from the local # Uploads a file or directory to the remote machine from the local
# machine. # machine.
# #
def cmd_download(*args) def cmd_upload(*args)
if (args.length < 2) if (args.length < 2)
print( print(
"Usage: upload [options] src1 src2 src3 ... destination\n\n" + "Usage: upload [options] src1 src2 src3 ... destination\n\n" +

View File

@ -0,0 +1,126 @@
require 'rex/post/meterpreter'
module Rex
module Post
module Meterpreter
module Ui
###
#
# Net
# ---
#
# The networking portion of the standard API extension.
#
###
class Console::CommandDispatcher::Stdapi::Net
Klass = Console::CommandDispatcher::Stdapi::Net
include Console::CommandDispatcher
#
# Options for the generate command
#
@@route_opts = Rex::Parser::Arguments.new(
"-h" => [ false, "Help banner." ])
#
# List of supported commands
#
def commands
{
"ipconfig" => "Display interfaces",
"route" => "View and modify the routing table",
}
end
#
# Name for this dispatcher
#
def name
"Stdapi: Networking"
end
#
# Displays interfaces on the remote machine.
#
def cmd_ipconfig(*args)
ifaces = client.net.config.interfaces
if (ifaces.length == 0)
print_line("No interfaces were found.")
else
client.net.config.each_interface { |iface|
print("\n" + iface.pretty + "\n")
}
end
end
#
# Displays or modifies the routing table on the remote machine.
#
def cmd_route(*args)
# Default to list
if (args.length == 0)
args.unshift("list")
end
# Check to see if they specified -h
@@route_opts.parse(args) { |opt, idx, val|
case opt
when "-h"
print(
"Usage: route [-h] command [args]\n\n" +
"Display or modify the routing table on the remote machine.\n\n" +
"Supported commands:\n\n" +
" add [subnet] [netmask] [gateway]\n" +
" delete [subnet] [netmask] [gateway]\n" +
" list\n\n")
return true
end
}
# Process the commands
case args.shift
when "list"
routes = client.net.config.routes
if (routes.length == 0)
print_line("No routes were found.")
else
tbl = Rex::Ui::Text::Table.new(
'Header' => "Network routes",
'Indent' => 4,
'Columns' =>
[
"Subnet",
"Netmask",
"Gateway"
])
routes.each { |route|
tbl << [ route.subnet, route.netmask, route.gateway ]
}
print("\n" + tbl.to_s + "\n")
end
when "add"
print_line("Creating route #{args[0]}/#{args[1]} -> #{args[2]}")
client.net.config.add_route(*args)
when "delete"
print_line("Deleting route #{args[0]}/#{args[1]} -> #{args[2]}")
client.net.config.add_route(*args)
else
print_error("Unsupported command: #{args[0]}")
end
end
end
end
end
end
end

View File

@ -0,0 +1,128 @@
require 'rex/post/meterpreter'
module Rex
module Post
module Meterpreter
module Ui
###
#
# Sys
# ---
#
# The system level portion of the standard API extension.
#
###
class Console::CommandDispatcher::Stdapi::Sys
Klass = Console::CommandDispatcher::Stdapi::Sys
include Console::CommandDispatcher
@@execute_opts = Rex::Parser::Arguments.new(
"-a" => [ true, "The arguments to pass to the command." ],
"-c" => [ false, "Channelized I/O (required for interaction)." ],
"-f" => [ true, "The executable command to run." ],
"-h" => [ false, "Help menu." ],
"-H" => [ false, "Create the process hidden from view." ],
"-i" => [ false, "Interact with the process after creating it." ])
#
# List of supported commands
#
def commands
{
"ps" => "List running processes",
"execute" => "Execute a command",
"kill" => "Terminate a process",
"getpid" => "Get the current process identifier",
}
end
#
# Name for this dispatcher
#
def name
"Stdapi: System"
end
#
# Executes a command with some options.
#
def cmd_execute(*args)
if (args.length == 0)
args.unshift("-h")
end
interact = false
channelized = nil
hidden = nil
cmd_args = nil
cmd_exec = nil
@@execute_opts.parse(args) { |opt, idx, val|
case opt
when "-a"
cmd_args = val
when "-c"
channelized = true
when "-f"
cmd_exec = val
when "-H"
hidden = true
when "-h"
print(
"Usage: execute -f file [options]\n\n" +
"Executes a command on the remote machine.\n" +
@@execute_opts.usage)
return true
when "-i"
channelized = true
interact = true
end
}
# Did we at least get an executable?
if (cmd_exec == nil)
print_error("You must specify an executable file with -f")
return true
end
# Execute it
p = client.sys.process.execute(cmd_exec, cmd_args,
'Channelized' => channelized,
'Hidden' => hidden)
print_line("Process #{p.pid} created.")
print_line("Channel #{p.channel.cid} created.") if (p.channel)
if (interact and p.channel)
shell.interact_with_channel(p.channel)
end
end
#
# Gets the process identifier that meterpreter is running in on the remote
# machine.
#
def cmd_getpid(*args)
end
#
# Kills one or more processes.
#
def cmd_kill(*args)
end
#
# Lists running processes
#
def cmd_ps(*args)
end
end
end
end
end
end

View File

@ -0,0 +1,94 @@
module Rex
module Post
module Meterpreter
module Ui
###
#
# InteractiveChannel
# ------------------
#
# Mixin that is meant to extend the base channel class from meterpreter in a
# manner that adds interactive capabilities.
#
###
module Console::InteractiveChannel
include Rex::Ui::Interactive
#
# Interacts with self.
#
def _interact
# If the channel has a left-side socket, then we can interact with it.
if (self.lsock)
self.interactive(true)
begin
interact_stream(self)
rescue
end
self.interactive(false)
else
print_error("Channel #{self.cid} does not support interaction.")
self.interacting = false
end
end
#
# Called when an interrupt is sent
#
def _interrupt
prompt_yesno("Terminate channel #{self.cid}?")
end
#
#
#
def _suspend
# Ask the user if they would like to background the session
if (prompt_yesno("Background channel #{self.cid}?") == true)
self.interacting = false
end
end
#
# Closes the channel like it aint no thang.
#
def _interact_complete
self.close
end
#
# Reads data from local input and writes it remotely.
#
def _stream_read_local_write_remote(channel)
data = user_input.gets
self.write(data)
end
#
# Reads from the channel and writes locally.
#
def _stream_read_remote_write_local(channel)
data = channel.read
user_output.print(data)
end
#
# Returns the remote file descriptor to select on
#
def _remote_fd
self.lsock
end
end
end
end
end
end

View File

@ -17,3 +17,4 @@ require 'rex/ui/text/table'
# Ui subscriber # Ui subscriber
require 'rex/ui/subscriber' require 'rex/ui/subscriber'
require 'rex/ui/interactive'

197
lib/rex/ui/interactive.rb Normal file
View File

@ -0,0 +1,197 @@
module Rex
module Ui
###
#
# Interactive
# -----------
#
# This class implements the stubs that are needed to provide an interactive
# user interface that is backed against something arbitrary.
#
###
module Interactive
#
# Interactive sessions by default may interact with the local user input
# and output.
#
include Rex::Ui::Subscriber
#
# Starts interacting with the session at the most raw level, simply
# forwarding input from user_input to rstream and forwarding input from
# rstream to user_output.
#
def interact
self.interacting = true
eof = false
# Handle suspend notifications
handle_suspend
callcc { |ctx|
# As long as we're interacting...
while (self.interacting == true)
begin
_interact
# If we get an interrupt exception, ask the user if they want to
# abort the interaction. If they do, then we return out of
# the interact function and call it a day.
rescue Interrupt
if (_interrupt)
eof = true
ctx.call
end
# If we reach EOF or the connection is reset...
rescue EOFError, Errno::ECONNRESET
eof = true
ctx.call
end
end
}
# Restore the suspend handler
restore_suspend
# If we've hit eof, call the interact complete handler
_interact_complete if (eof == true)
# Return whether or not EOF was reached
return eof
end
#
# Whether or not the session is currently being interacted with
#
attr_reader :interacting
protected
attr_writer :interacting
#
# The original suspend proc.
#
attr_accessor :orig_suspend
#
# Stub method that is meant to handler interaction
#
def _interact
end
#
# Called when an interrupt is sent.
#
def _interrupt
true
end
#
# Called when a suspend is sent.
#
def _suspend
false
end
#
# Called when interaction has completed and one of the sides has closed.
#
def _interact_complete
true
end
#
# Read from remote and write to local.
#
def _stream_read_remote_write_local(stream)
data = stream.get
user_output.print(data)
end
#
# Read from local and write to remote.
#
def _stream_read_local_write_remote(stream)
data = user_input.gets
stream.put(data)
end
def _local_fd
user_input.fd
end
def _remote_fd(stream)
stream.fd
end
#
# Interacts with two streaming connections, reading data from one and
# writing it to the other. Both are expected to implement Rex::IO::Stream.
#
def interact_stream(stream)
while self.interacting
# Select input and rstream
sd = Rex::ThreadSafe.select([ _local_fd, _remote_fd(stream) ])
# Cycle through the items that have data
# From the stream? Write to user_output.
sd[0].each { |s|
if (s == _remote_fd(stream))
_stream_read_remote_write_local(stream)
# From user_input? Write to stream.
elsif (s == _local_fd)
_stream_read_local_write_remote(stream)
end
} if (sd)
end
end
#
# Installs a signal handler to monitor suspend signal notifications.
#
def handle_suspend
if (orig_suspend == nil)
self.orig_suspend = Signal.trap("TSTP") {
_suspend
}
end
end
#
# Restores the previously installed signal handler for suspend
# notifications.
#
def restore_suspend
if (orig_suspend)
Signal.trap("TSTP", orig_suspend)
self.orig_suspend = nil
end
end
#
# Prompt the user for input if possible.
#
def prompt(query)
if (user_output and user_input)
user_output.print("\n" + query)
user_input.gets
end
end
#
# Check the return value of a yes/no prompt
#
def prompt_yesno(query)
(prompt(query + " [y/N] ") =~ /^y/i) ? true : false
end
end
end
end

View File

@ -101,7 +101,9 @@ module DispatcherShell
} }
end end
#
# Run a single command line # Run a single command line
#
def run_single(line) def run_single(line)
arguments = parse_line(line) arguments = parse_line(line)
method = arguments.shift method = arguments.shift
@ -115,10 +117,9 @@ module DispatcherShell
dispatcher_stack.each { |dispatcher| dispatcher_stack.each { |dispatcher|
begin begin
if (dispatcher.respond_to?('cmd_' + method)) if (dispatcher.respond_to?('cmd_' + method))
run_command(dispatcher, method, arguments)
found = true found = true
eval("
dispatcher.#{'cmd_' + method}(*arguments)
")
end end
rescue rescue
output.print_error( output.print_error(
@ -139,6 +140,13 @@ module DispatcherShell
return found return found
end end
#
# Runs the supplied command on the given dispatcher.
#
def run_command(dispatcher, method, arguments)
eval("dispatcher.#{'cmd_' + method}(*arguments)")
end
# #
# If the command is unknown... # If the command is unknown...
# #
@ -146,16 +154,58 @@ module DispatcherShell
output.print_error("Unknown command: #{method}.") output.print_error("Unknown command: #{method}.")
end end
#
# Push a dispatcher to the front of the stack # Push a dispatcher to the front of the stack
#
def enstack_dispatcher(dispatcher) def enstack_dispatcher(dispatcher)
self.dispatcher_stack.unshift(dispatcher.new(self)) self.dispatcher_stack.unshift(dispatcher.new(self))
end end
#
# Pop a dispatcher from the front of the stacker # Pop a dispatcher from the front of the stacker
#
def destack_dispatcher def destack_dispatcher
self.dispatcher_stack.shift self.dispatcher_stack.shift
end end
#
# Return a readable version of a help banner for all of the enstacked
# dispatchers.
#
def help_to_s(opts = {})
str = ''
dispatcher_stack.reverse.each { |dispatcher|
# No commands? Suckage.
next if ((dispatcher.respond_to?('commands') == false) or
(dispatcher.commands.length == 0))
# Display the commands
tbl = Table.new(
'Header' => "#{dispatcher.name} Commands",
'Indent' => opts['Indent'] || 4,
'Columns' =>
[
'Command',
'Description'
],
'ColProps' =>
{
'Command' =>
{
'MaxWidth' => 12
}
})
dispatcher.commands.sort.each { |c|
tbl << c
}
str += "\n" + tbl.to_s + "\n"
}
return str
end
attr_accessor :dispatcher_stack attr_accessor :dispatcher_stack