rush 0.1 → 0.2
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 +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
|