adamwiggins-rush 0.6.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.
- data/README.rdoc +87 -0
- data/Rakefile +61 -0
- data/VERSION +1 -0
- data/bin/rush +13 -0
- data/bin/rushd +7 -0
- data/lib/rush/access.rb +130 -0
- data/lib/rush/array_ext.rb +19 -0
- data/lib/rush/box.rb +112 -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 +185 -0
- data/lib/rush/exceptions.rb +31 -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 +402 -0
- data/lib/rush/process.rb +59 -0
- data/lib/rush/process_set.rb +62 -0
- data/lib/rush/remote.rb +156 -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/lib/rush.rb +87 -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 +64 -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 +352 -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 +141 -0
data/lib/rush/local.rb
ADDED
@@ -0,0 +1,402 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
# Rush::Box uses a connection object to execute all rush commands. If the box
|
6
|
+
# is local, Rush::Connection::Local is created. The local connection is the
|
7
|
+
# heart of rush's internals. (Users of the rush shell or library need never
|
8
|
+
# access the connection object directly, so the docs herein are intended for
|
9
|
+
# developers wishing to modify rush.)
|
10
|
+
#
|
11
|
+
# The local connection has a series of methods which do the actual work of
|
12
|
+
# modifying files, getting process lists, and so on. RushServer creates a
|
13
|
+
# local connection to handle incoming requests; the translation from a raw hash
|
14
|
+
# of parameters to an executed method is handled by
|
15
|
+
# Rush::Connection::Local#receive.
|
16
|
+
class Rush::Connection::Local
|
17
|
+
# Write raw bytes to a file.
|
18
|
+
def write_file(full_path, contents)
|
19
|
+
::File.open(full_path, 'w') do |f|
|
20
|
+
f.write contents
|
21
|
+
end
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
# Append contents to a file
|
26
|
+
def append_to_file(full_path, contents)
|
27
|
+
::File.open(full_path, 'a') do |f|
|
28
|
+
f.write contents
|
29
|
+
end
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
# Read raw bytes from a file.
|
34
|
+
def file_contents(full_path)
|
35
|
+
::File.read(full_path)
|
36
|
+
rescue Errno::ENOENT
|
37
|
+
raise Rush::DoesNotExist, full_path
|
38
|
+
end
|
39
|
+
|
40
|
+
# Destroy a file or dir.
|
41
|
+
def destroy(full_path)
|
42
|
+
raise "No." if full_path == '/'
|
43
|
+
FileUtils.rm_rf(full_path)
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
# Purge the contents of a dir.
|
48
|
+
def purge(full_path)
|
49
|
+
raise "No." if full_path == '/'
|
50
|
+
Dir.chdir(full_path) do
|
51
|
+
all = Dir.glob("*", File::FNM_DOTMATCH).reject { |f| f == '.' or f == '..' }
|
52
|
+
FileUtils.rm_rf all
|
53
|
+
end
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
# Create a dir.
|
58
|
+
def create_dir(full_path)
|
59
|
+
FileUtils.mkdir_p(full_path)
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
63
|
+
# Rename an entry within a dir.
|
64
|
+
def rename(path, name, new_name)
|
65
|
+
raise(Rush::NameCannotContainSlash, "#{path} rename #{name} to #{new_name}") if new_name.match(/\//)
|
66
|
+
old_full_path = "#{path}/#{name}"
|
67
|
+
new_full_path = "#{path}/#{new_name}"
|
68
|
+
raise(Rush::NameAlreadyExists, "#{path} rename #{name} to #{new_name}") if ::File.exists?(new_full_path)
|
69
|
+
FileUtils.mv(old_full_path, new_full_path)
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
# Copy ane entry from one path to another.
|
74
|
+
def copy(src, dst)
|
75
|
+
FileUtils.cp_r(src, dst)
|
76
|
+
true
|
77
|
+
rescue Errno::ENOENT
|
78
|
+
raise Rush::DoesNotExist, File.dirname(dst)
|
79
|
+
rescue RuntimeError
|
80
|
+
raise Rush::DoesNotExist, src
|
81
|
+
end
|
82
|
+
|
83
|
+
# Create an in-memory archive (tgz) of a file or dir, which can be
|
84
|
+
# transmitted to another server for a copy or move. Note that archive
|
85
|
+
# operations have the dir name implicit in the archive.
|
86
|
+
def read_archive(full_path)
|
87
|
+
`cd #{Rush.quote(::File.dirname(full_path))}; tar c #{Rush.quote(::File.basename(full_path))}`
|
88
|
+
end
|
89
|
+
|
90
|
+
# Extract an in-memory archive to a dir.
|
91
|
+
def write_archive(archive, dir)
|
92
|
+
IO.popen("cd #{Rush::quote(dir)}; tar x", "w") do |p|
|
93
|
+
p.write archive
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Get an index of files from the given path with the glob. Could return
|
98
|
+
# nested values if the glob contains a doubleglob. The return value is an
|
99
|
+
# array of full paths, with directories listed first.
|
100
|
+
def index(base_path, glob)
|
101
|
+
glob = '*' if glob == '' or glob.nil?
|
102
|
+
dirs = []
|
103
|
+
files = []
|
104
|
+
::Dir.chdir(base_path) do
|
105
|
+
::Dir.glob(glob).each do |fname|
|
106
|
+
if ::File.directory?(fname)
|
107
|
+
dirs << fname + '/'
|
108
|
+
else
|
109
|
+
files << fname
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
dirs.sort + files.sort
|
114
|
+
rescue Errno::ENOENT
|
115
|
+
raise Rush::DoesNotExist, base_path
|
116
|
+
end
|
117
|
+
|
118
|
+
# Fetch stats (size, ctime, etc) on an entry. Size will not be accurate for dirs.
|
119
|
+
def stat(full_path)
|
120
|
+
s = ::File.stat(full_path)
|
121
|
+
{
|
122
|
+
:size => s.size,
|
123
|
+
:ctime => s.ctime,
|
124
|
+
:atime => s.atime,
|
125
|
+
:mtime => s.mtime,
|
126
|
+
:mode => s.mode
|
127
|
+
}
|
128
|
+
rescue Errno::ENOENT
|
129
|
+
raise Rush::DoesNotExist, full_path
|
130
|
+
end
|
131
|
+
|
132
|
+
def set_access(full_path, access)
|
133
|
+
access.apply(full_path)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Fetch the size of a dir, since a standard file stat does not include the
|
137
|
+
# size of the contents.
|
138
|
+
def size(full_path)
|
139
|
+
`du -sb #{Rush.quote(full_path)}`.match(/(\d+)/)[1].to_i
|
140
|
+
end
|
141
|
+
|
142
|
+
# Get the list of processes as an array of hashes.
|
143
|
+
def processes
|
144
|
+
if ::File.directory? "/proc"
|
145
|
+
resolve_unix_uids(linux_processes)
|
146
|
+
elsif ::File.directory? "C:/WINDOWS"
|
147
|
+
windows_processes
|
148
|
+
else
|
149
|
+
os_x_processes
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Process list on Linux using /proc.
|
154
|
+
def linux_processes
|
155
|
+
list = []
|
156
|
+
::Dir["/proc/*/stat"].select { |file| file =~ /\/proc\/\d+\// }.each do |file|
|
157
|
+
begin
|
158
|
+
list << read_proc_file(file)
|
159
|
+
rescue
|
160
|
+
# process died between the dir listing and accessing the file
|
161
|
+
end
|
162
|
+
end
|
163
|
+
list
|
164
|
+
end
|
165
|
+
|
166
|
+
def resolve_unix_uids(list)
|
167
|
+
@uid_map = {} # reset the cache between uid resolutions.
|
168
|
+
list.each do |process|
|
169
|
+
process[:user] = resolve_unix_uid_to_user(process[:uid])
|
170
|
+
end
|
171
|
+
list
|
172
|
+
end
|
173
|
+
|
174
|
+
# resolve uid to user
|
175
|
+
def resolve_unix_uid_to_user(uid)
|
176
|
+
require 'etc'
|
177
|
+
|
178
|
+
@uid_map ||= {}
|
179
|
+
uid = uid.to_i
|
180
|
+
|
181
|
+
return @uid_map[uid] if !@uid_map[uid].nil?
|
182
|
+
|
183
|
+
begin
|
184
|
+
record = Etc.getpwuid(uid)
|
185
|
+
rescue ArgumentError
|
186
|
+
return nil
|
187
|
+
end
|
188
|
+
|
189
|
+
@uid_map[uid] = record.name
|
190
|
+
@uid_map[uid]
|
191
|
+
end
|
192
|
+
|
193
|
+
# Read a single file in /proc and store the parsed values in a hash suitable
|
194
|
+
# for use in the Rush::Process#new.
|
195
|
+
def read_proc_file(file)
|
196
|
+
data = ::File.read(file).split(" ")
|
197
|
+
uid = ::File.stat(file).uid
|
198
|
+
pid = data[0]
|
199
|
+
command = data[1].match(/^\((.*)\)$/)[1]
|
200
|
+
cmdline = ::File.read("/proc/#{pid}/cmdline").gsub(/\0/, ' ')
|
201
|
+
parent_pid = data[3].to_i
|
202
|
+
utime = data[13].to_i
|
203
|
+
ktime = data[14].to_i
|
204
|
+
vss = data[22].to_i / 1024
|
205
|
+
rss = data[23].to_i * 4
|
206
|
+
time = utime + ktime
|
207
|
+
|
208
|
+
{
|
209
|
+
:pid => pid,
|
210
|
+
:uid => uid,
|
211
|
+
:command => command,
|
212
|
+
:cmdline => cmdline,
|
213
|
+
:parent_pid => parent_pid,
|
214
|
+
:mem => rss,
|
215
|
+
:cpu => time,
|
216
|
+
}
|
217
|
+
end
|
218
|
+
|
219
|
+
# Process list on OS X or other unixes without a /proc.
|
220
|
+
def os_x_processes
|
221
|
+
raw = os_x_raw_ps.split("\n").slice(1, 99999)
|
222
|
+
raw.map do |line|
|
223
|
+
parse_ps(line)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# ps command used to generate list of processes on non-/proc unixes.
|
228
|
+
def os_x_raw_ps
|
229
|
+
`COLUMNS=9999 ps ax -o "pid uid ppid rss cpu command"`
|
230
|
+
end
|
231
|
+
|
232
|
+
# Parse a single line of the ps command and return the values in a hash
|
233
|
+
# suitable for use in the Rush::Process#new.
|
234
|
+
def parse_ps(line)
|
235
|
+
m = line.split(" ", 6)
|
236
|
+
params = {}
|
237
|
+
params[:pid] = m[0]
|
238
|
+
params[:uid] = m[1]
|
239
|
+
params[:parent_pid] = m[2].to_i
|
240
|
+
params[:mem] = m[3].to_i
|
241
|
+
params[:cpu] = m[4].to_i
|
242
|
+
params[:cmdline] = m[5]
|
243
|
+
params[:command] = params[:cmdline].split(" ").first
|
244
|
+
params
|
245
|
+
end
|
246
|
+
|
247
|
+
# Process list on Windows.
|
248
|
+
def windows_processes
|
249
|
+
require 'win32ole'
|
250
|
+
wmi = WIN32OLE.connect("winmgmts://")
|
251
|
+
wmi.ExecQuery("select * from win32_process").map do |proc_info|
|
252
|
+
parse_oleprocinfo(proc_info)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Parse the Windows OLE process info.
|
257
|
+
def parse_oleprocinfo(proc_info)
|
258
|
+
command = proc_info.Name
|
259
|
+
pid = proc_info.ProcessId
|
260
|
+
uid = 0
|
261
|
+
cmdline = proc_info.CommandLine
|
262
|
+
rss = proc_info.MaximumWorkingSetSize
|
263
|
+
time = proc_info.KernelModeTime.to_i + proc_info.UserModeTime.to_i
|
264
|
+
|
265
|
+
{
|
266
|
+
:pid => pid,
|
267
|
+
:uid => uid,
|
268
|
+
:command => command,
|
269
|
+
:cmdline => cmdline,
|
270
|
+
:mem => rss,
|
271
|
+
:cpu => time,
|
272
|
+
}
|
273
|
+
end
|
274
|
+
|
275
|
+
# Returns true if the specified pid is running.
|
276
|
+
def process_alive(pid)
|
277
|
+
::Process.kill(0, pid)
|
278
|
+
true
|
279
|
+
rescue Errno::ESRCH
|
280
|
+
false
|
281
|
+
end
|
282
|
+
|
283
|
+
# Terminate a process, by pid.
|
284
|
+
def kill_process(pid, options={})
|
285
|
+
# time to wait before terminating the process, in seconds
|
286
|
+
wait = options[:wait] || 3
|
287
|
+
|
288
|
+
if wait > 0
|
289
|
+
::Process.kill('TERM', pid)
|
290
|
+
|
291
|
+
# keep trying until it's dead (technique borrowed from god)
|
292
|
+
begin
|
293
|
+
Timeout.timeout(wait) do
|
294
|
+
loop do
|
295
|
+
return if !process_alive(pid)
|
296
|
+
sleep 0.5
|
297
|
+
::Process.kill('TERM', pid) rescue nil
|
298
|
+
end
|
299
|
+
end
|
300
|
+
rescue Timeout::Error
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
::Process.kill('KILL', pid) rescue nil
|
305
|
+
|
306
|
+
rescue Errno::ESRCH
|
307
|
+
# if it's dead, great - do nothing
|
308
|
+
end
|
309
|
+
|
310
|
+
def bash(command, user=nil, background=false)
|
311
|
+
return bash_background(command, user) if background
|
312
|
+
|
313
|
+
require 'session'
|
314
|
+
|
315
|
+
sh = Session::Bash.new
|
316
|
+
|
317
|
+
if user and user != ""
|
318
|
+
out, err = sh.execute "cd /; sudo -H -u #{user} bash", :stdin => command
|
319
|
+
else
|
320
|
+
out, err = sh.execute command
|
321
|
+
end
|
322
|
+
|
323
|
+
retval = sh.status
|
324
|
+
sh.close!
|
325
|
+
|
326
|
+
raise(Rush::BashFailed, err) if retval != 0
|
327
|
+
|
328
|
+
out
|
329
|
+
end
|
330
|
+
|
331
|
+
def bash_background(command, user)
|
332
|
+
pid = fork do
|
333
|
+
inpipe, outpipe = IO.pipe
|
334
|
+
|
335
|
+
outpipe.write command
|
336
|
+
outpipe.close
|
337
|
+
STDIN.reopen(inpipe)
|
338
|
+
|
339
|
+
close_all_descriptors([inpipe.to_i])
|
340
|
+
|
341
|
+
if user and user != ''
|
342
|
+
exec "cd /; sudo -H -u #{user} bash"
|
343
|
+
else
|
344
|
+
exec "bash"
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
Process::detach pid
|
349
|
+
|
350
|
+
pid
|
351
|
+
end
|
352
|
+
|
353
|
+
def close_all_descriptors(keep_open = [])
|
354
|
+
3.upto(256) do |fd|
|
355
|
+
next if keep_open.include?(fd)
|
356
|
+
IO::new(fd).close rescue nil
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
####################################
|
361
|
+
|
362
|
+
# Raised when the action passed in by RushServer is not known.
|
363
|
+
class UnknownAction < Exception; end
|
364
|
+
|
365
|
+
# RushServer uses this method to transform a hash (:action plus parameters
|
366
|
+
# specific to that action type) into a method call on the connection. The
|
367
|
+
# returned value must be text so that it can be transmitted across the wire
|
368
|
+
# as an HTTP response.
|
369
|
+
def receive(params)
|
370
|
+
case params[:action]
|
371
|
+
when 'write_file' then write_file(params[:full_path], params[:payload])
|
372
|
+
when 'append_to_file' then append_to_file(params[:full_path], params[:payload])
|
373
|
+
when 'file_contents' then file_contents(params[:full_path])
|
374
|
+
when 'destroy' then destroy(params[:full_path])
|
375
|
+
when 'purge' then purge(params[:full_path])
|
376
|
+
when 'create_dir' then create_dir(params[:full_path])
|
377
|
+
when 'rename' then rename(params[:path], params[:name], params[:new_name])
|
378
|
+
when 'copy' then copy(params[:src], params[:dst])
|
379
|
+
when 'read_archive' then read_archive(params[:full_path])
|
380
|
+
when 'write_archive' then write_archive(params[:payload], params[:dir])
|
381
|
+
when 'index' then index(params[:base_path], params[:glob]).join("\n") + "\n"
|
382
|
+
when 'stat' then YAML.dump(stat(params[:full_path]))
|
383
|
+
when 'set_access' then set_access(params[:full_path], Rush::Access.from_hash(params))
|
384
|
+
when 'size' then size(params[:full_path])
|
385
|
+
when 'processes' then YAML.dump(processes)
|
386
|
+
when 'process_alive' then process_alive(params[:pid]) ? '1' : '0'
|
387
|
+
when 'kill_process' then kill_process(params[:pid].to_i, YAML.load(params[:payload]))
|
388
|
+
when 'bash' then bash(params[:payload], params[:user], params[:background] == 'true')
|
389
|
+
else
|
390
|
+
raise UnknownAction
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
# No-op for duck typing with remote connection.
|
395
|
+
def ensure_tunnel(options={})
|
396
|
+
end
|
397
|
+
|
398
|
+
# Local connections are always alive.
|
399
|
+
def alive?
|
400
|
+
true
|
401
|
+
end
|
402
|
+
end
|
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,156 @@
|
|
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)
|
42
|
+
transmit(:action => 'rename', :path => path, :name => name, :new_name => 'new_name')
|
43
|
+
end
|
44
|
+
|
45
|
+
def copy(src, dst)
|
46
|
+
transmit(:action => 'copy', :src => src, :dst => dst)
|
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)
|
54
|
+
transmit(:action => 'write_archive', :dir => dir, :payload => archive)
|
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)
|
86
|
+
transmit(:action => 'bash', :payload => command, :user => user, :background => background)
|
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
|
+
res = http.request(req, payload)
|
109
|
+
process_result(res.code, res.body)
|
110
|
+
end
|
111
|
+
rescue EOFError
|
112
|
+
raise Rush::RushdNotRunning
|
113
|
+
end
|
114
|
+
|
115
|
+
# Take the http result of a transmit and raise an error, or return the body
|
116
|
+
# of the result when valid.
|
117
|
+
def process_result(code, body)
|
118
|
+
raise Rush::NotAuthorized if code == "401"
|
119
|
+
|
120
|
+
if code == "400"
|
121
|
+
klass, message = parse_exception(body)
|
122
|
+
raise klass, "#{host}:#{message}"
|
123
|
+
end
|
124
|
+
|
125
|
+
raise Rush::FailedTransmit if code != "200"
|
126
|
+
|
127
|
+
body
|
128
|
+
end
|
129
|
+
|
130
|
+
# Parse an exception returned from the server, with the class name on the
|
131
|
+
# first line and the message on the second.
|
132
|
+
def parse_exception(body)
|
133
|
+
klass, message = body.split("\n", 2)
|
134
|
+
raise "invalid exception class: #{klass}" unless klass.match(/^Rush::[A-Za-z]+$/)
|
135
|
+
klass = Object.module_eval(klass)
|
136
|
+
[ klass, message.strip ]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Set up the tunnel if it is not already running.
|
140
|
+
def ensure_tunnel(options={})
|
141
|
+
tunnel.ensure_tunnel(options)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Remote connections are alive when the box on the other end is responding
|
145
|
+
# to commands.
|
146
|
+
def alive?
|
147
|
+
index('/', 'alive_check')
|
148
|
+
true
|
149
|
+
rescue
|
150
|
+
false
|
151
|
+
end
|
152
|
+
|
153
|
+
def config
|
154
|
+
@config ||= Rush::Config.new
|
155
|
+
end
|
156
|
+
end
|