rush 0.1 → 0.2

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