rush 0.1 → 0.2

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.
data/Rakefile CHANGED
@@ -31,7 +31,7 @@ require 'rake/rdoctask'
31
31
  require 'fileutils'
32
32
  include FileUtils
33
33
 
34
- version = "0.1"
34
+ version = "0.2"
35
35
  name = "rush"
36
36
 
37
37
  spec = Gem::Specification.new do |s|
@@ -45,6 +45,10 @@ spec = Gem::Specification.new do |s|
45
45
  s.executables = [ "rush", "rushd" ]
46
46
  s.rubyforge_project = "ruby-shell"
47
47
 
48
+ s.add_dependency 'mongrel'
49
+ s.add_dependency 'rspec'
50
+ s.add_dependency 'session'
51
+
48
52
  s.platform = Gem::Platform::RUBY
49
53
  s.has_rdoc = true
50
54
 
@@ -1,8 +1,11 @@
1
+ require 'rubygems'
2
+
1
3
  module Rush; end
2
4
  module Rush::Connection; end
3
5
 
4
6
  $LOAD_PATH.unshift(File.dirname(__FILE__) + '/rush')
5
7
 
8
+ require 'exceptions'
6
9
  require 'config'
7
10
  require 'commands'
8
11
  require 'entry'
@@ -10,6 +13,7 @@ require 'file'
10
13
  require 'dir'
11
14
  require 'search_results'
12
15
  require 'head_tail'
16
+ require 'find_by'
13
17
  require 'string_ext'
14
18
  require 'fixnum_ext'
15
19
  require 'array_ext'
@@ -13,5 +13,7 @@ class Array
13
13
  self
14
14
  end
15
15
 
16
+ include Rush::FindBy
17
+
16
18
  include Rush::HeadTail
17
19
  end
@@ -49,6 +49,25 @@ class Rush::Box
49
49
  end
50
50
  end
51
51
 
52
+ # Execute a command in the standard unix shell. Until the day when it's no
53
+ # longer needed...
54
+ def bash(command)
55
+ connection.bash(command)
56
+ end
57
+
58
+ # Returns true if the box is responding to commands.
59
+ def alive?
60
+ connection.alive?
61
+ end
62
+
63
+ # This is called automatically the first time an action is invoked, but you
64
+ # may wish to call it manually ahead of time in order to have the tunnel
65
+ # already set up and running. You can also use this to pass a timeout option,
66
+ # either :timeout => (seconds) or :timeout => :infinite.
67
+ def establish_connection(options={})
68
+ connection.ensure_tunnel(options)
69
+ end
70
+
52
71
  def connection # :nodoc:
53
72
  @connection ||= make_connection
54
73
  end
@@ -118,11 +118,21 @@ class Rush::Dir < Rush::Entry
118
118
  end
119
119
  end
120
120
 
121
+ # Run a bash command starting in this directory.
122
+ def bash(command)
123
+ box.bash "cd #{full_path} && #{command}"
124
+ end
125
+
126
+ # Destroy all of the contents of the directory, leaving it fresh and clean.
127
+ def purge
128
+ connection.purge full_path
129
+ end
130
+
121
131
  # Text output of dir listing, equivalent to the regular unix shell's ls command.
122
132
  def ls
123
133
  out = [ "#{self}" ]
124
134
  nonhidden_dirs.each do |dir|
125
- out << " #{dir.name}+"
135
+ out << " #{dir.name}/"
126
136
  end
127
137
  nonhidden_files.each do |file|
128
138
  out << " #{file.name}"
@@ -132,12 +142,12 @@ class Rush::Dir < Rush::Entry
132
142
 
133
143
  # Run rake within this dir.
134
144
  def rake(*args)
135
- system "cd #{full_path}; rake #{args.join(' ')}"
145
+ bash "rake #{args.join(' ')}"
136
146
  end
137
147
 
138
148
  # Run git within this dir.
139
149
  def git(*args)
140
- system "cd #{full_path}; git #{args.join(' ')}"
150
+ bash "git #{args.join(' ')}"
141
151
  end
142
152
 
143
153
  include Rush::Commands
@@ -48,6 +48,14 @@ class Rush::Entry
48
48
  "#{@path}/#{@name}"
49
49
  end
50
50
 
51
+ # Return true if the entry currently exists on the filesystem of the box.
52
+ def exists?
53
+ stat
54
+ true
55
+ rescue Rush::DoesNotExist
56
+ false
57
+ end
58
+
51
59
  # Timestamp of entry creation.
52
60
  def created_at
53
61
  stat[:ctime]
@@ -69,9 +77,6 @@ class Rush::Entry
69
77
  # Do not use rename or duplicate with a slash; use copy_to or move_to instead.
70
78
  class NameCannotContainSlash < Exception; end
71
79
 
72
- # You cannot move or copy entries to a path that is not a dir.
73
- class NotADir < Exception; end
74
-
75
80
  # Rename an entry to another name within the same dir. The object's name
76
81
  # will be updated to match the change on the filesystem.
77
82
  def rename(new_name)
@@ -0,0 +1,26 @@
1
+ module Rush
2
+ # Base class for all rush exceptions.
3
+ class Exception < ::Exception; end
4
+
5
+ # Client was not authorized by remote server; check credentials.
6
+ class NotAuthorized < Exception; end
7
+
8
+ # Failed to transmit to the remote server; check if the ssh tunnel is alive,
9
+ # and rushd is listening on the other end.
10
+ class FailedTransmit < Exception; end
11
+
12
+ # The entry (file or dir) referenced does not exist. Message is the entry's full path.
13
+ class DoesNotExist < Exception; end
14
+
15
+ # The bash command had a non-zero return value. Message is stderr.
16
+ class BashFailed < Exception; end
17
+
18
+ # There's already an entry by the given name in the given dir.
19
+ class NameAlreadyExists < Exception; end
20
+
21
+ # The name cannot contain a slash; use two operations, rename and then move, instead.
22
+ class NameCannotContainSlash < Exception; end
23
+
24
+ # You cannot move or copy entries to a path that is not a dir (should end with trailing slash).
25
+ class NotADir < Exception; end
26
+ end
@@ -52,7 +52,9 @@ class Rush::File < Rush::Entry
52
52
 
53
53
  # Return the file's contents, or if it doesn't exist, a blank string.
54
54
  def contents_or_blank
55
- contents rescue ""
55
+ contents
56
+ rescue Rush::DoesNotExist
57
+ ""
56
58
  end
57
59
 
58
60
  # Count the number of lines in the file.
@@ -62,7 +64,9 @@ class Rush::File < Rush::Entry
62
64
 
63
65
  # Return an array of lines, or an empty array if the file does not exist.
64
66
  def lines_or_empty
65
- lines rescue []
67
+ lines
68
+ rescue Rush::DoesNotExist
69
+ []
66
70
  end
67
71
 
68
72
  include Rush::Commands
@@ -0,0 +1,37 @@
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
+ end
16
+ end
17
+
18
+ def find_by(field, arg)
19
+ detect do |item|
20
+ item.respond_to?(field) and compare_or_match(item.send(field), arg)
21
+ end
22
+ end
23
+
24
+ def find_all_by(field, arg)
25
+ select do |item|
26
+ item.respond_to?(field) and compare_or_match(item.send(field), arg)
27
+ end
28
+ end
29
+
30
+ def compare_or_match(value, against)
31
+ if against.class == Regexp
32
+ value.match(against) ? true : false
33
+ else
34
+ value == against
35
+ end
36
+ end
37
+ end
@@ -24,6 +24,8 @@ class Rush::Connection::Local
24
24
  # Read raw bytes from a file.
25
25
  def file_contents(full_path)
26
26
  ::File.read(full_path)
27
+ rescue Errno::ENOENT
28
+ raise Rush::DoesNotExist, full_path
27
29
  end
28
30
 
29
31
  # Destroy a file or dir.
@@ -33,22 +35,28 @@ class Rush::Connection::Local
33
35
  true
34
36
  end
35
37
 
38
+ # Purge the contents of a dir.
39
+ def purge(full_path)
40
+ raise "No." if full_path == '/'
41
+ Dir.chdir(full_path) do
42
+ all = Dir.glob("*", File::FNM_DOTMATCH).reject { |f| f == '.' or f == '..' }
43
+ FileUtils.rm_rf all
44
+ end
45
+ true
46
+ end
47
+
36
48
  # Create a dir.
37
49
  def create_dir(full_path)
38
50
  FileUtils.mkdir_p(full_path)
39
51
  true
40
52
  end
41
53
 
42
- class NameAlreadyExists < Exception; end
43
- class NameCannotContainSlash < Exception; end
44
- class NotADir < Exception; end
45
-
46
54
  # Rename an entry within a dir.
47
55
  def rename(path, name, new_name)
48
- raise NameCannotContainSlash if new_name.match(/\//)
56
+ raise(Rush::NameCannotContainSlash, "#{path} rename #{name} to #{new_name}") if new_name.match(/\//)
49
57
  old_full_path = "#{path}/#{name}"
50
58
  new_full_path = "#{path}/#{new_name}"
51
- raise NameAlreadyExists if ::File.exists?(new_full_path)
59
+ raise(Rush::NameAlreadyExists, "#{path} rename #{name} to #{new_name}") if ::File.exists?(new_full_path)
52
60
  FileUtils.mv(old_full_path, new_full_path)
53
61
  true
54
62
  end
@@ -57,6 +65,10 @@ class Rush::Connection::Local
57
65
  def copy(src, dst)
58
66
  FileUtils.cp_r(src, dst)
59
67
  true
68
+ rescue Errno::ENOENT
69
+ raise Rush::DoesNotExist, File.dirname(dst)
70
+ rescue RuntimeError
71
+ raise Rush::DoesNotExist, src
60
72
  end
61
73
 
62
74
  # Create an in-memory archive (tgz) of a file or dir, which can be
@@ -89,7 +101,9 @@ class Rush::Connection::Local
89
101
  end
90
102
  end
91
103
  end
92
- dirs + files
104
+ dirs.sort + files.sort
105
+ rescue Errno::ENOENT
106
+ raise Rush::DoesNotExist, base_path
93
107
  end
94
108
 
95
109
  # Fetch stats (size, ctime, etc) on an entry. Size will not be accurate for dirs.
@@ -101,6 +115,8 @@ class Rush::Connection::Local
101
115
  :atime => s.atime,
102
116
  :mtime => s.mtime,
103
117
  }
118
+ rescue Errno::ENOENT
119
+ raise Rush::DoesNotExist, full_path
104
120
  end
105
121
 
106
122
  # Fetch the size of a dir, since a standard file stat does not include the
@@ -113,6 +129,8 @@ class Rush::Connection::Local
113
129
  def processes
114
130
  if ::File.directory? "/proc"
115
131
  linux_processes
132
+ elsif ::File.directory? "C:/WINDOWS"
133
+ windows_processes
116
134
  else
117
135
  os_x_processes
118
136
  end
@@ -139,6 +157,7 @@ class Rush::Connection::Local
139
157
  pid = data[0]
140
158
  command = data[1].match(/^\((.*)\)$/)[1]
141
159
  cmdline = ::File.read("/proc/#{pid}/cmdline").gsub(/\0/, ' ')
160
+ parent_pid = data[3].to_i
142
161
  utime = data[13].to_i
143
162
  ktime = data[14].to_i
144
163
  vss = data[22].to_i / 1024
@@ -150,6 +169,7 @@ class Rush::Connection::Local
150
169
  :uid => uid,
151
170
  :command => command,
152
171
  :cmdline => cmdline,
172
+ :parent_pid => parent_pid,
153
173
  :mem => rss,
154
174
  :cpu => time,
155
175
  }
@@ -165,31 +185,85 @@ class Rush::Connection::Local
165
185
 
166
186
  # ps command used to generate list of processes on non-/proc unixes.
167
187
  def os_x_raw_ps
168
- `COLUMNS=9999 ps ax -o "pid uid rss cpu command"`
188
+ `COLUMNS=9999 ps ax -o "pid uid ppid rss cpu command"`
169
189
  end
170
190
 
171
191
  # Parse a single line of the ps command and return the values in a hash
172
192
  # suitable for use in the Rush::Process#new.
173
193
  def parse_ps(line)
174
- m = line.split(" ", 5)
194
+ m = line.split(" ", 6)
175
195
  params = {}
176
196
  params[:pid] = m[0]
177
197
  params[:uid] = m[1]
178
- params[:rss] = m[2]
179
- params[:cpu] = m[3]
180
- params[:cmdline] = m[4]
198
+ params[:parent_pid] = m[2].to_i
199
+ params[:mem] = m[3].to_i
200
+ params[:cpu] = m[4].to_i
201
+ params[:cmdline] = m[5]
181
202
  params[:command] = params[:cmdline].split(" ").first
182
203
  params
183
204
  end
184
205
 
206
+ # Process list on Windows.
207
+ def windows_processes
208
+ require 'win32ole'
209
+ wmi = WIN32OLE.connect("winmgmts://")
210
+ wmi.ExecQuery("select * from win32_process").map do |proc_info|
211
+ parse_oleprocinfo(proc_info)
212
+ end
213
+ end
214
+
215
+ # Parse the Windows OLE process info.
216
+ def parse_oleprocinfo(proc_info)
217
+ command = proc_info.Name
218
+ pid = proc_info.ProcessId
219
+ uid = 0
220
+ cmdline = proc_info.CommandLine
221
+ rss = proc_info.MaximumWorkingSetSize
222
+ time = proc_info.KernelModeTime.to_i + proc_info.UserModeTime.to_i
223
+
224
+ {
225
+ :pid => pid,
226
+ :uid => uid,
227
+ :command => command,
228
+ :cmdline => cmdline,
229
+ :mem => rss,
230
+ :cpu => time,
231
+ }
232
+ end
233
+
185
234
  # Returns true if the specified pid is running.
186
235
  def process_alive(pid)
187
- `ps -p #{pid} | wc -l`.to_i >= 2
236
+ ::Process.kill(0, pid)
237
+ true
238
+ rescue Errno::ESRCH
239
+ false
188
240
  end
189
241
 
190
242
  # Terminate a process, by pid.
191
243
  def kill_process(pid)
192
- ::Process.kill('TERM', pid.to_i)
244
+ ::Process.kill('TERM', pid)
245
+
246
+ # keep trying until it's dead (technique borrowed from god)
247
+ 5.times do
248
+ return if !process_alive(pid)
249
+ sleep 0.5
250
+ ::Process.kill('TERM', pid) rescue nil
251
+ end
252
+
253
+ ::Process.kill('KILL', pid) rescue nil
254
+ end
255
+
256
+ def bash(command)
257
+ require 'session'
258
+
259
+ sh = Session::Bash.new
260
+ out, err = sh.execute command
261
+ retval = sh.status
262
+ sh.close!
263
+
264
+ raise(Rush::BashFailed, err) if retval != 0
265
+
266
+ out
193
267
  end
194
268
 
195
269
  ####################################
@@ -206,6 +280,7 @@ class Rush::Connection::Local
206
280
  when 'write_file' then write_file(params[:full_path], params[:payload])
207
281
  when 'file_contents' then file_contents(params[:full_path])
208
282
  when 'destroy' then destroy(params[:full_path])
283
+ when 'purge' then purge(params[:full_path])
209
284
  when 'create_dir' then create_dir(params[:full_path])
210
285
  when 'rename' then rename(params[:path], params[:name], params[:new_name])
211
286
  when 'copy' then copy(params[:src], params[:dst])
@@ -216,9 +291,19 @@ class Rush::Connection::Local
216
291
  when 'size' then size(params[:full_path])
217
292
  when 'processes' then YAML.dump(processes)
218
293
  when 'process_alive' then process_alive(params[:pid]) ? '1' : '0'
219
- when 'kill_process' then kill_process(params[:pid])
294
+ when 'kill_process' then kill_process(params[:pid].to_i)
295
+ when 'bash' then bash(params[:payload])
220
296
  else
221
297
  raise UnknownAction
222
298
  end
223
299
  end
300
+
301
+ # No-op for duck typing with remote connection.
302
+ def ensure_tunnel(options={})
303
+ end
304
+
305
+ # Local connections are always alive.
306
+ def alive?
307
+ true
308
+ end
224
309
  end
@@ -1,6 +1,6 @@
1
1
  # An array of these objects is returned by Rush::Box#processes.
2
2
  class Rush::Process
3
- attr_reader :box, :pid, :uid, :command, :cmdline, :mem, :cpu
3
+ attr_reader :box, :pid, :uid, :parent_pid, :command, :cmdline, :mem, :cpu
4
4
 
5
5
  # params is a hash returned by the system-specific method of looking up the
6
6
  # process list.
@@ -11,8 +11,9 @@ class Rush::Process
11
11
  @uid = params[:uid].to_i
12
12
  @command = params[:command]
13
13
  @cmdline = params[:cmdline]
14
- @mem = params[:rss]
15
- @cpu = params[:time]
14
+ @mem = params[:mem]
15
+ @cpu = params[:cpu]
16
+ @parent_pid = params[:parent_pid]
16
17
  end
17
18
 
18
19
  def to_s # :nodoc:
@@ -20,7 +21,17 @@ class Rush::Process
20
21
  end
21
22
 
22
23
  def inspect # :nodoc:
23
- "Process #{@pid}: #{@cmdline}"
24
+ "#{box} process #{@pid}: #{@cmdline}"
25
+ end
26
+
27
+ # Returns the Rush::Process parent of this process.
28
+ def parent
29
+ box.processes.select { |p| p.pid == parent_pid }.first
30
+ end
31
+
32
+ # Returns an array of child processes owned by this process.
33
+ def children
34
+ box.processes.select { |p| p.parent_pid == pid }
24
35
  end
25
36
 
26
37
  # Returns true if the process is currently running.
@@ -33,6 +44,10 @@ class Rush::Process
33
44
  box.connection.kill_process(pid)
34
45
  end
35
46
 
47
+ def ==(other) # :nodoc:
48
+ pid == other.pid and box == other.box
49
+ end
50
+
36
51
  def self.all
37
52
  Rush::Box.new('localhost').processes
38
53
  end