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.
Files changed (50) hide show
  1. data/README.rdoc +89 -0
  2. data/Rakefile +59 -0
  3. data/VERSION +1 -0
  4. data/bin/rush +13 -0
  5. data/bin/rushd +7 -0
  6. data/lib/rush.rb +87 -0
  7. data/lib/rush/access.rb +130 -0
  8. data/lib/rush/array_ext.rb +19 -0
  9. data/lib/rush/box.rb +115 -0
  10. data/lib/rush/commands.rb +55 -0
  11. data/lib/rush/config.rb +154 -0
  12. data/lib/rush/dir.rb +160 -0
  13. data/lib/rush/embeddable_shell.rb +26 -0
  14. data/lib/rush/entry.rb +189 -0
  15. data/lib/rush/exceptions.rb +39 -0
  16. data/lib/rush/file.rb +85 -0
  17. data/lib/rush/find_by.rb +39 -0
  18. data/lib/rush/fixnum_ext.rb +18 -0
  19. data/lib/rush/head_tail.rb +11 -0
  20. data/lib/rush/local.rb +398 -0
  21. data/lib/rush/process.rb +59 -0
  22. data/lib/rush/process_set.rb +62 -0
  23. data/lib/rush/remote.rb +158 -0
  24. data/lib/rush/search_results.rb +58 -0
  25. data/lib/rush/server.rb +117 -0
  26. data/lib/rush/shell.rb +187 -0
  27. data/lib/rush/ssh_tunnel.rb +122 -0
  28. data/lib/rush/string_ext.rb +3 -0
  29. data/spec/access_spec.rb +134 -0
  30. data/spec/array_ext_spec.rb +15 -0
  31. data/spec/base.rb +24 -0
  32. data/spec/box_spec.rb +80 -0
  33. data/spec/commands_spec.rb +47 -0
  34. data/spec/config_spec.rb +108 -0
  35. data/spec/dir_spec.rb +164 -0
  36. data/spec/embeddable_shell_spec.rb +17 -0
  37. data/spec/entry_spec.rb +133 -0
  38. data/spec/file_spec.rb +83 -0
  39. data/spec/find_by_spec.rb +58 -0
  40. data/spec/fixnum_ext_spec.rb +19 -0
  41. data/spec/local_spec.rb +364 -0
  42. data/spec/process_set_spec.rb +50 -0
  43. data/spec/process_spec.rb +73 -0
  44. data/spec/remote_spec.rb +140 -0
  45. data/spec/rush_spec.rb +28 -0
  46. data/spec/search_results_spec.rb +44 -0
  47. data/spec/shell_spec.rb +23 -0
  48. data/spec/ssh_tunnel_spec.rb +122 -0
  49. data/spec/string_ext_spec.rb +23 -0
  50. metadata +142 -0
@@ -0,0 +1,39 @@
1
+ module Rush
2
+ # Base class for all rush exceptions.
3
+ class Exception < ::RuntimeError; end
4
+
5
+ # Client was not authorized by remote server; check credentials.
6
+ class NotAuthorized < Exception; end
7
+
8
+ # rushd is not running on the remote box.
9
+ class RushdNotRunning < Exception; end
10
+
11
+ # An unrecognized status code was returned by rushd.
12
+ class FailedTransmit < Exception; end
13
+
14
+ # The entry (file or dir) referenced does not exist. Message is the entry's full path.
15
+ class DoesNotExist < Exception; end
16
+
17
+ # The bash command had a non-zero return value. Message is stderr. stdout is also available.
18
+ class BashFailed < Exception
19
+ def stderr; to_s; end
20
+ attr_reader :stdout
21
+
22
+ def initialize(err, out)
23
+ super(err)
24
+ @stdout = out
25
+ end
26
+ end
27
+
28
+ # There's already an entry by the given name in the given dir.
29
+ class NameAlreadyExists < Exception; end
30
+
31
+ # The name cannot contain a slash; use two operations, rename and then move, instead.
32
+ class NameCannotContainSlash < Exception; end
33
+
34
+ # You cannot move or copy entries to a path that is not a dir (should end with trailing slash).
35
+ class NotADir < Exception; end
36
+
37
+ # A user or permission value specified to set access was not valid.
38
+ class BadAccessSpecifier < Exception; end
39
+ end
data/lib/rush/file.rb ADDED
@@ -0,0 +1,85 @@
1
+ # Files are a subclass of Rush::Entry. Most of the file-specific operations
2
+ # relate to manipulating the file's contents, like search and replace.
3
+ class Rush::File < Rush::Entry
4
+ def dir?
5
+ false
6
+ end
7
+
8
+ # Create a blank file.
9
+ def create
10
+ write('')
11
+ self
12
+ end
13
+
14
+ # Size in bytes on disk.
15
+ def size
16
+ stat[:size]
17
+ end
18
+
19
+ # Raw contents of the file. For non-text files, you probably want to avoid
20
+ # printing this on the screen.
21
+ def contents
22
+ connection.file_contents(full_path)
23
+ end
24
+
25
+ alias :read :contents
26
+
27
+ # Write to the file, overwriting whatever was already in it.
28
+ #
29
+ # Example: file.write "hello, world\n"
30
+ def write(new_contents)
31
+ connection.write_file(full_path, new_contents)
32
+ end
33
+
34
+ # Append new contents to the end of the file, keeping what was in it.
35
+ def append(contents)
36
+ connection.append_to_file(full_path, contents)
37
+ end
38
+ alias_method :<<, :append
39
+
40
+ # Return an array of lines from the file, similar to stdlib's File#readlines.
41
+ def lines
42
+ contents.split("\n")
43
+ end
44
+
45
+ # Search the file's for a regular expression. Returns nil if no match, or
46
+ # each of the matching lines in its entirety.
47
+ #
48
+ # Example: box['/etc/hosts'].search(/localhost/) # -> [ "127.0.0.1 localhost\n", "::1 localhost\n" ]
49
+ def search(pattern)
50
+ matching_lines = lines.select { |line| line.match(pattern) }
51
+ matching_lines.size == 0 ? nil : matching_lines
52
+ end
53
+
54
+ # Search-and-replace file contents.
55
+ #
56
+ # Example: box['/etc/hosts'].replace_contents!(/localhost/, 'local.host')
57
+ def replace_contents!(pattern, replace_with)
58
+ write contents.gsub(pattern, replace_with)
59
+ end
60
+
61
+ # Return the file's contents, or if it doesn't exist, a blank string.
62
+ def contents_or_blank
63
+ contents
64
+ rescue Rush::DoesNotExist
65
+ ""
66
+ end
67
+
68
+ # Count the number of lines in the file.
69
+ def line_count
70
+ lines.size
71
+ end
72
+
73
+ # Return an array of lines, or an empty array if the file does not exist.
74
+ def lines_or_empty
75
+ lines
76
+ rescue Rush::DoesNotExist
77
+ []
78
+ end
79
+
80
+ include Rush::Commands
81
+
82
+ def entries
83
+ [ self ]
84
+ end
85
+ end
@@ -0,0 +1,39 @@
1
+ # Generic find_by (returns first match) and find_all_by (returns all matches)
2
+ # against arrays.
3
+ #
4
+ # Examples:
5
+ #
6
+ # processes.find_by_pid(::Process.pid)
7
+ # processes.find_all_by_cmdline(/mongrel_rails/)
8
+ #
9
+ module Rush::FindBy
10
+ def method_missing(meth, *args)
11
+ if m = meth.to_s.match(/^find_by_([a-z_]+)$/)
12
+ find_by(m[1], args.first)
13
+ elsif m = meth.to_s.match(/^find_all_by_([a-z_]+)$/)
14
+ find_all_by(m[1], args.first)
15
+ else
16
+ super
17
+ end
18
+ end
19
+
20
+ def find_by(field, arg)
21
+ detect do |item|
22
+ item.respond_to?(field) and compare_or_match(item.send(field), arg)
23
+ end
24
+ end
25
+
26
+ def find_all_by(field, arg)
27
+ select do |item|
28
+ item.respond_to?(field) and compare_or_match(item.send(field), arg)
29
+ end
30
+ end
31
+
32
+ def compare_or_match(value, against)
33
+ if against.class == Regexp
34
+ value.match(against) ? true : false
35
+ else
36
+ value == against
37
+ end
38
+ end
39
+ end
@@ -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 Fixnum
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,11 @@
1
+ # Mixin for array and string for methods I wish they had.
2
+ module Rush::HeadTail
3
+ def head(n)
4
+ slice(0, n)
5
+ end
6
+
7
+ def tail(n)
8
+ n = [ n, length ].min
9
+ slice(-n, n)
10
+ end
11
+ end
data/lib/rush/local.rb ADDED
@@ -0,0 +1,398 @@
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, force)
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) and not force)
69
+ FileUtils.mv(old_full_path, new_full_path, :force => force)
70
+ true
71
+ end
72
+
73
+ # Copy ane entry from one path to another.
74
+ def copy(src, dst, force)
75
+ FileUtils.cp_r(src, dst, :remove_destination => force)
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 (tar) 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, force)
92
+ IO.popen("cd #{Rush::quote(dir)}; tar x #{'--keep-old-files' unless force}", "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, reset_environment=false)
311
+ return bash_background(command, user, reset_environment) if background
312
+
313
+ require 'session'
314
+ sh = Session::Bash.new
315
+ cmd = reset_environment ? "env -i bash" : "bash"
316
+ cmd = "cd /; sudo -H -u #{user} \"#{cmd}\"" if user and user != ""
317
+ out, err = sh.execute(cmd, :stdin => command)
318
+ retval = sh.status
319
+ sh.close!
320
+
321
+ raise Rush::BashFailed.new(err, out) if retval != 0
322
+ out
323
+ end
324
+
325
+ def bash_background(command, user, reset_environment)
326
+ pid = fork do
327
+ inpipe, outpipe = IO.pipe
328
+
329
+ outpipe.write command
330
+ outpipe.close
331
+ STDIN.reopen(inpipe)
332
+
333
+ close_all_descriptors([inpipe.to_i])
334
+
335
+ shell = reset_environment ? "env -i bash" : "bash"
336
+
337
+ if user and user != ''
338
+ exec "cd /; sudo -H -u #{user} \"#{shell}\""
339
+ else
340
+ exec shell
341
+ end
342
+ end
343
+
344
+ Process::detach pid
345
+
346
+ pid
347
+ end
348
+
349
+ def close_all_descriptors(keep_open = [])
350
+ 3.upto(256) do |fd|
351
+ next if keep_open.include?(fd)
352
+ IO::new(fd).close rescue nil
353
+ end
354
+ end
355
+
356
+ ####################################
357
+
358
+ # Raised when the action passed in by RushServer is not known.
359
+ class UnknownAction < Exception; end
360
+
361
+ # RushServer uses this method to transform a hash (:action plus parameters
362
+ # specific to that action type) into a method call on the connection. The
363
+ # returned value must be text so that it can be transmitted across the wire
364
+ # as an HTTP response.
365
+ def receive(params)
366
+ case params[:action]
367
+ when 'write_file' then write_file(params[:full_path], params[:payload])
368
+ when 'append_to_file' then append_to_file(params[:full_path], params[:payload])
369
+ when 'file_contents' then file_contents(params[:full_path])
370
+ when 'destroy' then destroy(params[:full_path])
371
+ when 'purge' then purge(params[:full_path])
372
+ when 'create_dir' then create_dir(params[:full_path])
373
+ when 'rename' then rename(params[:path], params[:name], params[:new_name], params[:force])
374
+ when 'copy' then copy(params[:src], params[:dst], params[:force])
375
+ when 'read_archive' then read_archive(params[:full_path])
376
+ when 'write_archive' then write_archive(params[:payload], params[:dir], params[:force])
377
+ when 'index' then index(params[:base_path], params[:glob]).join("\n") + "\n"
378
+ when 'stat' then YAML.dump(stat(params[:full_path]))
379
+ when 'set_access' then set_access(params[:full_path], Rush::Access.from_hash(params))
380
+ when 'size' then size(params[:full_path])
381
+ when 'processes' then YAML.dump(processes)
382
+ when 'process_alive' then process_alive(params[:pid]) ? '1' : '0'
383
+ when 'kill_process' then kill_process(params[:pid].to_i, YAML.load(params[:payload]))
384
+ when 'bash' then bash(params[:payload], params[:user], params[:background] == 'true', params[:reset_environment] == 'true')
385
+ else
386
+ raise UnknownAction
387
+ end
388
+ end
389
+
390
+ # No-op for duck typing with remote connection.
391
+ def ensure_tunnel(options={})
392
+ end
393
+
394
+ # Local connections are always alive.
395
+ def alive?
396
+ true
397
+ end
398
+ end