rush 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +5 -1
- data/lib/rush.rb +4 -0
- data/lib/rush/array_ext.rb +2 -0
- data/lib/rush/box.rb +19 -0
- data/lib/rush/dir.rb +13 -3
- data/lib/rush/entry.rb +8 -3
- data/lib/rush/exceptions.rb +26 -0
- data/lib/rush/file.rb +6 -2
- data/lib/rush/find_by.rb +37 -0
- data/lib/rush/local.rb +100 -15
- data/lib/rush/process.rb +19 -4
- data/lib/rush/remote.rb +47 -6
- data/lib/rush/server.rb +48 -12
- data/lib/rush/shell.rb +23 -14
- data/lib/rush/ssh_tunnel.rb +21 -12
- data/spec/box_spec.rb +25 -0
- data/spec/dir_spec.rb +5 -0
- data/spec/entry_spec.rb +2 -2
- data/spec/file_spec.rb +4 -0
- data/spec/find_by_spec.rb +55 -0
- data/spec/local_spec.rb +80 -13
- data/spec/process_spec.rb +32 -3
- data/spec/remote_spec.rb +46 -0
- data/spec/shell_spec.rb +1 -1
- data/spec/ssh_tunnel_spec.rb +22 -6
- metadata +33 -4
data/Rakefile
CHANGED
@@ -31,7 +31,7 @@ require 'rake/rdoctask'
|
|
31
31
|
require 'fileutils'
|
32
32
|
include FileUtils
|
33
33
|
|
34
|
-
version = "0.
|
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
|
|
data/lib/rush.rb
CHANGED
@@ -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'
|
data/lib/rush/array_ext.rb
CHANGED
data/lib/rush/box.rb
CHANGED
@@ -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
|
data/lib/rush/dir.rb
CHANGED
@@ -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
|
-
|
145
|
+
bash "rake #{args.join(' ')}"
|
136
146
|
end
|
137
147
|
|
138
148
|
# Run git within this dir.
|
139
149
|
def git(*args)
|
140
|
-
|
150
|
+
bash "git #{args.join(' ')}"
|
141
151
|
end
|
142
152
|
|
143
153
|
include Rush::Commands
|
data/lib/rush/entry.rb
CHANGED
@@ -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
|
data/lib/rush/file.rb
CHANGED
@@ -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
|
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
|
67
|
+
lines
|
68
|
+
rescue Rush::DoesNotExist
|
69
|
+
[]
|
66
70
|
end
|
67
71
|
|
68
72
|
include Rush::Commands
|
data/lib/rush/find_by.rb
ADDED
@@ -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
|
data/lib/rush/local.rb
CHANGED
@@ -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
|
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
|
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(" ",
|
194
|
+
m = line.split(" ", 6)
|
175
195
|
params = {}
|
176
196
|
params[:pid] = m[0]
|
177
197
|
params[:uid] = m[1]
|
178
|
-
params[:
|
179
|
-
params[:
|
180
|
-
params[:
|
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
|
-
|
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
|
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
|
data/lib/rush/process.rb
CHANGED
@@ -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[:
|
15
|
-
@cpu = params[:
|
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
|
-
"
|
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
|