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 +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
|