rush 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|