aleksi-rush 0.6.6

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.
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