rush3 3.0.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 +15 -0
  3. data/Gemfile.lock +93 -0
  4. data/README.rdoc +124 -0
  5. data/Rakefile +58 -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 +16 -0
  12. data/lib/rush/box.rb +130 -0
  13. data/lib/rush/commands.rb +94 -0
  14. data/lib/rush/config.rb +147 -0
  15. data/lib/rush/dir.rb +159 -0
  16. data/lib/rush/embeddable_shell.rb +26 -0
  17. data/lib/rush/entry.rb +238 -0
  18. data/lib/rush/exceptions.rb +32 -0
  19. data/lib/rush/file.rb +100 -0
  20. data/lib/rush/find_by.rb +39 -0
  21. data/lib/rush/head_tail.rb +11 -0
  22. data/lib/rush/integer_ext.rb +18 -0
  23. data/lib/rush/local.rb +404 -0
  24. data/lib/rush/path.rb +9 -0
  25. data/lib/rush/process.rb +59 -0
  26. data/lib/rush/process_set.rb +62 -0
  27. data/lib/rush/search_results.rb +71 -0
  28. data/lib/rush/shell.rb +118 -0
  29. data/lib/rush/shell/completion.rb +108 -0
  30. data/lib/rush/string_ext.rb +33 -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 +162 -0
  40. data/spec/file_spec.rb +95 -0
  41. data/spec/find_by_spec.rb +58 -0
  42. data/spec/integer_ext_spec.rb +19 -0
  43. data/spec/local_spec.rb +363 -0
  44. data/spec/path_spec.rb +13 -0
  45. data/spec/process_set_spec.rb +50 -0
  46. data/spec/process_spec.rb +89 -0
  47. data/spec/rush_spec.rb +28 -0
  48. data/spec/search_results_spec.rb +44 -0
  49. data/spec/shell_spec.rb +39 -0
  50. data/spec/ssh_tunnel_spec.rb +122 -0
  51. data/spec/string_ext_spec.rb +23 -0
  52. metadata +228 -0
@@ -0,0 +1,18 @@
1
+ # Integer extensions for file and dir sizes (returned in bytes).
2
+ #
3
+ # Example:
4
+ #
5
+ # box['/assets/'].files_flattened.select { |f| f.size > 10.mb }
6
+ class Integer
7
+ def kb
8
+ self * 1024
9
+ end
10
+
11
+ def mb
12
+ kb * 1024
13
+ end
14
+
15
+ def gb
16
+ mb * 1024
17
+ end
18
+ end
@@ -0,0 +1,404 @@
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 a hard link to another file
80
+ def ln(src, dst, options = {})
81
+ raise(Rush::DoesNotExist, src) unless File.exists?(src)
82
+ raise(Rush::DoesNotExist, File.dirname(dst)) unless File.exists?(File.dirname(dst))
83
+ FileUtils.ln(src, dst, options)
84
+ true
85
+ end
86
+
87
+ # Create a symbolic link to another file
88
+ def ln_s(src, dst, options = {})
89
+ raise(Rush::DoesNotExist, src) unless File.exists?(src)
90
+ raise(Rush::DoesNotExist, File.dirname(dst)) unless File.exists?(File.dirname(dst))
91
+ FileUtils.ln_s(src, dst, options)
92
+ true
93
+ end
94
+
95
+ # Is this path a symlink?
96
+ def symlink?(path)
97
+ File.symlink? path
98
+ end
99
+
100
+ # Create an in-memory archive (tgz) of a file or dir, which can be
101
+ # transmitted to another server for a copy or move. Note that archive
102
+ # operations have the dir name implicit in the archive.
103
+ def read_archive(full_path)
104
+ `cd #{Rush.quote(::File.dirname(full_path))}; tar c #{Rush.quote(::File.basename(full_path))}`
105
+ end
106
+
107
+ # Extract an in-memory archive to a dir.
108
+ def write_archive(archive, dir)
109
+ IO.popen("cd #{Rush::quote(dir)}; tar x", "w") do |p|
110
+ p.write archive
111
+ end
112
+ end
113
+
114
+ # Get an index of files from the given path with the glob. Could return
115
+ # nested values if the glob contains a doubleglob. The return value is an
116
+ # array of full paths, with directories listed first.
117
+ def index(base_path, glob)
118
+ glob = '*' if glob == '' or glob.nil?
119
+ dirs = []
120
+ files = []
121
+ ::Dir.chdir(base_path) do
122
+ ::Dir.glob(glob).each do |fname|
123
+ if ::File.directory?(fname)
124
+ dirs << fname + '/'
125
+ else
126
+ files << fname
127
+ end
128
+ end
129
+ end
130
+ dirs.sort + files.sort
131
+ rescue Errno::ENOENT
132
+ raise Rush::DoesNotExist, base_path
133
+ end
134
+
135
+ # Fetch stats (size, ctime, etc) on an entry. Size will not be accurate for dirs.
136
+ def stat(full_path)
137
+ s = ::File.stat(full_path)
138
+ {
139
+ :size => s.size,
140
+ :ctime => s.ctime,
141
+ :atime => s.atime,
142
+ :mtime => s.mtime,
143
+ :mode => s.mode
144
+ }
145
+ rescue Errno::ENOENT
146
+ raise Rush::DoesNotExist, full_path
147
+ end
148
+
149
+ def set_access(full_path, access)
150
+ access.apply(full_path)
151
+ end
152
+
153
+ # A frontend for FileUtils::chown
154
+ #
155
+ def chown( full_path, user=nil, group=nil, options={} )
156
+ if options[:recursive]
157
+ FileUtils.chown_R(user, group, full_path, options)
158
+ else
159
+ FileUtils.chown(user, group, full_path, options)
160
+ end
161
+ end
162
+
163
+ # Fetch the size of a dir, since a standard file stat does not include the
164
+ # size of the contents.
165
+ def size(full_path)
166
+ if RUBY_PLATFORM.match(/darwin/)
167
+ `find #{Rush.quote(full_path)} -print0 | xargs -0 stat -f%z`.split(/\n/).map(&:to_i).reduce(:+)
168
+ else
169
+ `du -sb #{Rush.quote(full_path)}`.match(/(\d+)/)[1].to_i
170
+ end
171
+ end
172
+
173
+ # Get the list of processes as an array of hashes.
174
+ def processes
175
+ if ::File.directory? "/proc"
176
+ resolve_unix_uids(linux_processes)
177
+ elsif ::File.directory? "C:/WINDOWS"
178
+ windows_processes
179
+ else
180
+ os_x_processes
181
+ end.uniq
182
+ end
183
+
184
+ # Process list on Linux using /proc.
185
+ def linux_processes
186
+ ::Dir["/proc/*/stat"].
187
+ select { |file| file =~ /\/proc\/\d+\// }.
188
+ inject([]) { |list, file| (list << read_proc_file(file)) rescue list }
189
+ end
190
+
191
+ def resolve_unix_uids(list)
192
+ @uid_map = {} # reset the cache between uid resolutions.
193
+ list.each do |process|
194
+ process[:user] = resolve_unix_uid_to_user(process[:uid])
195
+ end
196
+ list
197
+ end
198
+
199
+ # resolve uid to user
200
+ def resolve_unix_uid_to_user(uid)
201
+ uid = uid.to_i
202
+ require 'etc'
203
+ @uid_map ||= {}
204
+ return @uid_map[uid] if @uid_map[uid]
205
+ record = Etc.getpwuid(uid) rescue (return nil)
206
+ @uid_map.merge uid => record.name
207
+ record.name
208
+ end
209
+
210
+ # Read a single file in /proc and store the parsed values in a hash suitable
211
+ # for use in the Rush::Process#new.
212
+ def read_proc_file(file)
213
+ stat_contents = ::File.read(file)
214
+ stat_contents.gsub!(/\((.*)\)/, "")
215
+ command = $1
216
+ data = stat_contents.split(" ")
217
+ uid = ::File.stat(file).uid
218
+ pid = data[0]
219
+ cmdline = ::File.read("/proc/#{pid}/cmdline").gsub(/\0/, ' ')
220
+ parent_pid = data[2].to_i
221
+ utime = data[12].to_i
222
+ ktime = data[13].to_i
223
+ vss = data[21].to_i / 1024
224
+ rss = data[22].to_i * 4
225
+ time = utime + ktime
226
+
227
+ {
228
+ :pid => pid,
229
+ :uid => uid,
230
+ :command => command,
231
+ :cmdline => cmdline,
232
+ :parent_pid => parent_pid,
233
+ :mem => rss,
234
+ :cpu => time
235
+ }
236
+ end
237
+
238
+ # Process list on OS X or other unixes without a /proc.
239
+ def os_x_processes
240
+ raw = os_x_raw_ps.split("\n").slice(1, 99999)
241
+ raw.map do |line|
242
+ parse_ps(line)
243
+ end
244
+ end
245
+
246
+ # ps command used to generate list of processes on non-/proc unixes.
247
+ def os_x_raw_ps
248
+ `COLUMNS=9999 ps ax -o "pid uid ppid rss cpu command"`
249
+ end
250
+
251
+ # Parse a single line of the ps command and return the values in a hash
252
+ # suitable for use in the Rush::Process#new.
253
+ def parse_ps(line)
254
+ m = line.split(" ", 6)
255
+ {
256
+ pid: m[0],
257
+ uid: m[1],
258
+ parent_pid: m[2].to_i,
259
+ mem: m[3].to_i,
260
+ cpu: m[4].to_i,
261
+ cmdline: m[5],
262
+ command: m[5].split(' ').first
263
+ }
264
+ end
265
+
266
+ # Process list on Windows.
267
+ def windows_processes
268
+ require 'win32ole'
269
+ wmi = WIN32OLE.connect("winmgmts://")
270
+ wmi.ExecQuery("select * from win32_process").map do |proc_info|
271
+ parse_oleprocinfo(proc_info)
272
+ end
273
+ end
274
+
275
+ # Parse the Windows OLE process info.
276
+ def parse_oleprocinfo(proc_info)
277
+ {
278
+ pid: proc_info.ProcessId,
279
+ uid: 0,
280
+ command: proc_info.Name,
281
+ cmdline: proc_info.CommandLine,
282
+ mem: proc_info.MaximumWorkingSetSize,
283
+ cpu: proc_info.KernelModeTime.to_i + proc_info.UserModeTime.to_i
284
+ }
285
+ end
286
+
287
+ # Returns true if the specified pid is running.
288
+ def process_alive(pid)
289
+ ::Process.kill(0, pid)
290
+ true
291
+ rescue Errno::ESRCH
292
+ false
293
+ end
294
+
295
+ # Terminate a process, by pid.
296
+ def kill_process(pid, options={})
297
+ # time to wait before terminating the process, in seconds
298
+ wait = options[:wait] || 3
299
+
300
+ if wait > 0
301
+ ::Process.kill('TERM', pid)
302
+
303
+ # keep trying until it's dead (technique borrowed from god)
304
+ begin
305
+ Timeout.timeout(wait) do
306
+ loop do
307
+ return if !process_alive(pid)
308
+ sleep 0.5
309
+ ::Process.kill('TERM', pid) rescue nil
310
+ end
311
+ end
312
+ rescue Timeout::Error
313
+ end
314
+ end
315
+
316
+ ::Process.kill('KILL', pid) rescue nil
317
+
318
+ rescue Errno::ESRCH
319
+ # if it's dead, great - do nothing
320
+ end
321
+
322
+ def bash(command, user=nil, background=false, reset_environment=false)
323
+ return bash_background(command, user, reset_environment) if background
324
+ sh = Session::Bash.new
325
+ shell = reset_environment ? "env -i bash" : "bash"
326
+ out, err = sh.execute sudo(user, shell), :stdin => command
327
+ retval = sh.status
328
+ sh.close!
329
+ raise(Rush::BashFailed, err) if retval != 0
330
+ out
331
+ end
332
+
333
+ def bash_background(command, user, reset_environment)
334
+ pid = fork do
335
+ inpipe, outpipe = IO.pipe
336
+ outpipe.write command
337
+ outpipe.close
338
+ STDIN.reopen(inpipe)
339
+ close_all_descriptors([inpipe.to_i])
340
+ shell = reset_environment ? "env -i bash" : "bash"
341
+ exec sudo(user, shell)
342
+ end
343
+ Process::detach pid
344
+ pid
345
+ end
346
+
347
+ def sudo(user, shell)
348
+ return shell if user.nil? || user.empty?
349
+ "cd /; sudo -H -u #{user} \"#{shell}\""
350
+ end
351
+
352
+ def close_all_descriptors(keep_open = [])
353
+ 3.upto(256) do |fd|
354
+ next if keep_open.include?(fd)
355
+ IO::new(fd).close rescue nil
356
+ end
357
+ end
358
+
359
+ ####################################
360
+
361
+ # Raised when the action passed in by RushServer is not known.
362
+ class UnknownAction < Exception; end
363
+
364
+ # RushServer uses this method to transform a hash (:action plus parameters
365
+ # specific to that action type) into a method call on the connection. The
366
+ # returned value must be text so that it can be transmitted across the wire
367
+ # as an HTTP response.
368
+ def receive(params)
369
+ case params[:action]
370
+ when 'write_file' then write_file(params[:full_path], params[:payload])
371
+ when 'append_to_file' then append_to_file(params[:full_path], params[:payload])
372
+ when 'file_contents' then file_contents(params[:full_path])
373
+ when 'destroy' then destroy(params[:full_path])
374
+ when 'purge' then purge(params[:full_path])
375
+ when 'create_dir' then create_dir(params[:full_path])
376
+ when 'rename' then rename(params[:path], params[:name], params[:new_name])
377
+ when 'copy' then copy(params[:src], params[:dst])
378
+ when 'ln' then ln(params[:src], params[:dst], params[:options])
379
+ when 'ln_s' then ln_s(params[:src], params[:dst], params[:options])
380
+ when 'read_archive' then read_archive(params[:full_path])
381
+ when 'write_archive' then write_archive(params[:payload], params[:dir])
382
+ when 'index' then index(params[:base_path], params[:glob]).join("\n") + "\n"
383
+ when 'stat' then YAML.dump(stat(params[:full_path]))
384
+ when 'set_access' then set_access(params[:full_path], Rush::Access.from_hash(params))
385
+ when 'chown' then chown(params[:full_path], params[:user], params[:group], params[:options])
386
+ when 'size' then size(params[:full_path])
387
+ when 'processes' then YAML.dump(processes)
388
+ when 'process_alive' then process_alive(params[:pid]) ? '1' : '0'
389
+ when 'kill_process' then kill_process(params[:pid].to_i, YAML.load(params[:payload]))
390
+ when 'bash' then bash(params[:payload], params[:user], params[:background] == 'true', params[:reset_environment] == 'true')
391
+ else
392
+ raise UnknownAction
393
+ end
394
+ end
395
+
396
+ # No-op for duck typing with remote connection.
397
+ def ensure_tunnel(options={})
398
+ end
399
+
400
+ # Local connections are always alive.
401
+ def alive?
402
+ true
403
+ end
404
+ end
@@ -0,0 +1,9 @@
1
+ # A tiny wrapper around ENV['PATH']
2
+ class Rush::Path
3
+ def self.executables
4
+ ENV['PATH'].split(':')
5
+ .select { |f| ::File.directory?(f) }
6
+ .map { |x| Rush::Dir.new(x).entries.map(&:name) }
7
+ .flatten
8
+ end
9
+ 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