aleksi-rush 0.6.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/README.rdoc +89 -0
  2. data/Rakefile +59 -0
  3. data/VERSION +1 -0
  4. data/bin/rush +13 -0
  5. data/bin/rushd +7 -0
  6. data/lib/rush.rb +87 -0
  7. data/lib/rush/access.rb +130 -0
  8. data/lib/rush/array_ext.rb +19 -0
  9. data/lib/rush/box.rb +115 -0
  10. data/lib/rush/commands.rb +55 -0
  11. data/lib/rush/config.rb +154 -0
  12. data/lib/rush/dir.rb +160 -0
  13. data/lib/rush/embeddable_shell.rb +26 -0
  14. data/lib/rush/entry.rb +189 -0
  15. data/lib/rush/exceptions.rb +39 -0
  16. data/lib/rush/file.rb +85 -0
  17. data/lib/rush/find_by.rb +39 -0
  18. data/lib/rush/fixnum_ext.rb +18 -0
  19. data/lib/rush/head_tail.rb +11 -0
  20. data/lib/rush/local.rb +398 -0
  21. data/lib/rush/process.rb +59 -0
  22. data/lib/rush/process_set.rb +62 -0
  23. data/lib/rush/remote.rb +158 -0
  24. data/lib/rush/search_results.rb +58 -0
  25. data/lib/rush/server.rb +117 -0
  26. data/lib/rush/shell.rb +187 -0
  27. data/lib/rush/ssh_tunnel.rb +122 -0
  28. data/lib/rush/string_ext.rb +3 -0
  29. data/spec/access_spec.rb +134 -0
  30. data/spec/array_ext_spec.rb +15 -0
  31. data/spec/base.rb +24 -0
  32. data/spec/box_spec.rb +80 -0
  33. data/spec/commands_spec.rb +47 -0
  34. data/spec/config_spec.rb +108 -0
  35. data/spec/dir_spec.rb +164 -0
  36. data/spec/embeddable_shell_spec.rb +17 -0
  37. data/spec/entry_spec.rb +133 -0
  38. data/spec/file_spec.rb +83 -0
  39. data/spec/find_by_spec.rb +58 -0
  40. data/spec/fixnum_ext_spec.rb +19 -0
  41. data/spec/local_spec.rb +364 -0
  42. data/spec/process_set_spec.rb +50 -0
  43. data/spec/process_spec.rb +73 -0
  44. data/spec/remote_spec.rb +140 -0
  45. data/spec/rush_spec.rb +28 -0
  46. data/spec/search_results_spec.rb +44 -0
  47. data/spec/shell_spec.rb +23 -0
  48. data/spec/ssh_tunnel_spec.rb +122 -0
  49. data/spec/string_ext_spec.rb +23 -0
  50. metadata +142 -0
@@ -0,0 +1,59 @@
1
+ # An array of these objects is returned by Rush::Box#processes.
2
+ class Rush::Process
3
+ attr_reader :box, :pid, :uid, :parent_pid, :command, :cmdline, :mem, :cpu, :user
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
+ @user = params[:user]
13
+ @command = params[:command]
14
+ @cmdline = params[:cmdline]
15
+ @mem = params[:mem]
16
+ @cpu = params[:cpu]
17
+ @parent_pid = params[:parent_pid]
18
+ end
19
+
20
+ def to_s # :nodoc:
21
+ inspect
22
+ end
23
+
24
+ def inspect # :nodoc:
25
+ if box.to_s != 'localhost'
26
+ "#{box} #{@pid}: #{@cmdline}"
27
+ else
28
+ "#{@pid}: #{@cmdline}"
29
+ end
30
+ end
31
+
32
+ # Returns the Rush::Process parent of this process.
33
+ def parent
34
+ box.processes.select { |p| p.pid == parent_pid }.first
35
+ end
36
+
37
+ # Returns an array of child processes owned by this process.
38
+ def children
39
+ box.processes.select { |p| p.parent_pid == pid }
40
+ end
41
+
42
+ # Returns true if the process is currently running.
43
+ def alive?
44
+ box.connection.process_alive(pid)
45
+ end
46
+
47
+ # Terminate the process.
48
+ def kill(options={})
49
+ box.connection.kill_process(pid, options)
50
+ end
51
+
52
+ def ==(other) # :nodoc:
53
+ pid == other.pid and box == other.box
54
+ end
55
+
56
+ def self.all
57
+ Rush::Box.new('localhost').processes
58
+ end
59
+ end
@@ -0,0 +1,62 @@
1
+ # A container for processes that behaves like an array, and adds process-specific operations
2
+ # on the entire set, like kill.
3
+ #
4
+ # Example:
5
+ #
6
+ # processes.filter(:cmdline => /mongrel_rails/).kill
7
+ #
8
+ class Rush::ProcessSet
9
+ attr_reader :processes
10
+
11
+ def initialize(processes)
12
+ @processes = processes
13
+ end
14
+
15
+ # Filter by any field that the process responds to. Specify an exact value,
16
+ # or a regular expression. All conditions are put together as a boolean
17
+ # AND, so these two statements are equivalent:
18
+ #
19
+ # processes.filter(:uid => 501).filter(:cmdline => /ruby/)
20
+ # processes.filter(:uid => 501, :cmdline => /ruby/)
21
+ #
22
+ def filter(conditions)
23
+ Rush::ProcessSet.new(
24
+ processes.select do |p|
25
+ conditions.all? do |key, value|
26
+ value.class == Regexp ?
27
+ value.match(p.send(key)) :
28
+ p.send(key) == value
29
+ end
30
+ end
31
+ )
32
+ end
33
+
34
+ # Kill all processes in the set.
35
+ def kill(options={})
36
+ processes.each { |p| p.kill(options) }
37
+ end
38
+
39
+ # Check status of all processes in the set, returns an array of booleans.
40
+ def alive?
41
+ processes.map { |p| p.alive? }
42
+ end
43
+
44
+ include Enumerable
45
+
46
+ def each
47
+ processes.each { |p| yield p }
48
+ end
49
+
50
+ def ==(other)
51
+ if other.class == self.class
52
+ other.processes == processes
53
+ else
54
+ to_a == other
55
+ end
56
+ end
57
+
58
+ # All other messages (like size or first) are passed through to the array.
59
+ def method_missing(meth, *args)
60
+ processes.send(meth, *args)
61
+ end
62
+ end
@@ -0,0 +1,158 @@
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 append_to_file(full_path, contents)
22
+ transmit(:action => 'append_to_file', :full_path => full_path, :payload => contents)
23
+ end
24
+
25
+ def file_contents(full_path)
26
+ transmit(:action => 'file_contents', :full_path => full_path)
27
+ end
28
+
29
+ def destroy(full_path)
30
+ transmit(:action => 'destroy', :full_path => full_path)
31
+ end
32
+
33
+ def purge(full_path)
34
+ transmit(:action => 'purge', :full_path => full_path)
35
+ end
36
+
37
+ def create_dir(full_path)
38
+ transmit(:action => 'create_dir', :full_path => full_path)
39
+ end
40
+
41
+ def rename(path, name, new_name, force)
42
+ transmit(:action => 'rename', :path => path, :name => name, :new_name => new_name, :force => force)
43
+ end
44
+
45
+ def copy(src, dst, force)
46
+ transmit(:action => 'copy', :src => src, :dst => dst, :force => force)
47
+ end
48
+
49
+ def read_archive(full_path)
50
+ transmit(:action => 'read_archive', :full_path => full_path)
51
+ end
52
+
53
+ def write_archive(archive, dir, force)
54
+ transmit(:action => 'write_archive', :dir => dir, :payload => archive, :force => force)
55
+ end
56
+
57
+ def index(base_path, glob)
58
+ transmit(:action => 'index', :base_path => base_path, :glob => glob).split("\n")
59
+ end
60
+
61
+ def stat(full_path)
62
+ YAML.load(transmit(:action => 'stat', :full_path => full_path))
63
+ end
64
+
65
+ def set_access(full_path, access)
66
+ transmit access.to_hash.merge(:action => 'set_access', :full_path => full_path)
67
+ end
68
+
69
+ def size(full_path)
70
+ transmit(:action => 'size', :full_path => full_path).to_i
71
+ end
72
+
73
+ def processes
74
+ YAML.load(transmit(:action => 'processes'))
75
+ end
76
+
77
+ def process_alive(pid)
78
+ transmit(:action => 'process_alive', :pid => pid)
79
+ end
80
+
81
+ def kill_process(pid, options={})
82
+ transmit(:action => 'kill_process', :pid => pid, :payload => YAML.dump(options))
83
+ end
84
+
85
+ def bash(command, user, background, reset_environment)
86
+ transmit(:action => 'bash', :payload => command, :user => user, :background => background, :reset_environment => reset_environment)
87
+ end
88
+
89
+ # Given a hash of parameters (converted by the method call on the connection
90
+ # object), send it across the wire to the RushServer listening on the other
91
+ # side. Uses http basic auth, with credentials fetched from the Rush::Config.
92
+ def transmit(params)
93
+ ensure_tunnel
94
+
95
+ require 'net/http'
96
+
97
+ payload = params.delete(:payload)
98
+
99
+ uri = "/?"
100
+ params.each do |key, value|
101
+ uri += "#{key}=#{value}&"
102
+ end
103
+
104
+ req = Net::HTTP::Post.new(uri)
105
+ req.basic_auth config.credentials_user, config.credentials_password
106
+
107
+ Net::HTTP.start(tunnel.host, tunnel.port) do |http|
108
+ http.read_timeout = 15*60
109
+ res = http.request(req, payload)
110
+ process_result(res.code, res.body)
111
+ end
112
+ rescue EOFError
113
+ raise Rush::RushdNotRunning
114
+ end
115
+
116
+ # Take the http result of a transmit and raise an error, or return the body
117
+ # of the result when valid.
118
+ def process_result(code, body)
119
+ raise Rush::NotAuthorized if code == "401"
120
+
121
+ if code == "400"
122
+ klass, stderr, stdout = parse_exception(body)
123
+ raise Rush::BashFailed.new(stderr, stdout) if klass == Rush::BashFailed
124
+ raise klass, "#{host}:#{stderr}"
125
+ end
126
+
127
+ raise Rush::FailedTransmit if code != "200"
128
+
129
+ body.unpack('M').to_s.strip
130
+ end
131
+
132
+ # Parse an exception returned from the server, with the class name on the
133
+ # first line, stderr/message on the second and stdout (if any) on the third.
134
+ def parse_exception(body)
135
+ klass, stderr, stdout = body.split("\n", 3)
136
+ raise "invalid exception class: #{klass}" unless klass.match(/^Rush::[A-Za-z]+$/)
137
+ klass = Object.module_eval(klass)
138
+ [ klass, stderr.unpack('M').to_s.strip, stdout.unpack('M').to_s.strip ]
139
+ end
140
+
141
+ # Set up the tunnel if it is not already running.
142
+ def ensure_tunnel(options={})
143
+ tunnel.ensure_tunnel(options)
144
+ end
145
+
146
+ # Remote connections are alive when the box on the other end is responding
147
+ # to commands.
148
+ def alive?
149
+ index('/', 'alive_check')
150
+ true
151
+ rescue
152
+ false
153
+ end
154
+
155
+ def config
156
+ @config ||= Rush::Config.new
157
+ end
158
+ 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
@@ -0,0 +1,117 @@
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.dup
23
+ without_action.delete(:action)
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
29
+
30
+ params[:payload] = payload
31
+
32
+ begin
33
+ result = box.connection.receive(params)
34
+
35
+ response.start(200) do |head, out|
36
+ out.write("#{[result].pack('M')}\n")
37
+ end
38
+ rescue Rush::Exception => e
39
+ response.start(400) do |head, out|
40
+ out.write("#{e.class}\n#{[e.stderr].pack('M')}\n#{[e.stdout].pack('M')}\n")
41
+ end
42
+ end
43
+ end
44
+ rescue Exception => e
45
+ log e.full_display
46
+ end
47
+
48
+ def authorize(auth)
49
+ unless m = auth.match(/^Basic (.+)$/)
50
+ log "Request with no authorization data"
51
+ return false
52
+ end
53
+
54
+ decoded = Base64.decode64(m[1])
55
+ user, password = decoded.split(':', 2)
56
+
57
+ if user.nil? or user.length == 0 or password.nil? or password.length == 0
58
+ log "Malformed user or password"
59
+ return false
60
+ end
61
+
62
+ if password == config.passwords[user]
63
+ return true
64
+ else
65
+ log "Access denied to #{user}"
66
+ return false
67
+ end
68
+ end
69
+
70
+ def box
71
+ @box ||= Rush::Box.new('localhost')
72
+ end
73
+
74
+ def config
75
+ @config ||= Rush::Config.new
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
83
+ end
84
+
85
+ # A container class to run the Mongrel server for rushd.
86
+ class RushServer
87
+ def run
88
+ host = "127.0.0.1"
89
+ port = Rush::Config::DefaultPort
90
+
91
+ rushd = RushHandler.new
92
+ rushd.log "rushd listening on #{host}:#{port}"
93
+
94
+ h = Mongrel::HttpServer.new(host, port)
95
+ h.register("/", rushd)
96
+ h.run.join
97
+ end
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