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.
@@ -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
- class NotAuthorized < Exception; end
74
- class FailedTransmit < Exception; end
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
- tunnel.ensure_tunnel
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
- raise NotAuthorized if res.code == "401"
97
- raise FailedTransmit if res.code != "200"
98
- res.body
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
@@ -21,23 +21,33 @@ class RushHandler < Mongrel::HttpHandler
21
21
 
22
22
  without_action = params
23
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
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
- response.start(200) do |head, out|
33
- out.write result
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
- puts "Request with no authorization data"
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
- puts "Malformed user or password"
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
- puts "Access denied to #{user}"
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
- puts "rushd listening on #{host}:#{port}"
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("/", RushHandler.new)
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
@@ -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
- eval @config.load_env, @pure_binding
23
+ @box = Rush::Box.new
24
+ @pure_binding = @box.instance_eval "binding"
25
+ $last_res = nil
27
26
 
28
- eval "def processes; Rush::Box.new('localhost').processes; end", @pure_binding
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}: #{e}"
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(/^(.+)\[(['"])([^\]]+)$/).to_a.slice(1, 3) rescue [ nil, nil, nil ]
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 and possible_var.match(/^[a-z0-9_]+$/)
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
- possible_var + '[' + quote + e.name + (e.dir? ? "/" : "")
124
+ (pre || '') + original_var + '[' + quote + fixed_path + e.name + (e.dir? ? "/" : "")
116
125
  end
117
126
  end
118
127
  end
@@ -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 ssh_tunnel_command
99
- ssh_tunnel_command_without_stall + " \"#{tunnel_options[:stall_command]}\""
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
@@ -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
@@ -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
@@ -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::Connection::Local::NameAlreadyExists)
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::Connection::Local::NameCannotContainSlash)
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
@@ -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