rush 0.1 → 0.2

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