dysinger-rush 0.4

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 (47) hide show
  1. data/Rakefile +65 -0
  2. data/bin/rush +13 -0
  3. data/bin/rushd +7 -0
  4. data/lib/rush.rb +27 -0
  5. data/lib/rush/access.rb +130 -0
  6. data/lib/rush/array_ext.rb +19 -0
  7. data/lib/rush/box.rb +112 -0
  8. data/lib/rush/commands.rb +55 -0
  9. data/lib/rush/config.rb +154 -0
  10. data/lib/rush/dir.rb +158 -0
  11. data/lib/rush/embeddable_shell.rb +26 -0
  12. data/lib/rush/entry.rb +178 -0
  13. data/lib/rush/exceptions.rb +31 -0
  14. data/lib/rush/file.rb +77 -0
  15. data/lib/rush/find_by.rb +39 -0
  16. data/lib/rush/fixnum_ext.rb +18 -0
  17. data/lib/rush/head_tail.rb +11 -0
  18. data/lib/rush/local.rb +374 -0
  19. data/lib/rush/process.rb +55 -0
  20. data/lib/rush/process_set.rb +62 -0
  21. data/lib/rush/remote.rb +152 -0
  22. data/lib/rush/search_results.rb +58 -0
  23. data/lib/rush/server.rb +117 -0
  24. data/lib/rush/shell.rb +148 -0
  25. data/lib/rush/ssh_tunnel.rb +122 -0
  26. data/lib/rush/string_ext.rb +3 -0
  27. data/spec/access_spec.rb +134 -0
  28. data/spec/array_ext_spec.rb +15 -0
  29. data/spec/base.rb +24 -0
  30. data/spec/box_spec.rb +64 -0
  31. data/spec/commands_spec.rb +47 -0
  32. data/spec/config_spec.rb +108 -0
  33. data/spec/dir_spec.rb +159 -0
  34. data/spec/embeddable_shell_spec.rb +17 -0
  35. data/spec/entry_spec.rb +129 -0
  36. data/spec/file_spec.rb +79 -0
  37. data/spec/find_by_spec.rb +58 -0
  38. data/spec/fixnum_ext_spec.rb +19 -0
  39. data/spec/local_spec.rb +313 -0
  40. data/spec/process_set_spec.rb +50 -0
  41. data/spec/process_spec.rb +73 -0
  42. data/spec/remote_spec.rb +135 -0
  43. data/spec/search_results_spec.rb +44 -0
  44. data/spec/shell_spec.rb +12 -0
  45. data/spec/ssh_tunnel_spec.rb +122 -0
  46. data/spec/string_ext_spec.rb +23 -0
  47. metadata +126 -0
@@ -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
36
+ processes.each { |p| p.kill }
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,152 @@
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 file_contents(full_path)
22
+ transmit(:action => 'file_contents', :full_path => full_path)
23
+ end
24
+
25
+ def destroy(full_path)
26
+ transmit(:action => 'destroy', :full_path => full_path)
27
+ end
28
+
29
+ def purge(full_path)
30
+ transmit(:action => 'purge', :full_path => full_path)
31
+ end
32
+
33
+ def create_dir(full_path)
34
+ transmit(:action => 'create_dir', :full_path => full_path)
35
+ end
36
+
37
+ def rename(path, name, new_name)
38
+ transmit(:action => 'rename', :path => path, :name => name, :new_name => 'new_name')
39
+ end
40
+
41
+ def copy(src, dst)
42
+ transmit(:action => 'copy', :src => src, :dst => dst)
43
+ end
44
+
45
+ def read_archive(full_path)
46
+ transmit(:action => 'read_archive', :full_path => full_path)
47
+ end
48
+
49
+ def write_archive(archive, dir)
50
+ transmit(:action => 'write_archive', :dir => dir, :payload => archive)
51
+ end
52
+
53
+ def index(base_path, glob)
54
+ transmit(:action => 'index', :base_path => base_path, :glob => glob).split("\n")
55
+ end
56
+
57
+ def stat(full_path)
58
+ YAML.load(transmit(:action => 'stat', :full_path => full_path))
59
+ end
60
+
61
+ def set_access(full_path, access)
62
+ transmit access.to_hash.merge(:action => 'set_access', :full_path => full_path)
63
+ end
64
+
65
+ def size(full_path)
66
+ transmit(:action => 'size', :full_path => full_path).to_i
67
+ end
68
+
69
+ def processes
70
+ YAML.load(transmit(:action => 'processes'))
71
+ end
72
+
73
+ def process_alive(pid)
74
+ transmit(:action => 'process_alive', :pid => pid)
75
+ end
76
+
77
+ def kill_process(pid)
78
+ transmit(:action => 'kill_process', :pid => pid)
79
+ end
80
+
81
+ def bash(command, user, background)
82
+ transmit(:action => 'bash', :payload => command, :user => user, :background => background)
83
+ end
84
+
85
+ # Given a hash of parameters (converted by the method call on the connection
86
+ # object), send it across the wire to the RushServer listening on the other
87
+ # side. Uses http basic auth, with credentials fetched from the Rush::Config.
88
+ def transmit(params)
89
+ ensure_tunnel
90
+
91
+ require 'net/http'
92
+
93
+ payload = params.delete(:payload)
94
+
95
+ uri = "/?"
96
+ params.each do |key, value|
97
+ uri += "#{key}=#{value}&"
98
+ end
99
+
100
+ req = Net::HTTP::Post.new(uri)
101
+ req.basic_auth config.credentials_user, config.credentials_password
102
+
103
+ Net::HTTP.start(tunnel.host, tunnel.port) do |http|
104
+ res = http.request(req, payload)
105
+ process_result(res.code, res.body)
106
+ end
107
+ rescue EOFError
108
+ raise Rush::RushdNotRunning
109
+ end
110
+
111
+ # Take the http result of a transmit and raise an error, or return the body
112
+ # of the result when valid.
113
+ def process_result(code, body)
114
+ raise Rush::NotAuthorized if code == "401"
115
+
116
+ if code == "400"
117
+ klass, message = parse_exception(body)
118
+ raise klass, "#{host}:#{message}"
119
+ end
120
+
121
+ raise Rush::FailedTransmit if code != "200"
122
+
123
+ body
124
+ end
125
+
126
+ # Parse an exception returned from the server, with the class name on the
127
+ # first line and the message on the second.
128
+ def parse_exception(body)
129
+ klass, message = body.split("\n", 2)
130
+ raise "invalid exception class: #{klass}" unless klass.match(/^Rush::[A-Za-z]+$/)
131
+ klass = Object.module_eval(klass)
132
+ [ klass, message.strip ]
133
+ end
134
+
135
+ # Set up the tunnel if it is not already running.
136
+ def ensure_tunnel(options={})
137
+ tunnel.ensure_tunnel(options)
138
+ end
139
+
140
+ # Remote connections are alive when the box on the other end is responding
141
+ # to commands.
142
+ def alive?
143
+ index('/', 'alive_check')
144
+ true
145
+ rescue
146
+ false
147
+ end
148
+
149
+ def config
150
+ @config ||= Rush::Config.new
151
+ end
152
+ 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
23
+ without_action.delete(params[: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
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
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
data/lib/rush/shell.rb ADDED
@@ -0,0 +1,148 @@
1
+ require 'readline'
2
+
3
+ # Rush::Shell is used to create an interactive shell. It is invoked by the rush binary.
4
+ module Rush
5
+ class Shell
6
+ attr_accessor :suppress_output
7
+ # Set up the user's environment, including a pure binding into which
8
+ # env.rb and commands.rb are mixed.
9
+ def initialize
10
+ root = Rush::Dir.new('/')
11
+ home = Rush::Dir.new(ENV['HOME']) if ENV['HOME']
12
+ pwd = Rush::Dir.new(ENV['PWD']) if ENV['PWD']
13
+
14
+ @config = Rush::Config.new
15
+
16
+ @config.load_history.each do |item|
17
+ Readline::HISTORY.push(item)
18
+ end
19
+
20
+ Readline.basic_word_break_characters = ""
21
+ Readline.completion_append_character = nil
22
+ Readline.completion_proc = completion_proc
23
+
24
+ @box = Rush::Box.new
25
+ @pure_binding = @box.instance_eval "binding"
26
+ $last_res = nil
27
+
28
+ eval @config.load_env, @pure_binding
29
+
30
+ commands = @config.load_commands
31
+ Rush::Dir.class_eval commands
32
+ Array.class_eval commands
33
+ end
34
+
35
+ # Run a single command.
36
+ def execute(cmd)
37
+ res = eval(cmd, @pure_binding)
38
+ $last_res = res
39
+ eval("_ = $last_res", @pure_binding)
40
+ print_result res
41
+ rescue Rush::Exception => e
42
+ puts "Exception #{e.class} -> #{e.message}"
43
+ rescue ::Exception => e
44
+ puts "Exception #{e.class} -> #{e.message}"
45
+ e.backtrace.each do |t|
46
+ puts " #{::File.expand_path(t)}"
47
+ end
48
+ end
49
+
50
+ # Run the interactive shell using readline.
51
+ def run
52
+ loop do
53
+ cmd = Readline.readline('rush> ')
54
+
55
+ finish if cmd.nil? or cmd == 'exit'
56
+ next if cmd == ""
57
+ Readline::HISTORY.push(cmd)
58
+
59
+ execute(cmd)
60
+ end
61
+ end
62
+
63
+ # Save history to ~/.rush/history when the shell exists.
64
+ def finish
65
+ @config.save_history(Readline::HISTORY.to_a)
66
+ puts
67
+ exit
68
+ end
69
+
70
+ # Nice printing of different return types, particularly Rush::SearchResults.
71
+ def print_result(res)
72
+ return if self.suppress_output
73
+ if res.kind_of? String
74
+ puts res
75
+ elsif res.kind_of? Rush::SearchResults
76
+ widest = res.entries.map { |k| k.full_path.length }.max
77
+ res.entries_with_lines.each do |entry, lines|
78
+ print entry.full_path
79
+ print ' ' * (widest - entry.full_path.length + 2)
80
+ print "=> "
81
+ print res.colorize(lines.first.strip.head(30))
82
+ print "..." if lines.first.strip.length > 30
83
+ if lines.size > 1
84
+ print " (plus #{lines.size - 1} more matches)"
85
+ end
86
+ print "\n"
87
+ end
88
+ puts "#{res.entries.size} matching files with #{res.lines.size} matching lines"
89
+ elsif res.respond_to? :each
90
+ counts = {}
91
+ res.each do |item|
92
+ puts item
93
+ counts[item.class] ||= 0
94
+ counts[item.class] += 1
95
+ end
96
+ if counts == {}
97
+ puts "=> (empty set)"
98
+ else
99
+ count_s = counts.map do |klass, count|
100
+ "#{count} x #{klass}"
101
+ end.join(', ')
102
+ puts "=> #{count_s}"
103
+ end
104
+ else
105
+ puts "=> #{res.inspect}"
106
+ end
107
+ end
108
+
109
+ def path_parts(input) # :nodoc:
110
+ input.match(/(\w+(?:\[[^\]]+\])*)\[(['"])([^\]]+)$/)
111
+ $~.to_a.slice(1, 3).push($~.pre_match)
112
+ rescue
113
+ [ nil, nil, nil, nil ]
114
+ end
115
+
116
+ # Try to do tab completion on dir square brackets accessors.
117
+ #
118
+ # Example:
119
+ #
120
+ # dir['subd # presing tab here will produce dir['subdir/ if subdir exists
121
+ #
122
+ # This isn't that cool yet, because it can't do multiple levels of subdirs.
123
+ # It does work remotely, though, which is pretty sweet.
124
+ def completion_proc
125
+ proc do |input|
126
+ possible_var, quote, partial_path, pre = path_parts(input)
127
+ if possible_var
128
+ original_var, fixed_path = possible_var, ''
129
+ if /^(.+\/)([^\/]+)$/ === partial_path
130
+ fixed_path, partial_path = $~.captures
131
+ possible_var += "['#{fixed_path}']"
132
+ end
133
+ full_path = eval("#{possible_var}.full_path", @pure_binding) rescue nil
134
+ box = eval("#{possible_var}.box", @pure_binding) rescue nil
135
+ if full_path and box
136
+ dir = Rush::Dir.new(full_path, box)
137
+ return dir.entries.select do |e|
138
+ e.name.match(/^#{Regexp.escape(partial_path)}/)
139
+ end.map do |e|
140
+ (pre || '') + original_var + '[' + quote + fixed_path + e.name + (e.dir? ? "/" : "")
141
+ end
142
+ end
143
+ end
144
+ nil
145
+ end
146
+ end
147
+ end
148
+ end