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