rush 0.1

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.
@@ -0,0 +1,39 @@
1
+ # An array of these objects is returned by Rush::Box#processes.
2
+ class Rush::Process
3
+ attr_reader :box, :pid, :uid, :command, :cmdline, :mem, :cpu
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
+ @command = params[:command]
13
+ @cmdline = params[:cmdline]
14
+ @mem = params[:rss]
15
+ @cpu = params[:time]
16
+ end
17
+
18
+ def to_s # :nodoc:
19
+ inspect
20
+ end
21
+
22
+ def inspect # :nodoc:
23
+ "Process #{@pid}: #{@cmdline}"
24
+ end
25
+
26
+ # Returns true if the process is currently running.
27
+ def alive?
28
+ box.connection.process_alive(pid)
29
+ end
30
+
31
+ # Terminate the process.
32
+ def kill
33
+ box.connection.kill_process(pid)
34
+ end
35
+
36
+ def self.all
37
+ Rush::Box.new('localhost').processes
38
+ end
39
+ end
@@ -0,0 +1,105 @@
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 create_dir(full_path)
30
+ transmit(:action => 'create_dir', :full_path => full_path)
31
+ end
32
+
33
+ def rename(path, name, new_name)
34
+ transmit(:action => 'rename', :path => path, :name => name, :new_name => 'new_name')
35
+ end
36
+
37
+ def copy(src, dst)
38
+ transmit(:action => 'copy', :src => src, :dst => dst)
39
+ end
40
+
41
+ def read_archive(full_path)
42
+ transmit(:action => 'read_archive', :full_path => full_path)
43
+ end
44
+
45
+ def write_archive(archive, dir)
46
+ transmit(:action => 'write_archive', :dir => dir, :payload => archive)
47
+ end
48
+
49
+ def index(base_path, glob)
50
+ transmit(:action => 'index', :base_path => base_path, :glob => glob).split("\n")
51
+ end
52
+
53
+ def stat(full_path)
54
+ YAML.load(transmit(:action => 'stat', :full_path => full_path))
55
+ end
56
+
57
+ def size(full_path)
58
+ transmit(:action => 'size', :full_path => full_path)
59
+ end
60
+
61
+ def processes
62
+ YAML.load(transmit(:action => 'processes'))
63
+ end
64
+
65
+ def process_alive(pid)
66
+ transmit(:action => 'process_alive', :pid => pid)
67
+ end
68
+
69
+ def kill_process(pid)
70
+ transmit(:action => 'kill_process', :pid => pid)
71
+ end
72
+
73
+ class NotAuthorized < Exception; end
74
+ class FailedTransmit < Exception; end
75
+
76
+ # Given a hash of parameters (converted by the method call on the connection
77
+ # object), send it across the wire to the RushServer listening on the other
78
+ # side. Uses http basic auth, with credentials fetched from the Rush::Config.
79
+ def transmit(params)
80
+ tunnel.ensure_tunnel
81
+
82
+ require 'net/http'
83
+
84
+ payload = params.delete(:payload)
85
+
86
+ uri = "/?"
87
+ params.each do |key, value|
88
+ uri += "#{key}=#{value}&"
89
+ end
90
+
91
+ req = Net::HTTP::Post.new(uri)
92
+ req.basic_auth config.credentials_user, config.credentials_password
93
+
94
+ Net::HTTP.start(tunnel.host, tunnel.port) do |http|
95
+ res = http.request(req, payload)
96
+ raise NotAuthorized if res.code == "401"
97
+ raise FailedTransmit if res.code != "200"
98
+ res.body
99
+ end
100
+ end
101
+
102
+ def config
103
+ @config ||= Rush::Config.new
104
+ end
105
+ 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,81 @@
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
+ printf "%-20s", params[:action]
25
+ print without_action.inspect
26
+ print " + #{payload.size} bytes of payload" if payload.size > 0
27
+ puts
28
+
29
+ params[:payload] = payload
30
+ result = box.connection.receive(params)
31
+
32
+ response.start(200) do |head, out|
33
+ out.write result
34
+ end
35
+ end
36
+ end
37
+
38
+ def authorize(auth)
39
+ unless m = auth.match(/^Basic (.+)$/)
40
+ puts "Request with no authorization data"
41
+ return false
42
+ end
43
+
44
+ decoded = Base64.decode64(m[1])
45
+ user, password = decoded.split(':', 2)
46
+
47
+ if user.nil? or user.length == 0 or password.nil? or password.length == 0
48
+ puts "Malformed user or password"
49
+ return false
50
+ end
51
+
52
+ if password == config.passwords[user]
53
+ return true
54
+ else
55
+ puts "Access denied to #{user}"
56
+ return false
57
+ end
58
+ end
59
+
60
+ def box
61
+ @box ||= Rush::Box.new('localhost')
62
+ end
63
+
64
+ def config
65
+ @config ||= Rush::Config.new
66
+ end
67
+ end
68
+
69
+ # A container class to run the Mongrel server for rushd.
70
+ class RushServer
71
+ def run
72
+ host = "127.0.0.1"
73
+ port = Rush::Config::DefaultPort
74
+
75
+ puts "rushd listening on #{host}:#{port}"
76
+
77
+ h = Mongrel::HttpServer.new(host, port)
78
+ h.register("/", RushHandler.new)
79
+ h.run.join
80
+ end
81
+ end
@@ -0,0 +1,123 @@
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
+ # Set up the user's environment, including a pure binding into which
7
+ # env.rb and commands.rb are mixed.
8
+ def initialize
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
15
+
16
+ @config = Rush::Config.new
17
+
18
+ @config.load_history.each do |item|
19
+ Readline::HISTORY.push(item)
20
+ end
21
+
22
+ Readline.basic_word_break_characters = ""
23
+ Readline.completion_append_character = nil
24
+ Readline.completion_proc = completion_proc
25
+
26
+ eval @config.load_env, @pure_binding
27
+
28
+ eval "def processes; Rush::Box.new('localhost').processes; end", @pure_binding
29
+
30
+ commands = @config.load_commands
31
+ Rush::Dir.class_eval commands
32
+ Array.class_eval commands
33
+ end
34
+
35
+ # Run the interactive shell using readline.
36
+ def run
37
+ loop do
38
+ cmd = Readline.readline('rush> ')
39
+
40
+ finish if cmd.nil?
41
+ next if cmd == ""
42
+ Readline::HISTORY.push(cmd)
43
+
44
+ begin
45
+ res = eval(cmd, @pure_binding)
46
+ $last_res = res
47
+ eval("_ = $last_res", @pure_binding)
48
+ print_result res
49
+ rescue Exception => e
50
+ puts "Exception #{e.class}: #{e}"
51
+ e.backtrace.each do |t|
52
+ puts " #{::File.expand_path(t)}"
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # Save history to ~/.rush/history when the shell exists.
59
+ def finish
60
+ @config.save_history(Readline::HISTORY.to_a)
61
+ puts
62
+ exit
63
+ end
64
+
65
+ # Nice printing of different return types, particularly Rush::SearchResults.
66
+ def print_result(res)
67
+ if res.kind_of? String
68
+ puts res
69
+ elsif res.kind_of? Array
70
+ res.each do |item|
71
+ puts item
72
+ end
73
+ elsif res.kind_of? Rush::SearchResults
74
+ widest = res.entries.map { |k| k.full_path.length }.max
75
+ res.entries_with_lines.each do |entry, lines|
76
+ print entry.full_path
77
+ print ' ' * (widest - entry.full_path.length + 2)
78
+ print "=> "
79
+ print res.colorize(lines.first.strip.head(30))
80
+ print "..." if lines.first.strip.length > 30
81
+ if lines.size > 1
82
+ print " (plus #{lines.size - 1} more matches)"
83
+ end
84
+ print "\n"
85
+ end
86
+ puts "#{res.entries.size} matching files with #{res.lines.size} matching lines"
87
+ else
88
+ puts "=> #{res.inspect}"
89
+ end
90
+ end
91
+
92
+ def path_parts(input) # :nodoc:
93
+ input.match(/^(.+)\[(['"])([^\]]+)$/).to_a.slice(1, 3) rescue [ nil, nil, nil ]
94
+ end
95
+
96
+ # Try to do tab completion on dir square brackets accessors.
97
+ #
98
+ # Example:
99
+ #
100
+ # dir['subd # presing tab here will produce dir['subdir/ if subdir exists
101
+ #
102
+ # This isn't that cool yet, because it can't do multiple levels of subdirs.
103
+ # It does work remotely, though, which is pretty sweet.
104
+ def completion_proc
105
+ proc do |input|
106
+ possible_var, quote, partial_path = path_parts(input)
107
+ if possible_var and possible_var.match(/^[a-z0-9_]+$/)
108
+ full_path = eval("#{possible_var}.full_path", @pure_binding) rescue nil
109
+ box = eval("#{possible_var}.box", @pure_binding) rescue nil
110
+ if full_path and box
111
+ dir = Rush::Dir.new(full_path, box)
112
+ return dir.entries.select do |e|
113
+ e.name.match(/^#{partial_path}/)
114
+ end.map do |e|
115
+ possible_var + '[' + quote + e.name + (e.dir? ? "/" : "")
116
+ end
117
+ end
118
+ end
119
+ nil
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,113 @@
1
+ # Internal class for managing an ssh tunnel, across which relatively insecure
2
+ # HTTP commands can be sent by Rush::Connection::Remote.
3
+ class Rush::SshTunnel
4
+ def initialize(real_host)
5
+ @real_host = real_host
6
+ end
7
+
8
+ def host
9
+ 'localhost'
10
+ end
11
+
12
+ def port
13
+ @port
14
+ end
15
+
16
+ def ensure_tunnel
17
+ return if @port and tunnel_alive?
18
+
19
+ @port = config.tunnels[@real_host]
20
+
21
+ if !@port or !tunnel_alive?
22
+ setup_everything
23
+ end
24
+ end
25
+
26
+ def setup_everything
27
+ display "Connecting to #{@real_host}..."
28
+ push_credentials
29
+ launch_rushd
30
+ establish_tunnel
31
+ end
32
+
33
+ def push_credentials
34
+ display "Pushing credentials"
35
+ config.ensure_credentials_exist
36
+ ssh_append_to_credentials(config.credentials_file.contents.strip)
37
+ end
38
+
39
+ def ssh_append_to_credentials(string)
40
+ # the following horror is exactly why rush is needed
41
+ passwords_file = "~/.rush/passwords"
42
+ string = "'#{string}'"
43
+ ssh "M=`grep #{string} #{passwords_file} | wc -l`; if [ $M = 0 ]; then echo #{string} >> #{passwords_file}; fi"
44
+ end
45
+
46
+ def launch_rushd
47
+ display "Launching rushd"
48
+ ssh("if [ `ps aux | grep rushd | grep -v grep | wc -l` -ge 1 ]; then exit; fi; rushd > /dev/null 2>&1 &")
49
+ end
50
+
51
+ def establish_tunnel
52
+ display "Establishing ssh tunnel"
53
+ @port = next_available_port
54
+
55
+ make_ssh_tunnel
56
+
57
+ tunnels = config.tunnels
58
+ tunnels[@real_host] = @port
59
+ config.save_tunnels tunnels
60
+
61
+ sleep 0.5
62
+ end
63
+
64
+ def tunnel_options
65
+ {
66
+ :local_port => @port,
67
+ :remote_port => Rush::Config::DefaultPort,
68
+ :ssh_host => @real_host,
69
+ :stall_command => "sleep 9000"
70
+ }
71
+ end
72
+
73
+ def tunnel_alive?
74
+ `#{tunnel_count_command}`.to_i > 0
75
+ end
76
+
77
+ def tunnel_count_command
78
+ "ps x | grep '#{ssh_tunnel_command_without_stall}' | grep -v grep | wc -l"
79
+ end
80
+
81
+ class SshFailed < Exception; end
82
+ class NoPortSelectedYet < Exception; end
83
+
84
+ def ssh(command)
85
+ raise SshFailed unless system("ssh #{@real_host} '#{command}'")
86
+ end
87
+
88
+ def make_ssh_tunnel
89
+ raise SshFailed unless system(ssh_tunnel_command)
90
+ end
91
+
92
+ def ssh_tunnel_command_without_stall
93
+ options = tunnel_options
94
+ raise NoPortSelectedYet unless options[:local_port]
95
+ "ssh -f -L #{options[:local_port]}:127.0.0.1:#{options[:remote_port]} #{options[:ssh_host]}"
96
+ end
97
+
98
+ def ssh_tunnel_command
99
+ ssh_tunnel_command_without_stall + " \"#{tunnel_options[:stall_command]}\""
100
+ end
101
+
102
+ def next_available_port
103
+ (config.tunnels.values.max || Rush::Config::DefaultPort) + 1
104
+ end
105
+
106
+ def config
107
+ @config ||= Rush::Config.new
108
+ end
109
+
110
+ def display(msg)
111
+ puts msg
112
+ end
113
+ end