rush 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +5 -1
- data/lib/rush.rb +4 -0
- data/lib/rush/array_ext.rb +2 -0
- data/lib/rush/box.rb +19 -0
- data/lib/rush/dir.rb +13 -3
- data/lib/rush/entry.rb +8 -3
- data/lib/rush/exceptions.rb +26 -0
- data/lib/rush/file.rb +6 -2
- data/lib/rush/find_by.rb +37 -0
- data/lib/rush/local.rb +100 -15
- data/lib/rush/process.rb +19 -4
- data/lib/rush/remote.rb +47 -6
- data/lib/rush/server.rb +48 -12
- data/lib/rush/shell.rb +23 -14
- data/lib/rush/ssh_tunnel.rb +21 -12
- data/spec/box_spec.rb +25 -0
- data/spec/dir_spec.rb +5 -0
- data/spec/entry_spec.rb +2 -2
- data/spec/file_spec.rb +4 -0
- data/spec/find_by_spec.rb +55 -0
- data/spec/local_spec.rb +80 -13
- data/spec/process_spec.rb +32 -3
- data/spec/remote_spec.rb +46 -0
- data/spec/shell_spec.rb +1 -1
- data/spec/ssh_tunnel_spec.rb +22 -6
- metadata +33 -4
data/lib/rush/remote.rb
CHANGED
@@ -26,6 +26,10 @@ class Rush::Connection::Remote
|
|
26
26
|
transmit(:action => 'destroy', :full_path => full_path)
|
27
27
|
end
|
28
28
|
|
29
|
+
def purge(full_path)
|
30
|
+
transmit(:action => 'purge', :full_path => full_path)
|
31
|
+
end
|
32
|
+
|
29
33
|
def create_dir(full_path)
|
30
34
|
transmit(:action => 'create_dir', :full_path => full_path)
|
31
35
|
end
|
@@ -70,14 +74,15 @@ class Rush::Connection::Remote
|
|
70
74
|
transmit(:action => 'kill_process', :pid => pid)
|
71
75
|
end
|
72
76
|
|
73
|
-
|
74
|
-
|
77
|
+
def bash(command)
|
78
|
+
transmit(:action => 'bash', :payload => command)
|
79
|
+
end
|
75
80
|
|
76
81
|
# Given a hash of parameters (converted by the method call on the connection
|
77
82
|
# object), send it across the wire to the RushServer listening on the other
|
78
83
|
# side. Uses http basic auth, with credentials fetched from the Rush::Config.
|
79
84
|
def transmit(params)
|
80
|
-
|
85
|
+
ensure_tunnel
|
81
86
|
|
82
87
|
require 'net/http'
|
83
88
|
|
@@ -93,10 +98,46 @@ class Rush::Connection::Remote
|
|
93
98
|
|
94
99
|
Net::HTTP.start(tunnel.host, tunnel.port) do |http|
|
95
100
|
res = http.request(req, payload)
|
96
|
-
|
97
|
-
|
98
|
-
|
101
|
+
process_result(res.code, res.body)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Take the http result of a transmit and raise an error, or return the body
|
106
|
+
# of the result when valid.
|
107
|
+
def process_result(code, body)
|
108
|
+
raise Rush::NotAuthorized if code == "401"
|
109
|
+
|
110
|
+
if code == "400"
|
111
|
+
klass, message = parse_exception(body)
|
112
|
+
raise klass, "#{host}:#{message}"
|
99
113
|
end
|
114
|
+
|
115
|
+
raise Rush::FailedTransmit if code != "200"
|
116
|
+
|
117
|
+
body
|
118
|
+
end
|
119
|
+
|
120
|
+
# Parse an exception returned from the server, with the class name on the
|
121
|
+
# first line and the message on the second.
|
122
|
+
def parse_exception(body)
|
123
|
+
klass, message = body.split("\n", 2)
|
124
|
+
raise "invalid exception class: #{klass}" unless klass.match(/^Rush::[A-Za-z]+$/)
|
125
|
+
klass = Object.module_eval(klass)
|
126
|
+
[ klass, message.strip ]
|
127
|
+
end
|
128
|
+
|
129
|
+
# Set up the tunnel if it is not already running.
|
130
|
+
def ensure_tunnel(options={})
|
131
|
+
tunnel.ensure_tunnel(options)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Remote connections are alive when the box on the other end is responding
|
135
|
+
# to commands.
|
136
|
+
def alive?
|
137
|
+
index('/', 'alive_check')
|
138
|
+
true
|
139
|
+
rescue
|
140
|
+
false
|
100
141
|
end
|
101
142
|
|
102
143
|
def config
|
data/lib/rush/server.rb
CHANGED
@@ -21,23 +21,33 @@ class RushHandler < Mongrel::HttpHandler
|
|
21
21
|
|
22
22
|
without_action = params
|
23
23
|
without_action.delete(params[:action])
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
|
25
|
+
msg = sprintf "%-20s", params[:action]
|
26
|
+
msg += without_action.inspect
|
27
|
+
msg += " + #{payload.size} bytes of payload" if payload.size > 0
|
28
|
+
log msg
|
28
29
|
|
29
30
|
params[:payload] = payload
|
30
|
-
result = box.connection.receive(params)
|
31
31
|
|
32
|
-
|
33
|
-
|
32
|
+
begin
|
33
|
+
result = box.connection.receive(params)
|
34
|
+
|
35
|
+
response.start(200) do |head, out|
|
36
|
+
out.write result
|
37
|
+
end
|
38
|
+
rescue Rush::Exception => e
|
39
|
+
response.start(400) do |head, out|
|
40
|
+
out.write "#{e.class}\n#{e.message}\n"
|
41
|
+
end
|
34
42
|
end
|
35
43
|
end
|
44
|
+
rescue Exception => e
|
45
|
+
log e.full_display
|
36
46
|
end
|
37
47
|
|
38
48
|
def authorize(auth)
|
39
49
|
unless m = auth.match(/^Basic (.+)$/)
|
40
|
-
|
50
|
+
log "Request with no authorization data"
|
41
51
|
return false
|
42
52
|
end
|
43
53
|
|
@@ -45,14 +55,14 @@ class RushHandler < Mongrel::HttpHandler
|
|
45
55
|
user, password = decoded.split(':', 2)
|
46
56
|
|
47
57
|
if user.nil? or user.length == 0 or password.nil? or password.length == 0
|
48
|
-
|
58
|
+
log "Malformed user or password"
|
49
59
|
return false
|
50
60
|
end
|
51
61
|
|
52
62
|
if password == config.passwords[user]
|
53
63
|
return true
|
54
64
|
else
|
55
|
-
|
65
|
+
log "Access denied to #{user}"
|
56
66
|
return false
|
57
67
|
end
|
58
68
|
end
|
@@ -64,6 +74,12 @@ class RushHandler < Mongrel::HttpHandler
|
|
64
74
|
def config
|
65
75
|
@config ||= Rush::Config.new
|
66
76
|
end
|
77
|
+
|
78
|
+
def log(msg)
|
79
|
+
File.open('rushd.log', 'a') do |f|
|
80
|
+
f.puts "#{Time.now.strftime('%Y-%m-%d %H:%I:%S')} :: #{msg}"
|
81
|
+
end
|
82
|
+
end
|
67
83
|
end
|
68
84
|
|
69
85
|
# A container class to run the Mongrel server for rushd.
|
@@ -72,10 +88,30 @@ class RushServer
|
|
72
88
|
host = "127.0.0.1"
|
73
89
|
port = Rush::Config::DefaultPort
|
74
90
|
|
75
|
-
|
91
|
+
rushd = RushHandler.new
|
92
|
+
rushd.log "rushd listening on #{host}:#{port}"
|
76
93
|
|
77
94
|
h = Mongrel::HttpServer.new(host, port)
|
78
|
-
h.register("/",
|
95
|
+
h.register("/", rushd)
|
79
96
|
h.run.join
|
80
97
|
end
|
81
98
|
end
|
99
|
+
|
100
|
+
class Exception
|
101
|
+
def full_display
|
102
|
+
out = []
|
103
|
+
out << "Exception #{self.class} => #{self}"
|
104
|
+
out << "Backtrace:"
|
105
|
+
out << self.filtered_backtrace.collect do |t|
|
106
|
+
" #{t}"
|
107
|
+
end
|
108
|
+
out << ""
|
109
|
+
out.join("\n")
|
110
|
+
end
|
111
|
+
|
112
|
+
def filtered_backtrace
|
113
|
+
backtrace.reject do |bt|
|
114
|
+
bt.match(/^\/usr\//)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/rush/shell.rb
CHANGED
@@ -7,11 +7,8 @@ module Rush
|
|
7
7
|
# env.rb and commands.rb are mixed.
|
8
8
|
def initialize
|
9
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
|
10
|
+
home = Rush::Dir.new(ENV['HOME']) if ENV['HOME']
|
11
|
+
pwd = Rush::Dir.new(ENV['PWD']) if ENV['PWD']
|
15
12
|
|
16
13
|
@config = Rush::Config.new
|
17
14
|
|
@@ -23,9 +20,11 @@ module Rush
|
|
23
20
|
Readline.completion_append_character = nil
|
24
21
|
Readline.completion_proc = completion_proc
|
25
22
|
|
26
|
-
|
23
|
+
@box = Rush::Box.new
|
24
|
+
@pure_binding = @box.instance_eval "binding"
|
25
|
+
$last_res = nil
|
27
26
|
|
28
|
-
eval
|
27
|
+
eval @config.load_env, @pure_binding
|
29
28
|
|
30
29
|
commands = @config.load_commands
|
31
30
|
Rush::Dir.class_eval commands
|
@@ -37,7 +36,7 @@ module Rush
|
|
37
36
|
loop do
|
38
37
|
cmd = Readline.readline('rush> ')
|
39
38
|
|
40
|
-
finish if cmd.nil?
|
39
|
+
finish if cmd.nil? or cmd == 'exit'
|
41
40
|
next if cmd == ""
|
42
41
|
Readline::HISTORY.push(cmd)
|
43
42
|
|
@@ -46,8 +45,10 @@ module Rush
|
|
46
45
|
$last_res = res
|
47
46
|
eval("_ = $last_res", @pure_binding)
|
48
47
|
print_result res
|
49
|
-
rescue Exception => e
|
50
|
-
puts "Exception #{e.class}
|
48
|
+
rescue Rush::Exception => e
|
49
|
+
puts "Exception #{e.class} -> #{e}"
|
50
|
+
rescue ::Exception => e
|
51
|
+
puts "Exception #{e.class} -> #{e}"
|
51
52
|
e.backtrace.each do |t|
|
52
53
|
puts " #{::File.expand_path(t)}"
|
53
54
|
end
|
@@ -90,7 +91,10 @@ module Rush
|
|
90
91
|
end
|
91
92
|
|
92
93
|
def path_parts(input) # :nodoc:
|
93
|
-
input.match(
|
94
|
+
input.match(/(\w+(?:\[[^\]]+\])*)\[(['"])([^\]]+)$/)
|
95
|
+
$~.to_a.slice(1, 3).push($~.pre_match)
|
96
|
+
rescue
|
97
|
+
[ nil, nil, nil, nil ]
|
94
98
|
end
|
95
99
|
|
96
100
|
# Try to do tab completion on dir square brackets accessors.
|
@@ -103,8 +107,13 @@ module Rush
|
|
103
107
|
# It does work remotely, though, which is pretty sweet.
|
104
108
|
def completion_proc
|
105
109
|
proc do |input|
|
106
|
-
possible_var, quote, partial_path = path_parts(input)
|
107
|
-
if possible_var
|
110
|
+
possible_var, quote, partial_path, pre = path_parts(input)
|
111
|
+
if possible_var
|
112
|
+
original_var, fixed_path = possible_var, ''
|
113
|
+
if /^(.+\/)([^\/]+)$/ === partial_path
|
114
|
+
fixed_path, partial_path = $~.captures
|
115
|
+
possible_var += "['#{fixed_path}']"
|
116
|
+
end
|
108
117
|
full_path = eval("#{possible_var}.full_path", @pure_binding) rescue nil
|
109
118
|
box = eval("#{possible_var}.box", @pure_binding) rescue nil
|
110
119
|
if full_path and box
|
@@ -112,7 +121,7 @@ module Rush
|
|
112
121
|
return dir.entries.select do |e|
|
113
122
|
e.name.match(/^#{partial_path}/)
|
114
123
|
end.map do |e|
|
115
|
-
|
124
|
+
(pre || '') + original_var + '[' + quote + fixed_path + e.name + (e.dir? ? "/" : "")
|
116
125
|
end
|
117
126
|
end
|
118
127
|
end
|
data/lib/rush/ssh_tunnel.rb
CHANGED
@@ -13,21 +13,21 @@ class Rush::SshTunnel
|
|
13
13
|
@port
|
14
14
|
end
|
15
15
|
|
16
|
-
def ensure_tunnel
|
16
|
+
def ensure_tunnel(options={})
|
17
17
|
return if @port and tunnel_alive?
|
18
18
|
|
19
19
|
@port = config.tunnels[@real_host]
|
20
20
|
|
21
21
|
if !@port or !tunnel_alive?
|
22
|
-
setup_everything
|
22
|
+
setup_everything(options)
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
def setup_everything
|
26
|
+
def setup_everything(options={})
|
27
27
|
display "Connecting to #{@real_host}..."
|
28
28
|
push_credentials
|
29
29
|
launch_rushd
|
30
|
-
establish_tunnel
|
30
|
+
establish_tunnel(options)
|
31
31
|
end
|
32
32
|
|
33
33
|
def push_credentials
|
@@ -40,7 +40,7 @@ class Rush::SshTunnel
|
|
40
40
|
# the following horror is exactly why rush is needed
|
41
41
|
passwords_file = "~/.rush/passwords"
|
42
42
|
string = "'#{string}'"
|
43
|
-
ssh "M=`grep #{string} #{passwords_file} | wc -l`; if [ $M = 0 ]; then echo #{string} >> #{passwords_file}; fi"
|
43
|
+
ssh "M=`grep #{string} #{passwords_file} 2>/dev/null | wc -l`; if [ $M = 0 ]; then mkdir -p .rush; chmod 700 .rush; echo #{string} >> #{passwords_file}; chmod 600 #{passwords_file}; fi"
|
44
44
|
end
|
45
45
|
|
46
46
|
def launch_rushd
|
@@ -48,11 +48,11 @@ class Rush::SshTunnel
|
|
48
48
|
ssh("if [ `ps aux | grep rushd | grep -v grep | wc -l` -ge 1 ]; then exit; fi; rushd > /dev/null 2>&1 &")
|
49
49
|
end
|
50
50
|
|
51
|
-
def establish_tunnel
|
51
|
+
def establish_tunnel(options={})
|
52
52
|
display "Establishing ssh tunnel"
|
53
53
|
@port = next_available_port
|
54
54
|
|
55
|
-
make_ssh_tunnel
|
55
|
+
make_ssh_tunnel(options)
|
56
56
|
|
57
57
|
tunnels = config.tunnels
|
58
58
|
tunnels[@real_host] = @port
|
@@ -66,7 +66,6 @@ class Rush::SshTunnel
|
|
66
66
|
:local_port => @port,
|
67
67
|
:remote_port => Rush::Config::DefaultPort,
|
68
68
|
:ssh_host => @real_host,
|
69
|
-
:stall_command => "sleep 9000"
|
70
69
|
}
|
71
70
|
end
|
72
71
|
|
@@ -85,8 +84,8 @@ class Rush::SshTunnel
|
|
85
84
|
raise SshFailed unless system("ssh #{@real_host} '#{command}'")
|
86
85
|
end
|
87
86
|
|
88
|
-
def make_ssh_tunnel
|
89
|
-
raise SshFailed unless system(ssh_tunnel_command)
|
87
|
+
def make_ssh_tunnel(options={})
|
88
|
+
raise SshFailed unless system(ssh_tunnel_command(options))
|
90
89
|
end
|
91
90
|
|
92
91
|
def ssh_tunnel_command_without_stall
|
@@ -95,8 +94,18 @@ class Rush::SshTunnel
|
|
95
94
|
"ssh -f -L #{options[:local_port]}:127.0.0.1:#{options[:remote_port]} #{options[:ssh_host]}"
|
96
95
|
end
|
97
96
|
|
98
|
-
def
|
99
|
-
|
97
|
+
def ssh_stall_command(options={})
|
98
|
+
if options[:timeout] == :infinite
|
99
|
+
"while [ 1 ]; do sleep 1000; done"
|
100
|
+
elsif options[:timeout].to_i > 10
|
101
|
+
"sleep #{options[:timeout].to_i}"
|
102
|
+
else
|
103
|
+
"sleep 9000"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def ssh_tunnel_command(options={})
|
108
|
+
ssh_tunnel_command_without_stall + ' "' + ssh_stall_command(options) + '"'
|
100
109
|
end
|
101
110
|
|
102
111
|
def next_available_port
|
data/spec/box_spec.rb
CHANGED
@@ -15,4 +15,29 @@ describe Rush::Box do
|
|
15
15
|
it "looks up entries with [] syntax" do
|
16
16
|
@box['/'].should == Rush::Dir.new('/', @box)
|
17
17
|
end
|
18
|
+
|
19
|
+
it "looks up processes" do
|
20
|
+
@box.connection.should_receive(:processes).and_return([ { :pid => 123 } ])
|
21
|
+
@box.processes.should == [ Rush::Process.new({ :pid => 123 }, @box) ]
|
22
|
+
end
|
23
|
+
|
24
|
+
it "executes bash commands" do
|
25
|
+
@box.connection.should_receive(:bash).with('cmd').and_return('output')
|
26
|
+
@box.bash('cmd').should == 'output'
|
27
|
+
end
|
28
|
+
|
29
|
+
it "checks the connection to determine if it is alive" do
|
30
|
+
@box.connection.should_receive(:alive?).and_return(true)
|
31
|
+
@box.should be_alive
|
32
|
+
end
|
33
|
+
|
34
|
+
it "establish_connection to set up the connection manually" do
|
35
|
+
@box.connection.should_receive(:ensure_tunnel)
|
36
|
+
@box.establish_connection
|
37
|
+
end
|
38
|
+
|
39
|
+
it "establish_connection can take a hash of options" do
|
40
|
+
@box.connection.should_receive(:ensure_tunnel).with(:timeout => :infinite)
|
41
|
+
@box.establish_connection(:timeout => :infinite)
|
42
|
+
end
|
18
43
|
end
|
data/spec/dir_spec.rb
CHANGED
@@ -145,4 +145,9 @@ describe Rush::Dir do
|
|
145
145
|
@dir.create_dir('a').create_file('b').write('c')
|
146
146
|
@dir.destroy
|
147
147
|
end
|
148
|
+
|
149
|
+
it "can run a bash command within itself" do
|
150
|
+
system "echo test > #{@dir.full_path}/file"
|
151
|
+
@dir.bash("cat file").should == "test\n"
|
152
|
+
end
|
148
153
|
end
|
data/spec/entry_spec.rb
CHANGED
@@ -60,11 +60,11 @@ describe Rush::Entry do
|
|
60
60
|
new_file = "test3"
|
61
61
|
system "touch #{@sandbox_dir}/#{new_file}"
|
62
62
|
|
63
|
-
lambda { @entry.rename(new_file) }.should raise_error(Rush::
|
63
|
+
lambda { @entry.rename(new_file) }.should raise_error(Rush::NameAlreadyExists, /#{new_file}/)
|
64
64
|
end
|
65
65
|
|
66
66
|
it "can't rename itself to something with a slash in it" do
|
67
|
-
lambda { @entry.rename('has/slash') }.should raise_error(Rush::
|
67
|
+
lambda { @entry.rename('has/slash') }.should raise_error(Rush::NameCannotContainSlash, /slash/)
|
68
68
|
end
|
69
69
|
|
70
70
|
it "can duplicate itself within the directory" do
|
data/spec/file_spec.rb
CHANGED
@@ -72,4 +72,8 @@ describe Rush::File do
|
|
72
72
|
it "can fetch contents or blank if doesn't exist" do
|
73
73
|
Rush::File.new('/does/not/exist').contents_or_blank.should == ""
|
74
74
|
end
|
75
|
+
|
76
|
+
it "can fetch lines, or empty if doesn't exist" do
|
77
|
+
Rush::File.new('/does/not/exist').lines_or_empty.should == []
|
78
|
+
end
|
75
79
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
describe Rush::FindBy do
|
4
|
+
before do
|
5
|
+
class Foo
|
6
|
+
attr_accessor :bar
|
7
|
+
|
8
|
+
def initialize(bar)
|
9
|
+
@bar = bar
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
@one = Foo.new('one')
|
14
|
+
@two = Foo.new('two')
|
15
|
+
@three = Foo.new('three')
|
16
|
+
|
17
|
+
@list = [ @one, @two, @three ]
|
18
|
+
end
|
19
|
+
|
20
|
+
it "compare_or_match exact match success" do
|
21
|
+
@list.compare_or_match('1', '1').should == true
|
22
|
+
end
|
23
|
+
|
24
|
+
it "compare_or_match exact match failure" do
|
25
|
+
@list.compare_or_match('1', '2').should == false
|
26
|
+
end
|
27
|
+
|
28
|
+
it "compare_or_match regexp match success" do
|
29
|
+
@list.compare_or_match('123', /2/).should == true
|
30
|
+
end
|
31
|
+
|
32
|
+
it "compare_or_match regexp match failure" do
|
33
|
+
@list.compare_or_match('123', /x/).should == false
|
34
|
+
end
|
35
|
+
|
36
|
+
it "find_by_ extact match" do
|
37
|
+
@list.find_by_bar('two').should == @two
|
38
|
+
end
|
39
|
+
|
40
|
+
it "find_by_ regexp match" do
|
41
|
+
@list.find_by_bar(/.hree/).should == @three
|
42
|
+
end
|
43
|
+
|
44
|
+
it "find_all_by_ exact match" do
|
45
|
+
@list.find_all_by_bar('one').should == [ @one ]
|
46
|
+
end
|
47
|
+
|
48
|
+
it "find_all_by_ regexp match" do
|
49
|
+
@list.find_all_by_bar(/^...$/).should == [ @one, @two ]
|
50
|
+
end
|
51
|
+
|
52
|
+
it "find_by_ with field not recognized by objects raises no errors" do
|
53
|
+
@list.find_by_nothing('x')
|
54
|
+
end
|
55
|
+
end
|