pete-live_console 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +91 -0
- data/bin/udscat +51 -0
- data/doc/LICENSE +19 -0
- data/doc/README +127 -0
- data/doc/lc_example.rb +38 -0
- data/doc/lc_unix_example.rb +37 -0
- data/lib/live_console/io_methods/socket_io.rb +27 -0
- data/lib/live_console/io_methods/unix_socket_io.rb +31 -0
- data/lib/live_console/io_methods.rb +53 -0
- data/lib/live_console.rb +227 -0
- data/lib/live_console_config.rb +9 -0
- metadata +66 -0
data/Rakefile
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'rake/gempackagetask'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'lib/live_console_config'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
$: << "#{File.dirname(__FILE__)}/lib"
|
7
|
+
|
8
|
+
spec = Gem::Specification.new { |s|
|
9
|
+
s.name = LiveConsoleConfig::PkgName
|
10
|
+
s.version = LiveConsoleConfig::Version
|
11
|
+
s.author = LiveConsoleConfig::Authors
|
12
|
+
s.email = LiveConsoleConfig::Email
|
13
|
+
s.homepage = LiveConsoleConfig::URL
|
14
|
+
s.rubyforge_project = LiveConsoleConfig::Project
|
15
|
+
|
16
|
+
s.platform = Gem::Platform::RUBY
|
17
|
+
|
18
|
+
s.files = Dir["{lib,doc,bin,ext}/**/*"].delete_if {|f|
|
19
|
+
/\/rdoc(\/|$)/i.match f
|
20
|
+
} + %w(Rakefile)
|
21
|
+
s.require_path = 'lib'
|
22
|
+
s.has_rdoc = true
|
23
|
+
s.extra_rdoc_files = Dir['doc/*'].select(&File.method(:file?))
|
24
|
+
s.extensions << 'ext/extconf.rb' if File.exist? 'ext/extconf.rb'
|
25
|
+
Dir['bin/*'].map(&File.method(:basename)).map(&s.executables.method(:<<))
|
26
|
+
|
27
|
+
s.summary = 'A library to support adding an irb console to your ' \
|
28
|
+
'running application.'
|
29
|
+
%w().each &s.method(:add_dependency)
|
30
|
+
}
|
31
|
+
|
32
|
+
Rake::RDocTask.new(:doc) { |t|
|
33
|
+
t.main = 'doc/README'
|
34
|
+
t.rdoc_files.include 'lib/**/*.rb', 'doc/*', 'bin/*', 'ext/**/*.c',
|
35
|
+
'ext/**/*.rb'
|
36
|
+
t.options << '-S' << '-N'
|
37
|
+
t.rdoc_dir = 'doc/rdoc'
|
38
|
+
}
|
39
|
+
|
40
|
+
Rake::GemPackageTask.new(spec) { |pkg|
|
41
|
+
pkg.need_tar_bz2 = true
|
42
|
+
}
|
43
|
+
|
44
|
+
desc "Builds and installs the gem for #{spec.name}"
|
45
|
+
task(:install => :package) {
|
46
|
+
g = "pkg/#{spec.name}-#{spec.version}.gem"
|
47
|
+
system "sudo gem install -l #{g}"
|
48
|
+
}
|
49
|
+
|
50
|
+
desc "Runs IRB, automatically require()ing #{spec.name}."
|
51
|
+
task(:irb) {
|
52
|
+
exec "irb -Ilib -r#{spec.name}"
|
53
|
+
}
|
54
|
+
|
55
|
+
desc "Cleans up the pkg directory."
|
56
|
+
task(:clean) {
|
57
|
+
FileUtils.rm_rf 'pkg'
|
58
|
+
}
|
59
|
+
|
60
|
+
|
61
|
+
desc "Generates a static gemspec file; useful for github."
|
62
|
+
task(:static_gemspec) {
|
63
|
+
# This whole thing is hacky.
|
64
|
+
spec.validate
|
65
|
+
spec_attrs = %w(
|
66
|
+
platform author email files require_path has_rdoc extra_rdoc_files
|
67
|
+
extensions executables name summary homepage
|
68
|
+
).map { |attr|
|
69
|
+
"\ts.#{attr} = #{spec.send(attr).inspect}\n"
|
70
|
+
}.join <<
|
71
|
+
"\ts.version = #{spec.version.to_s.inspect}\n" <<
|
72
|
+
spec.dependencies.map { |dep|
|
73
|
+
"\ts.add_dependency #{dep.inspect}\n"
|
74
|
+
}.join
|
75
|
+
|
76
|
+
File.open("#{spec.name}.gemspec", 'w') { |f|
|
77
|
+
f.print <<-EOGEMSPEC
|
78
|
+
# This is a static gempsec automatically generated by rake. It's better to
|
79
|
+
# edit the Rakefile than this file. It is kept in the repository for the
|
80
|
+
# benefit of github.
|
81
|
+
|
82
|
+
spec = Gem::Specification.new { |s|
|
83
|
+
#{spec_attrs}}
|
84
|
+
if __FILE__ == $0
|
85
|
+
Gem::Builder.new(spec).build
|
86
|
+
else
|
87
|
+
spec
|
88
|
+
end
|
89
|
+
EOGEMSPEC
|
90
|
+
}
|
91
|
+
}
|
data/bin/udscat
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This is a client for the unix domain socket version of LiveConsole. It just
|
4
|
+
# talks to the socket. It has been included so that you don't have to track
|
5
|
+
# down a version of netcat that does this.
|
6
|
+
|
7
|
+
require 'socket'
|
8
|
+
|
9
|
+
socket_path = ARGV.first
|
10
|
+
if socket_path.nil?
|
11
|
+
$stderr.puts "You must supply a path to the socket you want to connect to."
|
12
|
+
exit 1
|
13
|
+
end
|
14
|
+
|
15
|
+
client = UNIXSocket.new socket_path
|
16
|
+
|
17
|
+
$stdin.sync = $stdout.sync = client.sync = true
|
18
|
+
|
19
|
+
read_thread =
|
20
|
+
Thread.new {
|
21
|
+
loop {
|
22
|
+
begin
|
23
|
+
Thread.pass
|
24
|
+
$stdout.write client.read_nonblock(1024)
|
25
|
+
rescue Errno::EAGAIN, Errno::EINTR => e
|
26
|
+
IO.select [client], [], [], 1
|
27
|
+
rescue Errno::EOFError, Errno::EPIPE => e
|
28
|
+
# nothing
|
29
|
+
end
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
Thread.new {
|
34
|
+
loop {
|
35
|
+
begin
|
36
|
+
l = $stdin.read_nonblock(1024)
|
37
|
+
rescue Errno::EAGAIN
|
38
|
+
retry
|
39
|
+
end
|
40
|
+
|
41
|
+
begin
|
42
|
+
client.print l if IO.select [], [client], [], 1
|
43
|
+
rescue Errno::EPIPE => e
|
44
|
+
$stderr.puts "Other end closed."
|
45
|
+
exit 0
|
46
|
+
end
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
trap('INT') { exit 0 }
|
51
|
+
loop { sleep 1 }
|
data/doc/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2007 Peter Elmore (pete.elmore at gmail.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
4
|
+
copy of this software and associated documentation files (the "Software"),
|
5
|
+
to deal in the Software without restriction, including without limitation
|
6
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
7
|
+
and/or sell copies of the Software, and to permit persons to whom the
|
8
|
+
Software is furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
18
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
19
|
+
DEALINGS IN THE SOFTWARE.
|
data/doc/README
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
= LiveConsole
|
2
|
+
|
3
|
+
== Summary
|
4
|
+
|
5
|
+
LiveConsole is a library for providing IRB over a TCP connection or a Unix
|
6
|
+
Domain Socket. If you add it to your application, you can run arbitrary code
|
7
|
+
against your application.
|
8
|
+
For example, you can:
|
9
|
+
* Inspect the state of a running application
|
10
|
+
* Change the state of the application
|
11
|
+
* Patch code on the fly, without a restart.
|
12
|
+
* Let anyone on the net 0wn you if you bind to a public interface. :)
|
13
|
+
It's useful as a diagnostic tool, a debugging tool, and a way to impress your
|
14
|
+
friends or get those Lisp guys off your back. You know the ones I mean.
|
15
|
+
|
16
|
+
== Stern Security Warning. Grrr.
|
17
|
+
|
18
|
+
Have a look at the bugs section. It should be pretty apparent that incorrect
|
19
|
+
use of this library could create a large security hole, especially before
|
20
|
+
authentication is implemented.
|
21
|
+
|
22
|
+
== Installation
|
23
|
+
|
24
|
+
You can install via rubygems,
|
25
|
+
|
26
|
+
gem install live_console
|
27
|
+
|
28
|
+
or plain old setup.rb:
|
29
|
+
|
30
|
+
ruby setup.rb install
|
31
|
+
|
32
|
+
== How to use LiveConsole
|
33
|
+
|
34
|
+
LiveConsole is very easy to use in your own app:
|
35
|
+
|
36
|
+
require 'rubygems'
|
37
|
+
require 'live_console'
|
38
|
+
|
39
|
+
# Create a LiveConsole using TCP on port 1337
|
40
|
+
lc = LiveConsole.new :socket, :port => 1337
|
41
|
+
# We're not yet accepting connections. We need to start it up:
|
42
|
+
lc.start # Starts the LiveConsole thread
|
43
|
+
# At this point, users can connect and get an IRB prompt.
|
44
|
+
lc.stop # Kills the LiveConsole thread
|
45
|
+
# Now, no one can connect.
|
46
|
+
|
47
|
+
# Create a LiveConsole using a Unix socket in /tmp/live-console.sock
|
48
|
+
lc = LiveConsole.new :unix_socket, :path => '/tmp/live-console.sock'
|
49
|
+
# As above:
|
50
|
+
lc.start
|
51
|
+
lc.stop
|
52
|
+
|
53
|
+
# Have a LiveConsole run code in a binding other than the top-level:
|
54
|
+
lc = LiveConsole.new :unix_socket, :path => '/tmp/live-console.sock'
|
55
|
+
:bind => binding
|
56
|
+
lc.start
|
57
|
+
# That will start IRB in the current binding. There is also an accessor:
|
58
|
+
lc.bind = binding
|
59
|
+
# Of course, you must restart before IRB will see the new binding:
|
60
|
+
lc.restart
|
61
|
+
|
62
|
+
Have a look at doc/lc_example.rb or doc/lc_unix_example.rb for brief examples
|
63
|
+
of how to use LiveConsole.
|
64
|
+
|
65
|
+
Try just running it:
|
66
|
+
|
67
|
+
$ ruby doc/lc_example.rb 4000 test
|
68
|
+
# Then, in a different shell:
|
69
|
+
$ netcat localhost 4000
|
70
|
+
irb(main):001:0> puts 'Wow, magic!'
|
71
|
+
|
72
|
+
$ ruby doc/lc_unix_example.rb /tmp/live-console.sock
|
73
|
+
# Then, in a different shell:
|
74
|
+
$ udscat /tmp/live-console.sock
|
75
|
+
irb(main):001:0> puts 'Words cannot describe the joy I feel.'
|
76
|
+
|
77
|
+
You can get creative about it, only starting LiveConsole when there's an
|
78
|
+
unhandled exception in your server, and then calling LiveConsole#stop when
|
79
|
+
you've diagnosed and fixed whatever the problem was.
|
80
|
+
|
81
|
+
Additionally, if you want to run LiveConsole on a server, but run netcat
|
82
|
+
locally, you can use SSH port forwarding to avoid having to open LiveConsole
|
83
|
+
to the world:
|
84
|
+
|
85
|
+
ssh -L4000:localhost:4000 you@server
|
86
|
+
|
87
|
+
Then, locally, you can do
|
88
|
+
|
89
|
+
netcat localhost 4000
|
90
|
+
|
91
|
+
and get the remote LiveConsole. man ssh for more details. Of course, this
|
92
|
+
only works for the TCP socket mode.
|
93
|
+
|
94
|
+
== Bugs
|
95
|
+
|
96
|
+
LiveConsole lacks many of the niceties of IRB on the console, like Readline
|
97
|
+
support.
|
98
|
+
|
99
|
+
Typing exit, hitting ^D, or sending signals (like INT or STOP) doesn't work.
|
100
|
+
Just exit the program you used to connect to it. This has more to do with the
|
101
|
+
program you use to connect to the socket.
|
102
|
+
|
103
|
+
For TCP connections, there is no authentication support yet, although it is
|
104
|
+
planned for the near future. This creates a security risk: anyone that can
|
105
|
+
connect to the socket can run arbitrary Ruby code as the user who owns the
|
106
|
+
process. In fact, even binding to localhost can be a security issue if you're
|
107
|
+
on a box with any untrusted users. If there's a chance you don't know what
|
108
|
+
you're doing, avoid using this library. The Unix Domain Socket version is more
|
109
|
+
secure, as you can control access via filesystem permissions.
|
110
|
+
|
111
|
+
Only one client can connect at a time. I don't think anyone needs multiple LC
|
112
|
+
connections to serve multiple instances of IRB to various clients, but if you
|
113
|
+
need it, let me know.
|
114
|
+
|
115
|
+
The README contains a slur against Lisp guys. Please stop hitting me with that PDP-10 manual. I love your language and the lambda tattoo on your chest.
|
116
|
+
|
117
|
+
Other than that, LiveConsole doesn't have any known bugs, but it is odd
|
118
|
+
software that also monkey-patches IRB, so they are likely to be there. Bug
|
119
|
+
reports and patches gratefully accepted.
|
120
|
+
|
121
|
+
== Credits
|
122
|
+
|
123
|
+
Pete Elmore -- (pete.elmore(a)gmail.com)
|
124
|
+
|
125
|
+
== Home page
|
126
|
+
|
127
|
+
http://debu.gs/live-console
|
data/doc/lc_example.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'live_console'
|
5
|
+
|
6
|
+
print <<-EOF
|
7
|
+
This is a demo program for LiveConsole. It starts a LiveConsole on the
|
8
|
+
specified port, and you can connect to it by using netcat or telnet to connect
|
9
|
+
to the specified port.
|
10
|
+
Usage:
|
11
|
+
#{$0} [port_number [value_for_$x]]
|
12
|
+
The default port is 3333, and $x is set by default to nil. Run this program,
|
13
|
+
and then in a different terminal, connect to it via netcat or telnet. You can
|
14
|
+
check that the value of $x is exactly what you set it to, and that you're
|
15
|
+
working inside this process, but there's not much to do inside the example
|
16
|
+
script. :)
|
17
|
+
|
18
|
+
EOF
|
19
|
+
|
20
|
+
port = ARGV.first.to_i
|
21
|
+
port = port.zero? ? 3333 : port
|
22
|
+
$x = ARGV[1]
|
23
|
+
|
24
|
+
lc = LiveConsole.new :socket, :port => port, :bind => binding
|
25
|
+
lc.start
|
26
|
+
|
27
|
+
puts "My PID is #{Process.pid}, " \
|
28
|
+
"I'm running on port #{port}, and $x = #{$x.inspect}"
|
29
|
+
|
30
|
+
oldx = $x
|
31
|
+
loop {
|
32
|
+
if $x != oldx
|
33
|
+
puts "The time is now #{Time.now.strftime('%R:%S')}.",
|
34
|
+
"The value of $x changed from #{oldx.inspect} to #{$x.inspect}."
|
35
|
+
oldx = $x
|
36
|
+
end
|
37
|
+
sleep 1
|
38
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'live_console'
|
5
|
+
|
6
|
+
default_path = "/tmp/lc_example_#{Process.uid}.sock"
|
7
|
+
|
8
|
+
print <<-EOF
|
9
|
+
This is a demo program for LiveConsole. It starts a LiveConsole at the
|
10
|
+
specified path, and you can connect to it by using netcat or telnet to connect
|
11
|
+
to the specified port.
|
12
|
+
Usage:
|
13
|
+
#{$0} [path_to_socket [value_for_$x]]
|
14
|
+
The default path is #{default_path}, and $x is set by default to nil.
|
15
|
+
Run this program, and then in a different terminal, connect to it via
|
16
|
+
the supplied udscat program or the BSD version of netcat.
|
17
|
+
EOF
|
18
|
+
|
19
|
+
path = ARGV.first
|
20
|
+
path = path.nil? ? default_path : path
|
21
|
+
$x = ARGV[1]
|
22
|
+
|
23
|
+
lc = LiveConsole.new :unix_socket, :path => path, :bind => binding
|
24
|
+
lc.start
|
25
|
+
|
26
|
+
puts "My PID is #{Process.pid}, " \
|
27
|
+
"I'm running on #{path}, and $x = #{$x.inspect}"
|
28
|
+
|
29
|
+
oldx = $x
|
30
|
+
loop {
|
31
|
+
if $x != oldx
|
32
|
+
puts "The time is now #{Time.now.strftime('%R:%S')}.",
|
33
|
+
"The value of $x changed from #{oldx.inspect} to #{$x.inspect}."
|
34
|
+
oldx = $x
|
35
|
+
end
|
36
|
+
sleep 1
|
37
|
+
}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class LiveConsole::IOMethods::SocketIO
|
2
|
+
DefaultOpts = {
|
3
|
+
:host => '127.0.0.1',
|
4
|
+
}.freeze
|
5
|
+
RequiredOpts = DefaultOpts.keys + [:port]
|
6
|
+
|
7
|
+
include LiveConsole::IOMethods::IOMethod
|
8
|
+
|
9
|
+
def start
|
10
|
+
@server ||= TCPServer.new host, port
|
11
|
+
|
12
|
+
begin
|
13
|
+
self.raw_input = self.raw_output = server.accept_nonblock
|
14
|
+
return true
|
15
|
+
rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO,
|
16
|
+
Errno::EINTR => e
|
17
|
+
select
|
18
|
+
retry
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop
|
23
|
+
select
|
24
|
+
raw_input.close rescue nil
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
class LiveConsole::IOMethods::UnixSocketIO
|
4
|
+
DefaultOpts = {
|
5
|
+
:mode => 0600,
|
6
|
+
:uid => Process.uid,
|
7
|
+
:gid => Process.gid,
|
8
|
+
}
|
9
|
+
RequiredOpts = DefaultOpts.keys + [:path]
|
10
|
+
|
11
|
+
include LiveConsole::IOMethods::IOMethod
|
12
|
+
|
13
|
+
def start
|
14
|
+
@server ||= UNIXServer.new path
|
15
|
+
|
16
|
+
begin
|
17
|
+
self.raw_input = self.raw_output = server.accept_nonblock
|
18
|
+
raw_input.sync = true
|
19
|
+
return true
|
20
|
+
rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO,
|
21
|
+
Errno::EINTR => e
|
22
|
+
select
|
23
|
+
retry
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def stop
|
28
|
+
select
|
29
|
+
raw_input.close
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module LiveConsole::IOMethods
|
2
|
+
List = []
|
3
|
+
|
4
|
+
Dir[File.join(File.dirname(__FILE__), 'io_methods', '*.rb')].each { |entry|
|
5
|
+
fname = entry.sub /\.rb$/, ''
|
6
|
+
classname = File.basename(entry, '.rb').capitalize.
|
7
|
+
gsub(/_(\w)/) { $1.upcase }.sub(/io$/i, 'IO').to_sym
|
8
|
+
mname = File.basename(fname).sub(/_io$/, '').to_sym
|
9
|
+
|
10
|
+
autoload classname, fname
|
11
|
+
List << mname
|
12
|
+
|
13
|
+
define_method(mname) {
|
14
|
+
const_get classname
|
15
|
+
}
|
16
|
+
}
|
17
|
+
List.freeze
|
18
|
+
|
19
|
+
extend self
|
20
|
+
|
21
|
+
module IOMethod
|
22
|
+
def initialize(opts)
|
23
|
+
self.opts = self.class::DefaultOpts.merge opts
|
24
|
+
unless missing_opts.empty?
|
25
|
+
raise ArgumentError, "Missing opts for " \
|
26
|
+
"#{self.class.name}: #{missing_opts.inspect}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def missing_opts
|
31
|
+
self.class::RequiredOpts - opts.keys
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.included(other)
|
35
|
+
other.instance_eval {
|
36
|
+
readers = [:opts, :raw_input, :raw_output]
|
37
|
+
attr_accessor *readers
|
38
|
+
private *readers.map { |r| (r.to_s + '=').to_sym }
|
39
|
+
|
40
|
+
other::RequiredOpts.each { |opt|
|
41
|
+
define_method(opt) { opts[opt] }
|
42
|
+
}
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def select
|
47
|
+
IO.select [server], [], [], 1 if server
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
attr_accessor :server
|
52
|
+
end
|
53
|
+
end
|
data/lib/live_console.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
# LiveConsole
|
2
|
+
# Pete Elmore (pete.elmore@gmail.com), 2007-10-18
|
3
|
+
# debu.gs/live-console
|
4
|
+
# See doc/LICENSE.
|
5
|
+
|
6
|
+
require 'irb'
|
7
|
+
require 'irb/frame'
|
8
|
+
require 'socket'
|
9
|
+
require 'live_console_config'
|
10
|
+
|
11
|
+
# LiveConsole provides a socket that can be connected to via netcat or telnet
|
12
|
+
# to use to connect to an IRB session inside a running process. It creates a
|
13
|
+
# thread that listens on the specified address/port or Unix Domain Socket path,
|
14
|
+
# and presents connecting clients with an IRB shell. Using this, you can
|
15
|
+
# execute code on a running instance of a Ruby process to inspect the state or
|
16
|
+
# even patch code on the fly. There is currently no readline support.
|
17
|
+
class LiveConsole
|
18
|
+
include Socket::Constants
|
19
|
+
autoload :IOMethods, 'live_console/io_methods'
|
20
|
+
|
21
|
+
attr_accessor :io_method, :io, :thread, :bind
|
22
|
+
private :io_method=, :io=, :thread=
|
23
|
+
|
24
|
+
# call-seq:
|
25
|
+
# # Bind a LiveConsole to localhost:3030 (only allow clients on this
|
26
|
+
# # machine to connect):
|
27
|
+
# LiveConsole.new :socket, :port => 3030
|
28
|
+
# # Accept connections from anywhere on port 3030. Ridiculously insecure:
|
29
|
+
# LiveConsole.new(:socket, :port => 3030, :host => '0.0.0.0')
|
30
|
+
# # Use a Unix Domain Socket (which is more secure) instead:
|
31
|
+
# LiveConsole.new(:unix_socket, :path => '/tmp/my_liveconsole.sock',
|
32
|
+
# :mode => 0600, :uid => Process.uid, :gid => Process.gid)
|
33
|
+
# # By default, the mode is 0600, and the uid and gid are those of the
|
34
|
+
# # current process. These three options are for the file's permissions.
|
35
|
+
# # You can also supply a binding for IRB's toplevel:
|
36
|
+
# LiveConsole.new(:unix_socket,
|
37
|
+
# :path => "/tmp/live_console_#{Process.pid}.sock", :bind => binding)
|
38
|
+
#
|
39
|
+
# Creates a new LiveConsole. You must next call LiveConsole#start when you
|
40
|
+
# want to spawn the thread to accept connections and start the console.
|
41
|
+
def initialize(io_method, opts = {})
|
42
|
+
self.io_method = io_method.to_sym
|
43
|
+
self.bind = opts.delete :bind
|
44
|
+
unless IOMethods::List.include?(self.io_method)
|
45
|
+
raise ArgumentError, "Unknown IO method: #{io_method}"
|
46
|
+
end
|
47
|
+
|
48
|
+
init_io opts
|
49
|
+
end
|
50
|
+
|
51
|
+
# LiveConsole#start spawns a thread to listen for, accept, and provide an
|
52
|
+
# IRB console to new connections. If a thread is already running, this
|
53
|
+
# method simply returns false; otherwise, it returns the new thread.
|
54
|
+
def start
|
55
|
+
if thread
|
56
|
+
if thread.alive?
|
57
|
+
return false
|
58
|
+
else
|
59
|
+
thread.join
|
60
|
+
self.thread = nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
self.thread = Thread.new {
|
65
|
+
loop {
|
66
|
+
Thread.pass
|
67
|
+
if io.start
|
68
|
+
irb_io = GenericIOMethod.new io.raw_input, io.raw_output
|
69
|
+
begin
|
70
|
+
IRB.start_with_io(irb_io, bind)
|
71
|
+
rescue Errno::EPIPE => e
|
72
|
+
io.stop
|
73
|
+
end
|
74
|
+
end
|
75
|
+
}
|
76
|
+
}
|
77
|
+
thread
|
78
|
+
end
|
79
|
+
|
80
|
+
# Ends the running thread, if it exists. Returns true if a thread was
|
81
|
+
# running, false otherwise.
|
82
|
+
def stop
|
83
|
+
if thread
|
84
|
+
if thread == Thread.current
|
85
|
+
self.thread = nil
|
86
|
+
Thread.current.exit!
|
87
|
+
end
|
88
|
+
|
89
|
+
thread.exit
|
90
|
+
if thread.join(0.1).nil?
|
91
|
+
thread.exit!
|
92
|
+
end
|
93
|
+
self.thread = nil
|
94
|
+
true
|
95
|
+
else
|
96
|
+
false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Restarts. Useful for binding changes. Return value is the same as for
|
101
|
+
# LiveConsole#start.
|
102
|
+
def restart
|
103
|
+
r = lambda { stop; start }
|
104
|
+
if thread == Thread.current
|
105
|
+
Thread.new &r # Leaks a thread, but works.
|
106
|
+
else
|
107
|
+
r.call
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def init_irb
|
114
|
+
return if @@irb_inited_already
|
115
|
+
IRB.setup nil
|
116
|
+
@@irb_inited_already = true
|
117
|
+
end
|
118
|
+
|
119
|
+
def init_io opts
|
120
|
+
self.io = IOMethods.send(io_method).new opts
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# We need to make a couple of changes to the IRB module to account for using a
|
125
|
+
# weird I/O method and re-starting IRB from time to time.
|
126
|
+
module IRB
|
127
|
+
@inited = false
|
128
|
+
|
129
|
+
ARGV = []
|
130
|
+
|
131
|
+
# Overridden a la FXIrb to accomodate our needs.
|
132
|
+
def IRB.start_with_io(io, bind, &block)
|
133
|
+
unless @inited
|
134
|
+
setup '/dev/null'
|
135
|
+
IRB.parse_opts
|
136
|
+
IRB.load_modules
|
137
|
+
@inited = true
|
138
|
+
end
|
139
|
+
|
140
|
+
bind ||= IRB::Frame.top(1)
|
141
|
+
ws = IRB::WorkSpace.new(bind)
|
142
|
+
irb = Irb.new(ws, io, io)
|
143
|
+
|
144
|
+
@CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC]
|
145
|
+
@CONF[:MAIN_CONTEXT] = irb.context
|
146
|
+
@CONF[:PROMPT_MODE] = :INF_RUBY
|
147
|
+
|
148
|
+
catch(:IRB_EXIT) {
|
149
|
+
begin
|
150
|
+
irb.eval_input
|
151
|
+
rescue StandardError => e
|
152
|
+
irb.print([e.to_s, e.backtrace].flatten.join("\n") + "\n")
|
153
|
+
retry
|
154
|
+
end
|
155
|
+
}
|
156
|
+
irb.print "\n"
|
157
|
+
end
|
158
|
+
|
159
|
+
class Context
|
160
|
+
# Fix an IRB bug; it ignores your output method.
|
161
|
+
def output *args
|
162
|
+
@output_method.print *args
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
class Irb
|
167
|
+
# Fix an IRB bug; it ignores your output method.
|
168
|
+
def printf(*args)
|
169
|
+
context.output(sprintf(*args))
|
170
|
+
end
|
171
|
+
|
172
|
+
# Fix an IRB bug; it ignores your output method.
|
173
|
+
def print(*args)
|
174
|
+
context.output *args
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# The GenericIOMethod is a class that wraps I/O for IRB.
|
180
|
+
class GenericIOMethod < IRB::StdioInputMethod
|
181
|
+
# call-seq:
|
182
|
+
# GenericIOMethod.new io
|
183
|
+
# GenericIOMethod.new input, output
|
184
|
+
#
|
185
|
+
# Creates a GenericIOMethod, using either a single object for both input
|
186
|
+
# and output, or one object for input and another for output.
|
187
|
+
def initialize(input, output = nil)
|
188
|
+
@input, @output = input, output
|
189
|
+
@line = []
|
190
|
+
@line_no = 0
|
191
|
+
end
|
192
|
+
|
193
|
+
attr_reader :input
|
194
|
+
def output
|
195
|
+
@output || input
|
196
|
+
end
|
197
|
+
|
198
|
+
def gets
|
199
|
+
output.print @prompt
|
200
|
+
output.flush
|
201
|
+
@line[@line_no += 1] = input.gets
|
202
|
+
# @io.flush # Not sure this is needed.
|
203
|
+
@line[@line_no]
|
204
|
+
end
|
205
|
+
|
206
|
+
# Returns the user input history.
|
207
|
+
def lines
|
208
|
+
@line.dup
|
209
|
+
end
|
210
|
+
|
211
|
+
def print(*a)
|
212
|
+
output.print *a
|
213
|
+
end
|
214
|
+
|
215
|
+
def file_name
|
216
|
+
input.inspect
|
217
|
+
end
|
218
|
+
|
219
|
+
def eof?
|
220
|
+
input.eof?
|
221
|
+
end
|
222
|
+
|
223
|
+
def close
|
224
|
+
input.close
|
225
|
+
output.close if @output
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# This module houses a pile of informative constants for LiveConsole.
|
2
|
+
module LiveConsoleConfig
|
3
|
+
Authors = 'Pete Elmore'
|
4
|
+
Email = 'pete.elmore@gmail.com'
|
5
|
+
PkgName = 'live_console'
|
6
|
+
Version = '0.2.1'
|
7
|
+
URL = 'http://debu.gs/live-console'
|
8
|
+
Project = 'live-console'
|
9
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pete-live_console
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pete Elmore
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-05-16 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: pete.elmore@gmail.com
|
18
|
+
executables:
|
19
|
+
- udscat
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- doc/LICENSE
|
24
|
+
- doc/README
|
25
|
+
- doc/lc_example.rb
|
26
|
+
- doc/lc_unix_example.rb
|
27
|
+
files:
|
28
|
+
- lib/live_console/io_methods/socket_io.rb
|
29
|
+
- lib/live_console/io_methods/unix_socket_io.rb
|
30
|
+
- lib/live_console/io_methods.rb
|
31
|
+
- lib/live_console.rb
|
32
|
+
- lib/live_console_config.rb
|
33
|
+
- doc/LICENSE
|
34
|
+
- doc/README
|
35
|
+
- doc/lc_example.rb
|
36
|
+
- doc/lc_unix_example.rb
|
37
|
+
- bin/udscat
|
38
|
+
- Rakefile
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://debu.gs/live-console
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: "0"
|
51
|
+
version:
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
version:
|
58
|
+
requirements: []
|
59
|
+
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 1.2.0
|
62
|
+
signing_key:
|
63
|
+
specification_version: 2
|
64
|
+
summary: A library to support adding an irb console to your running application.
|
65
|
+
test_files: []
|
66
|
+
|