aleksi-rush 0.6.6

Sign up to get free protection for your applications and to get access to all the features.
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