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.
- data/README.rdoc +89 -0
- data/Rakefile +59 -0
- data/VERSION +1 -0
- data/bin/rush +13 -0
- data/bin/rushd +7 -0
- data/lib/rush.rb +87 -0
- data/lib/rush/access.rb +130 -0
- data/lib/rush/array_ext.rb +19 -0
- data/lib/rush/box.rb +115 -0
- data/lib/rush/commands.rb +55 -0
- data/lib/rush/config.rb +154 -0
- data/lib/rush/dir.rb +160 -0
- data/lib/rush/embeddable_shell.rb +26 -0
- data/lib/rush/entry.rb +189 -0
- data/lib/rush/exceptions.rb +39 -0
- data/lib/rush/file.rb +85 -0
- data/lib/rush/find_by.rb +39 -0
- data/lib/rush/fixnum_ext.rb +18 -0
- data/lib/rush/head_tail.rb +11 -0
- data/lib/rush/local.rb +398 -0
- data/lib/rush/process.rb +59 -0
- data/lib/rush/process_set.rb +62 -0
- data/lib/rush/remote.rb +158 -0
- data/lib/rush/search_results.rb +58 -0
- data/lib/rush/server.rb +117 -0
- data/lib/rush/shell.rb +187 -0
- data/lib/rush/ssh_tunnel.rb +122 -0
- data/lib/rush/string_ext.rb +3 -0
- data/spec/access_spec.rb +134 -0
- data/spec/array_ext_spec.rb +15 -0
- data/spec/base.rb +24 -0
- data/spec/box_spec.rb +80 -0
- data/spec/commands_spec.rb +47 -0
- data/spec/config_spec.rb +108 -0
- data/spec/dir_spec.rb +164 -0
- data/spec/embeddable_shell_spec.rb +17 -0
- data/spec/entry_spec.rb +133 -0
- data/spec/file_spec.rb +83 -0
- data/spec/find_by_spec.rb +58 -0
- data/spec/fixnum_ext_spec.rb +19 -0
- data/spec/local_spec.rb +364 -0
- data/spec/process_set_spec.rb +50 -0
- data/spec/process_spec.rb +73 -0
- data/spec/remote_spec.rb +140 -0
- data/spec/rush_spec.rb +28 -0
- data/spec/search_results_spec.rb +44 -0
- data/spec/shell_spec.rb +23 -0
- data/spec/ssh_tunnel_spec.rb +122 -0
- data/spec/string_ext_spec.rb +23 -0
- metadata +142 -0
data/lib/rush/process.rb
ADDED
@@ -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
|
data/lib/rush/remote.rb
ADDED
@@ -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
|
data/lib/rush/server.rb
ADDED
@@ -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
|