rush 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Rakefile +86 -0
- data/bin/rush +6 -0
- data/bin/rushd +6 -0
- data/lib/rush.rb +20 -0
- data/lib/rush/array_ext.rb +17 -0
- data/lib/rush/box.rb +63 -0
- data/lib/rush/commands.rb +55 -0
- data/lib/rush/config.rb +154 -0
- data/lib/rush/dir.rb +148 -0
- data/lib/rush/entry.rb +141 -0
- data/lib/rush/file.rb +73 -0
- data/lib/rush/fixnum_ext.rb +18 -0
- data/lib/rush/head_tail.rb +11 -0
- data/lib/rush/local.rb +224 -0
- data/lib/rush/process.rb +39 -0
- data/lib/rush/remote.rb +105 -0
- data/lib/rush/search_results.rb +58 -0
- data/lib/rush/server.rb +81 -0
- data/lib/rush/shell.rb +123 -0
- data/lib/rush/ssh_tunnel.rb +113 -0
- data/lib/rush/string_ext.rb +3 -0
- data/spec/array_ext_spec.rb +15 -0
- data/spec/base.rb +24 -0
- data/spec/box_spec.rb +18 -0
- data/spec/commands_spec.rb +47 -0
- data/spec/config_spec.rb +108 -0
- data/spec/dir_spec.rb +148 -0
- data/spec/entry_spec.rb +118 -0
- data/spec/file_spec.rb +75 -0
- data/spec/fixnum_ext_spec.rb +19 -0
- data/spec/local_spec.rb +196 -0
- data/spec/process_spec.rb +44 -0
- data/spec/remote_spec.rb +84 -0
- data/spec/search_results_spec.rb +44 -0
- data/spec/shell_spec.rb +12 -0
- data/spec/ssh_tunnel_spec.rb +106 -0
- data/spec/string_ext_spec.rb +23 -0
- metadata +91 -0
data/lib/rush/process.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# An array of these objects is returned by Rush::Box#processes.
|
2
|
+
class Rush::Process
|
3
|
+
attr_reader :box, :pid, :uid, :command, :cmdline, :mem, :cpu
|
4
|
+
|
5
|
+
# params is a hash returned by the system-specific method of looking up the
|
6
|
+
# process list.
|
7
|
+
def initialize(params, box)
|
8
|
+
@box = box
|
9
|
+
|
10
|
+
@pid = params[:pid].to_i
|
11
|
+
@uid = params[:uid].to_i
|
12
|
+
@command = params[:command]
|
13
|
+
@cmdline = params[:cmdline]
|
14
|
+
@mem = params[:rss]
|
15
|
+
@cpu = params[:time]
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s # :nodoc:
|
19
|
+
inspect
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect # :nodoc:
|
23
|
+
"Process #{@pid}: #{@cmdline}"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns true if the process is currently running.
|
27
|
+
def alive?
|
28
|
+
box.connection.process_alive(pid)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Terminate the process.
|
32
|
+
def kill
|
33
|
+
box.connection.kill_process(pid)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.all
|
37
|
+
Rush::Box.new('localhost').processes
|
38
|
+
end
|
39
|
+
end
|
data/lib/rush/remote.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# This class it the mirror of Rush::Connection::Local. A Rush::Box which is
|
4
|
+
# not localhost has a remote connection, which it can use to convert method
|
5
|
+
# calls to text suitable for transmission across the wire.
|
6
|
+
#
|
7
|
+
# This is an internal class that does not need to be accessed in normal use of
|
8
|
+
# the rush shell or library.
|
9
|
+
class Rush::Connection::Remote
|
10
|
+
attr_reader :host, :tunnel
|
11
|
+
|
12
|
+
def initialize(host)
|
13
|
+
@host = host
|
14
|
+
@tunnel = Rush::SshTunnel.new(host)
|
15
|
+
end
|
16
|
+
|
17
|
+
def write_file(full_path, contents)
|
18
|
+
transmit(:action => 'write_file', :full_path => full_path, :payload => contents)
|
19
|
+
end
|
20
|
+
|
21
|
+
def file_contents(full_path)
|
22
|
+
transmit(:action => 'file_contents', :full_path => full_path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def destroy(full_path)
|
26
|
+
transmit(:action => 'destroy', :full_path => full_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_dir(full_path)
|
30
|
+
transmit(:action => 'create_dir', :full_path => full_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
def rename(path, name, new_name)
|
34
|
+
transmit(:action => 'rename', :path => path, :name => name, :new_name => 'new_name')
|
35
|
+
end
|
36
|
+
|
37
|
+
def copy(src, dst)
|
38
|
+
transmit(:action => 'copy', :src => src, :dst => dst)
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_archive(full_path)
|
42
|
+
transmit(:action => 'read_archive', :full_path => full_path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_archive(archive, dir)
|
46
|
+
transmit(:action => 'write_archive', :dir => dir, :payload => archive)
|
47
|
+
end
|
48
|
+
|
49
|
+
def index(base_path, glob)
|
50
|
+
transmit(:action => 'index', :base_path => base_path, :glob => glob).split("\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
def stat(full_path)
|
54
|
+
YAML.load(transmit(:action => 'stat', :full_path => full_path))
|
55
|
+
end
|
56
|
+
|
57
|
+
def size(full_path)
|
58
|
+
transmit(:action => 'size', :full_path => full_path)
|
59
|
+
end
|
60
|
+
|
61
|
+
def processes
|
62
|
+
YAML.load(transmit(:action => 'processes'))
|
63
|
+
end
|
64
|
+
|
65
|
+
def process_alive(pid)
|
66
|
+
transmit(:action => 'process_alive', :pid => pid)
|
67
|
+
end
|
68
|
+
|
69
|
+
def kill_process(pid)
|
70
|
+
transmit(:action => 'kill_process', :pid => pid)
|
71
|
+
end
|
72
|
+
|
73
|
+
class NotAuthorized < Exception; end
|
74
|
+
class FailedTransmit < Exception; end
|
75
|
+
|
76
|
+
# Given a hash of parameters (converted by the method call on the connection
|
77
|
+
# object), send it across the wire to the RushServer listening on the other
|
78
|
+
# side. Uses http basic auth, with credentials fetched from the Rush::Config.
|
79
|
+
def transmit(params)
|
80
|
+
tunnel.ensure_tunnel
|
81
|
+
|
82
|
+
require 'net/http'
|
83
|
+
|
84
|
+
payload = params.delete(:payload)
|
85
|
+
|
86
|
+
uri = "/?"
|
87
|
+
params.each do |key, value|
|
88
|
+
uri += "#{key}=#{value}&"
|
89
|
+
end
|
90
|
+
|
91
|
+
req = Net::HTTP::Post.new(uri)
|
92
|
+
req.basic_auth config.credentials_user, config.credentials_password
|
93
|
+
|
94
|
+
Net::HTTP.start(tunnel.host, tunnel.port) do |http|
|
95
|
+
res = http.request(req, payload)
|
96
|
+
raise NotAuthorized if res.code == "401"
|
97
|
+
raise FailedTransmit if res.code != "200"
|
98
|
+
res.body
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def config
|
103
|
+
@config ||= Rush::Config.new
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# An instance of this class is returned by Rush::Commands#search. It contains
|
2
|
+
# both the list of entries which matched the search, as well as the raw line
|
3
|
+
# matches. These methods get equivalent functionality to "grep -l" and "grep -h".
|
4
|
+
#
|
5
|
+
# SearchResults mixes in Rush::Commands so that you can chain multiple searches
|
6
|
+
# or do file operations on the resulting entries.
|
7
|
+
#
|
8
|
+
# Examples:
|
9
|
+
#
|
10
|
+
# myproj['**/*.rb'].search(/class/).entries.size
|
11
|
+
# myproj['**/*.rb'].search(/class/).lines.size
|
12
|
+
# myproj['**/*.rb'].search(/class/).copy_to other_dir
|
13
|
+
class Rush::SearchResults
|
14
|
+
attr_reader :entries, :lines, :entries_with_lines, :pattern
|
15
|
+
|
16
|
+
# Make a blank container. Track the pattern so that we can colorize the
|
17
|
+
# output to show what was matched.
|
18
|
+
def initialize(pattern)
|
19
|
+
# Duplication of data, but this lets us return everything in the exact
|
20
|
+
# order it was received.
|
21
|
+
@pattern = pattern
|
22
|
+
@entries = []
|
23
|
+
@entries_with_lines = {}
|
24
|
+
@lines = []
|
25
|
+
end
|
26
|
+
|
27
|
+
# Add a Rush::Entry and the array of string matches.
|
28
|
+
def add(entry, lines)
|
29
|
+
# this assumes that entry is unique
|
30
|
+
@entries << entry
|
31
|
+
@entries_with_lines[entry] = lines
|
32
|
+
@lines += lines
|
33
|
+
end
|
34
|
+
|
35
|
+
include Rush::Commands
|
36
|
+
|
37
|
+
def each(&block)
|
38
|
+
@entries.each(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
include Enumerable
|
42
|
+
|
43
|
+
def colorize(line)
|
44
|
+
lowlight + line.gsub(/(#{pattern.source})/, "#{hilight}\\1#{lowlight}") + normal
|
45
|
+
end
|
46
|
+
|
47
|
+
def hilight
|
48
|
+
"\e[34;1m"
|
49
|
+
end
|
50
|
+
|
51
|
+
def lowlight
|
52
|
+
"\e[37;2m"
|
53
|
+
end
|
54
|
+
|
55
|
+
def normal
|
56
|
+
"\e[0m"
|
57
|
+
end
|
58
|
+
end
|
data/lib/rush/server.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'mongrel'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
# Mongrel handler that translates the incoming HTTP request into a
|
6
|
+
# Rush::Connection::Local call. The results are sent back across the wire to
|
7
|
+
# be decoded by Rush::Connection::Remote on the other side.
|
8
|
+
class RushHandler < Mongrel::HttpHandler
|
9
|
+
def process(request, response)
|
10
|
+
params = {}
|
11
|
+
request.params['QUERY_STRING'].split("?").last.split("&").each do |tuple|
|
12
|
+
key, value = tuple.split("=")
|
13
|
+
params[key.to_sym] = value
|
14
|
+
end
|
15
|
+
|
16
|
+
unless authorize(request.params['HTTP_AUTHORIZATION'])
|
17
|
+
response.start(401) do |head, out|
|
18
|
+
end
|
19
|
+
else
|
20
|
+
payload = request.body.read
|
21
|
+
|
22
|
+
without_action = params
|
23
|
+
without_action.delete(params[:action])
|
24
|
+
printf "%-20s", params[:action]
|
25
|
+
print without_action.inspect
|
26
|
+
print " + #{payload.size} bytes of payload" if payload.size > 0
|
27
|
+
puts
|
28
|
+
|
29
|
+
params[:payload] = payload
|
30
|
+
result = box.connection.receive(params)
|
31
|
+
|
32
|
+
response.start(200) do |head, out|
|
33
|
+
out.write result
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def authorize(auth)
|
39
|
+
unless m = auth.match(/^Basic (.+)$/)
|
40
|
+
puts "Request with no authorization data"
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
|
44
|
+
decoded = Base64.decode64(m[1])
|
45
|
+
user, password = decoded.split(':', 2)
|
46
|
+
|
47
|
+
if user.nil? or user.length == 0 or password.nil? or password.length == 0
|
48
|
+
puts "Malformed user or password"
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
if password == config.passwords[user]
|
53
|
+
return true
|
54
|
+
else
|
55
|
+
puts "Access denied to #{user}"
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def box
|
61
|
+
@box ||= Rush::Box.new('localhost')
|
62
|
+
end
|
63
|
+
|
64
|
+
def config
|
65
|
+
@config ||= Rush::Config.new
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# A container class to run the Mongrel server for rushd.
|
70
|
+
class RushServer
|
71
|
+
def run
|
72
|
+
host = "127.0.0.1"
|
73
|
+
port = Rush::Config::DefaultPort
|
74
|
+
|
75
|
+
puts "rushd listening on #{host}:#{port}"
|
76
|
+
|
77
|
+
h = Mongrel::HttpServer.new(host, port)
|
78
|
+
h.register("/", RushHandler.new)
|
79
|
+
h.run.join
|
80
|
+
end
|
81
|
+
end
|
data/lib/rush/shell.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
# Rush::Shell is used to create an interactive shell. It is invoked by the rush binary.
|
4
|
+
module Rush
|
5
|
+
class Shell
|
6
|
+
# Set up the user's environment, including a pure binding into which
|
7
|
+
# env.rb and commands.rb are mixed.
|
8
|
+
def initialize
|
9
|
+
root = Rush::Dir.new('/')
|
10
|
+
home = Rush::Dir.new(ENV['HOME'])
|
11
|
+
pwd = Rush::Dir.new(ENV['PWD'])
|
12
|
+
|
13
|
+
@pure_binding = Proc.new { }
|
14
|
+
$last_res = nil
|
15
|
+
|
16
|
+
@config = Rush::Config.new
|
17
|
+
|
18
|
+
@config.load_history.each do |item|
|
19
|
+
Readline::HISTORY.push(item)
|
20
|
+
end
|
21
|
+
|
22
|
+
Readline.basic_word_break_characters = ""
|
23
|
+
Readline.completion_append_character = nil
|
24
|
+
Readline.completion_proc = completion_proc
|
25
|
+
|
26
|
+
eval @config.load_env, @pure_binding
|
27
|
+
|
28
|
+
eval "def processes; Rush::Box.new('localhost').processes; end", @pure_binding
|
29
|
+
|
30
|
+
commands = @config.load_commands
|
31
|
+
Rush::Dir.class_eval commands
|
32
|
+
Array.class_eval commands
|
33
|
+
end
|
34
|
+
|
35
|
+
# Run the interactive shell using readline.
|
36
|
+
def run
|
37
|
+
loop do
|
38
|
+
cmd = Readline.readline('rush> ')
|
39
|
+
|
40
|
+
finish if cmd.nil?
|
41
|
+
next if cmd == ""
|
42
|
+
Readline::HISTORY.push(cmd)
|
43
|
+
|
44
|
+
begin
|
45
|
+
res = eval(cmd, @pure_binding)
|
46
|
+
$last_res = res
|
47
|
+
eval("_ = $last_res", @pure_binding)
|
48
|
+
print_result res
|
49
|
+
rescue Exception => e
|
50
|
+
puts "Exception #{e.class}: #{e}"
|
51
|
+
e.backtrace.each do |t|
|
52
|
+
puts " #{::File.expand_path(t)}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Save history to ~/.rush/history when the shell exists.
|
59
|
+
def finish
|
60
|
+
@config.save_history(Readline::HISTORY.to_a)
|
61
|
+
puts
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
|
65
|
+
# Nice printing of different return types, particularly Rush::SearchResults.
|
66
|
+
def print_result(res)
|
67
|
+
if res.kind_of? String
|
68
|
+
puts res
|
69
|
+
elsif res.kind_of? Array
|
70
|
+
res.each do |item|
|
71
|
+
puts item
|
72
|
+
end
|
73
|
+
elsif res.kind_of? Rush::SearchResults
|
74
|
+
widest = res.entries.map { |k| k.full_path.length }.max
|
75
|
+
res.entries_with_lines.each do |entry, lines|
|
76
|
+
print entry.full_path
|
77
|
+
print ' ' * (widest - entry.full_path.length + 2)
|
78
|
+
print "=> "
|
79
|
+
print res.colorize(lines.first.strip.head(30))
|
80
|
+
print "..." if lines.first.strip.length > 30
|
81
|
+
if lines.size > 1
|
82
|
+
print " (plus #{lines.size - 1} more matches)"
|
83
|
+
end
|
84
|
+
print "\n"
|
85
|
+
end
|
86
|
+
puts "#{res.entries.size} matching files with #{res.lines.size} matching lines"
|
87
|
+
else
|
88
|
+
puts "=> #{res.inspect}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def path_parts(input) # :nodoc:
|
93
|
+
input.match(/^(.+)\[(['"])([^\]]+)$/).to_a.slice(1, 3) rescue [ nil, nil, nil ]
|
94
|
+
end
|
95
|
+
|
96
|
+
# Try to do tab completion on dir square brackets accessors.
|
97
|
+
#
|
98
|
+
# Example:
|
99
|
+
#
|
100
|
+
# dir['subd # presing tab here will produce dir['subdir/ if subdir exists
|
101
|
+
#
|
102
|
+
# This isn't that cool yet, because it can't do multiple levels of subdirs.
|
103
|
+
# It does work remotely, though, which is pretty sweet.
|
104
|
+
def completion_proc
|
105
|
+
proc do |input|
|
106
|
+
possible_var, quote, partial_path = path_parts(input)
|
107
|
+
if possible_var and possible_var.match(/^[a-z0-9_]+$/)
|
108
|
+
full_path = eval("#{possible_var}.full_path", @pure_binding) rescue nil
|
109
|
+
box = eval("#{possible_var}.box", @pure_binding) rescue nil
|
110
|
+
if full_path and box
|
111
|
+
dir = Rush::Dir.new(full_path, box)
|
112
|
+
return dir.entries.select do |e|
|
113
|
+
e.name.match(/^#{partial_path}/)
|
114
|
+
end.map do |e|
|
115
|
+
possible_var + '[' + quote + e.name + (e.dir? ? "/" : "")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
nil
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# Internal class for managing an ssh tunnel, across which relatively insecure
|
2
|
+
# HTTP commands can be sent by Rush::Connection::Remote.
|
3
|
+
class Rush::SshTunnel
|
4
|
+
def initialize(real_host)
|
5
|
+
@real_host = real_host
|
6
|
+
end
|
7
|
+
|
8
|
+
def host
|
9
|
+
'localhost'
|
10
|
+
end
|
11
|
+
|
12
|
+
def port
|
13
|
+
@port
|
14
|
+
end
|
15
|
+
|
16
|
+
def ensure_tunnel
|
17
|
+
return if @port and tunnel_alive?
|
18
|
+
|
19
|
+
@port = config.tunnels[@real_host]
|
20
|
+
|
21
|
+
if !@port or !tunnel_alive?
|
22
|
+
setup_everything
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def setup_everything
|
27
|
+
display "Connecting to #{@real_host}..."
|
28
|
+
push_credentials
|
29
|
+
launch_rushd
|
30
|
+
establish_tunnel
|
31
|
+
end
|
32
|
+
|
33
|
+
def push_credentials
|
34
|
+
display "Pushing credentials"
|
35
|
+
config.ensure_credentials_exist
|
36
|
+
ssh_append_to_credentials(config.credentials_file.contents.strip)
|
37
|
+
end
|
38
|
+
|
39
|
+
def ssh_append_to_credentials(string)
|
40
|
+
# the following horror is exactly why rush is needed
|
41
|
+
passwords_file = "~/.rush/passwords"
|
42
|
+
string = "'#{string}'"
|
43
|
+
ssh "M=`grep #{string} #{passwords_file} | wc -l`; if [ $M = 0 ]; then echo #{string} >> #{passwords_file}; fi"
|
44
|
+
end
|
45
|
+
|
46
|
+
def launch_rushd
|
47
|
+
display "Launching rushd"
|
48
|
+
ssh("if [ `ps aux | grep rushd | grep -v grep | wc -l` -ge 1 ]; then exit; fi; rushd > /dev/null 2>&1 &")
|
49
|
+
end
|
50
|
+
|
51
|
+
def establish_tunnel
|
52
|
+
display "Establishing ssh tunnel"
|
53
|
+
@port = next_available_port
|
54
|
+
|
55
|
+
make_ssh_tunnel
|
56
|
+
|
57
|
+
tunnels = config.tunnels
|
58
|
+
tunnels[@real_host] = @port
|
59
|
+
config.save_tunnels tunnels
|
60
|
+
|
61
|
+
sleep 0.5
|
62
|
+
end
|
63
|
+
|
64
|
+
def tunnel_options
|
65
|
+
{
|
66
|
+
:local_port => @port,
|
67
|
+
:remote_port => Rush::Config::DefaultPort,
|
68
|
+
:ssh_host => @real_host,
|
69
|
+
:stall_command => "sleep 9000"
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def tunnel_alive?
|
74
|
+
`#{tunnel_count_command}`.to_i > 0
|
75
|
+
end
|
76
|
+
|
77
|
+
def tunnel_count_command
|
78
|
+
"ps x | grep '#{ssh_tunnel_command_without_stall}' | grep -v grep | wc -l"
|
79
|
+
end
|
80
|
+
|
81
|
+
class SshFailed < Exception; end
|
82
|
+
class NoPortSelectedYet < Exception; end
|
83
|
+
|
84
|
+
def ssh(command)
|
85
|
+
raise SshFailed unless system("ssh #{@real_host} '#{command}'")
|
86
|
+
end
|
87
|
+
|
88
|
+
def make_ssh_tunnel
|
89
|
+
raise SshFailed unless system(ssh_tunnel_command)
|
90
|
+
end
|
91
|
+
|
92
|
+
def ssh_tunnel_command_without_stall
|
93
|
+
options = tunnel_options
|
94
|
+
raise NoPortSelectedYet unless options[:local_port]
|
95
|
+
"ssh -f -L #{options[:local_port]}:127.0.0.1:#{options[:remote_port]} #{options[:ssh_host]}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def ssh_tunnel_command
|
99
|
+
ssh_tunnel_command_without_stall + " \"#{tunnel_options[:stall_command]}\""
|
100
|
+
end
|
101
|
+
|
102
|
+
def next_available_port
|
103
|
+
(config.tunnels.values.max || Rush::Config::DefaultPort) + 1
|
104
|
+
end
|
105
|
+
|
106
|
+
def config
|
107
|
+
@config ||= Rush::Config.new
|
108
|
+
end
|
109
|
+
|
110
|
+
def display(msg)
|
111
|
+
puts msg
|
112
|
+
end
|
113
|
+
end
|