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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +13 -0
  3. data/Gemfile.lock +86 -0
  4. data/README.rdoc +125 -0
  5. data/Rakefile +57 -0
  6. data/VERSION +1 -0
  7. data/bin/rush +13 -0
  8. data/bin/rushd +7 -0
  9. data/lib/rush.rb +91 -0
  10. data/lib/rush/access.rb +121 -0
  11. data/lib/rush/array_ext.rb +19 -0
  12. data/lib/rush/box.rb +124 -0
  13. data/lib/rush/commands.rb +76 -0
  14. data/lib/rush/config.rb +147 -0
  15. data/lib/rush/dir.rb +166 -0
  16. data/lib/rush/embeddable_shell.rb +26 -0
  17. data/lib/rush/entry.rb +229 -0
  18. data/lib/rush/exceptions.rb +32 -0
  19. data/lib/rush/file.rb +94 -0
  20. data/lib/rush/find_by.rb +39 -0
  21. data/lib/rush/fixnum_ext.rb +18 -0
  22. data/lib/rush/head_tail.rb +11 -0
  23. data/lib/rush/local.rb +377 -0
  24. data/lib/rush/process.rb +59 -0
  25. data/lib/rush/process_set.rb +62 -0
  26. data/lib/rush/remote.rb +33 -0
  27. data/lib/rush/search_results.rb +71 -0
  28. data/lib/rush/shell.rb +111 -0
  29. data/lib/rush/shell/completion.rb +110 -0
  30. data/lib/rush/string_ext.rb +16 -0
  31. data/spec/access_spec.rb +134 -0
  32. data/spec/array_ext_spec.rb +15 -0
  33. data/spec/base.rb +22 -0
  34. data/spec/box_spec.rb +76 -0
  35. data/spec/commands_spec.rb +47 -0
  36. data/spec/config_spec.rb +108 -0
  37. data/spec/dir_spec.rb +163 -0
  38. data/spec/embeddable_shell_spec.rb +17 -0
  39. data/spec/entry_spec.rb +133 -0
  40. data/spec/file_spec.rb +83 -0
  41. data/spec/find_by_spec.rb +58 -0
  42. data/spec/fixnum_ext_spec.rb +19 -0
  43. data/spec/local_spec.rb +365 -0
  44. data/spec/process_set_spec.rb +50 -0
  45. data/spec/process_spec.rb +73 -0
  46. data/spec/remote_spec.rb +140 -0
  47. data/spec/rush_spec.rb +28 -0
  48. data/spec/search_results_spec.rb +44 -0
  49. data/spec/shell_spec.rb +35 -0
  50. data/spec/ssh_tunnel_spec.rb +122 -0
  51. data/spec/string_ext_spec.rb +23 -0
  52. metadata +209 -0
@@ -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
@@ -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