Skip to content

Instantly share code, notes, and snippets.

@candlerb
Created July 8, 2010 20:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save candlerb/468572 to your computer and use it in GitHub Desktop.
Save candlerb/468572 to your computer and use it in GitHub Desktop.
def lc
$lc = LiveConsole.new(:socket, :port => 3030, :host => '0.0.0.0')
$lc.start
end
#-------------------------------------------------------
# This module houses a pile of informative constants for LiveConsole.
module LiveConsoleConfig
Authors = 'Pete Elmore'
Email = 'pete.elmore@gmail.com'
PkgName = 'live_console'
Version = '0.2.1'
URL = 'http://debu.gs/live-console'
Project = 'live-console'
end
#-------------------------------------------------------
# LiveConsole
# Pete Elmore (pete.elmore@gmail.com), 2007-10-18
# debu.gs/live-console
# See doc/LICENSE.
require 'irb'
require 'irb/frame'
require 'socket'
#require 'live_console_config'
# LiveConsole provides a socket that can be connected to via netcat or telnet
# to use to connect to an IRB session inside a running process. It creates a
# thread that listens on the specified address/port or Unix Domain Socket path,
# and presents connecting clients with an IRB shell. Using this, you can
# execute code on a running instance of a Ruby process to inspect the state or
# even patch code on the fly. There is currently no readline support.
class LiveConsole
include Socket::Constants
#autoload :IOMethods, 'live_console/io_methods'
attr_accessor :io_method, :io, :thread, :bind
private :io_method=, :io=, :thread=
# call-seq:
# # Bind a LiveConsole to localhost:3030 (only allow clients on this
# # machine to connect):
# LiveConsole.new :socket, :port => 3030
# # Accept connections from anywhere on port 3030. Ridiculously insecure:
# LiveConsole.new(:socket, :port => 3030, :host => '0.0.0.0')
# # Use a Unix Domain Socket (which is more secure) instead:
# LiveConsole.new(:unix_socket, :path => '/tmp/my_liveconsole.sock',
# :mode => 0600, :uid => Process.uid, :gid => Process.gid)
# # By default, the mode is 0600, and the uid and gid are those of the
# # current process. These three options are for the file's permissions.
# # You can also supply a binding for IRB's toplevel:
# LiveConsole.new(:unix_socket,
# :path => "/tmp/live_console_#{Process.pid}.sock", :bind => binding)
#
# Creates a new LiveConsole. You must next call LiveConsole#start when you
# want to spawn the thread to accept connections and start the console.
def initialize(io_method, opts = {})
self.io_method = io_method.to_sym
self.bind = opts.delete :bind
unless IOMethods::List.include?(self.io_method)
raise ArgumentError, "Unknown IO method: #{io_method}"
end
init_io opts
end
# LiveConsole#start spawns a thread to listen for, accept, and provide an
# IRB console to new connections. If a thread is already running, this
# method simply returns false; otherwise, it returns the new thread.
def start
if thread
if thread.alive?
return false
else
thread.join
self.thread = nil
end
end
self.thread = Thread.new {
loop {
Thread.pass
if io.start
irb_io = GenericIOMethod.new io.raw_input, io.raw_output
begin
IRB.start_with_io(irb_io, bind)
rescue Errno::EPIPE => e
io.stop
end
end
}
}
thread
end
# Ends the running thread, if it exists. Returns true if a thread was
# running, false otherwise.
def stop
if thread
if thread == Thread.current
self.thread = nil
Thread.current.exit!
end
thread.exit
if thread.join(0.1).nil?
thread.exit!
end
self.thread = nil
true
else
false
end
end
# Restarts. Useful for binding changes. Return value is the same as for
# LiveConsole#start.
def restart
r = lambda { stop; start }
if thread == Thread.current
Thread.new &r # Leaks a thread, but works.
else
r.call
end
end
private
def init_irb
return if @@irb_inited_already
IRB.setup nil
@@irb_inited_already = true
end
def init_io opts
self.io = IOMethods.send(io_method).new opts
end
end
# We need to make a couple of changes to the IRB module to account for using a
# weird I/O method and re-starting IRB from time to time.
module IRB
@inited = false
ARGV = []
# Overridden a la FXIrb to accomodate our needs.
def IRB.start_with_io(io, bind, &block)
unless @inited
setup '/dev/null'
IRB.parse_opts
IRB.load_modules
@inited = true
end
bind ||= IRB::Frame.top(1)
ws = IRB::WorkSpace.new(bind)
irb = Irb.new(ws, io, io)
@CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
@CONF[:MAIN_CONTEXT] = irb.context
@CONF[:PROMPT_MODE] = :INF_RUBY
catch(:IRB_EXIT) {
begin
irb.eval_input
rescue StandardError => e
irb.print([e.to_s, e.backtrace].flatten.join("\n") + "\n")
retry
end
}
irb.print "\n"
end
class Context
# Fix an IRB bug; it ignores your output method.
def output *args
@output_method.print *args
end
end
class Irb
# Fix an IRB bug; it ignores your output method.
def printf(*args)
context.output(sprintf(*args))
end
# Fix an IRB bug; it ignores your output method.
def print(*args)
context.output *args
end
end
end
# The GenericIOMethod is a class that wraps I/O for IRB.
class GenericIOMethod < IRB::StdioInputMethod
# call-seq:
# GenericIOMethod.new io
# GenericIOMethod.new input, output
#
# Creates a GenericIOMethod, using either a single object for both input
# and output, or one object for input and another for output.
def initialize(input, output = nil)
@input, @output = input, output
@line = []
@line_no = 0
end
attr_reader :input
def output
@output || input
end
def gets
output.print @prompt
output.flush
@line[@line_no += 1] = input.gets
# @io.flush # Not sure this is needed.
@line[@line_no]
end
# Returns the user input history.
def lines
@line.dup
end
def print(*a)
output.print *a
end
def file_name
input.inspect
end
def eof?
input.eof?
end
def close
input.close
output.close if @output
end
end
#-------------------------------------------------------
module LiveConsole::IOMethods
List = [:socket]
define_method(:socket) { const_get :SocketIO }
List.freeze
extend self
module IOMethod
def initialize(opts)
self.opts = self.class::DefaultOpts.merge opts
unless missing_opts.empty?
raise ArgumentError, "Missing opts for " \
"#{self.class.name}: #{missing_opts.inspect}"
end
end
def missing_opts
self.class::RequiredOpts - opts.keys
end
def self.included(other)
other.instance_eval {
readers = [:opts, :raw_input, :raw_output]
attr_accessor *readers
private *readers.map { |r| (r.to_s + '=').to_sym }
other::RequiredOpts.each { |opt|
define_method(opt) { opts[opt] }
}
}
end
def select
IO.select [server], [], [], 1 if server
end
private
attr_accessor :server
end
end
#-------------------------------------------------------
class LiveConsole::IOMethods::SocketIO
DefaultOpts = {
:host => '127.0.0.1',
}.freeze
RequiredOpts = DefaultOpts.keys + [:port]
include LiveConsole::IOMethods::IOMethod
def start
@server ||= TCPServer.new host, port
begin
self.raw_input = self.raw_output = server.accept_nonblock
return true
rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO,
Errno::EINTR => e
select
retry
end
end
def stop
select
raw_input.close rescue nil
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment