rush 0.1
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 +86 -0
- data/bin/rush +6 -0
- data/bin/rushd +6 -0
- data/lib/rush.rb +20 -0
- data/lib/rush/array_ext.rb +17 -0
- data/lib/rush/box.rb +63 -0
- data/lib/rush/commands.rb +55 -0
- data/lib/rush/config.rb +154 -0
- data/lib/rush/dir.rb +148 -0
- data/lib/rush/entry.rb +141 -0
- data/lib/rush/file.rb +73 -0
- data/lib/rush/fixnum_ext.rb +18 -0
- data/lib/rush/head_tail.rb +11 -0
- data/lib/rush/local.rb +224 -0
- data/lib/rush/process.rb +39 -0
- data/lib/rush/remote.rb +105 -0
- data/lib/rush/search_results.rb +58 -0
- data/lib/rush/server.rb +81 -0
- data/lib/rush/shell.rb +123 -0
- data/lib/rush/ssh_tunnel.rb +113 -0
- data/lib/rush/string_ext.rb +3 -0
- data/spec/array_ext_spec.rb +15 -0
- data/spec/base.rb +24 -0
- data/spec/box_spec.rb +18 -0
- data/spec/commands_spec.rb +47 -0
- data/spec/config_spec.rb +108 -0
- data/spec/dir_spec.rb +148 -0
- data/spec/entry_spec.rb +118 -0
- data/spec/file_spec.rb +75 -0
- data/spec/fixnum_ext_spec.rb +19 -0
- data/spec/local_spec.rb +196 -0
- data/spec/process_spec.rb +44 -0
- data/spec/remote_spec.rb +84 -0
- data/spec/search_results_spec.rb +44 -0
- data/spec/shell_spec.rb +12 -0
- data/spec/ssh_tunnel_spec.rb +106 -0
- data/spec/string_ext_spec.rb +23 -0
- metadata +91 -0
data/lib/rush/entry.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
# Rush::Entry is the base class for Rush::File and Rush::Dir. One or more of
|
2
|
+
# these is instantiated whenever you use square brackets to access the
|
3
|
+
# filesystem on a box, as well as any other operation that returns an entry or
|
4
|
+
# list of entries.
|
5
|
+
class Rush::Entry
|
6
|
+
attr_reader :box, :name, :path
|
7
|
+
|
8
|
+
# Initialize with full path to the file or dir, and the box it resides on.
|
9
|
+
def initialize(full_path, box=nil)
|
10
|
+
full_path = ::File.expand_path(full_path, '/')
|
11
|
+
@path = ::File.dirname(full_path)
|
12
|
+
@name = ::File.basename(full_path)
|
13
|
+
@box = box || Rush::Box.new('localhost')
|
14
|
+
end
|
15
|
+
|
16
|
+
# The factory checks to see if the full path has a trailing slash for
|
17
|
+
# creating a Rush::Dir rather than the default Rush::File.
|
18
|
+
def self.factory(full_path, box=nil)
|
19
|
+
if full_path.tail(1) == '/'
|
20
|
+
Rush::Dir.new(full_path, box)
|
21
|
+
else
|
22
|
+
Rush::File.new(full_path, box)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s # :nodoc:
|
27
|
+
if box.host == 'localhost'
|
28
|
+
"#{full_path}"
|
29
|
+
else
|
30
|
+
inspect
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect # :nodoc:
|
35
|
+
"#{box}:#{full_path}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def connection
|
39
|
+
box ? box.connection : Rush::Connection::Local.new
|
40
|
+
end
|
41
|
+
|
42
|
+
# The parent dir. For example, box['/etc/hosts'].parent == box['etc/']
|
43
|
+
def parent
|
44
|
+
@parent ||= Rush::Dir.new(@path)
|
45
|
+
end
|
46
|
+
|
47
|
+
def full_path
|
48
|
+
"#{@path}/#{@name}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Timestamp of entry creation.
|
52
|
+
def created_at
|
53
|
+
stat[:ctime]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Timestamp that entry was last modified on.
|
57
|
+
def last_modified
|
58
|
+
stat[:mtime]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Timestamp that entry was last accessed on.
|
62
|
+
def last_accessed
|
63
|
+
stat[:atime]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Attempts to rename, copy, or otherwise place an entry into a dir that already contains an entry by that name will fail with this exception.
|
67
|
+
class NameAlreadyExists < Exception; end
|
68
|
+
|
69
|
+
# Do not use rename or duplicate with a slash; use copy_to or move_to instead.
|
70
|
+
class NameCannotContainSlash < Exception; end
|
71
|
+
|
72
|
+
# You cannot move or copy entries to a path that is not a dir.
|
73
|
+
class NotADir < Exception; end
|
74
|
+
|
75
|
+
# Rename an entry to another name within the same dir. The object's name
|
76
|
+
# will be updated to match the change on the filesystem.
|
77
|
+
def rename(new_name)
|
78
|
+
connection.rename(@path, @name, new_name)
|
79
|
+
@name = new_name
|
80
|
+
end
|
81
|
+
|
82
|
+
# Rename an entry to another name within the same dir. The existing object
|
83
|
+
# will not be affected, but a new object representing the newly-created
|
84
|
+
# entry will be returned.
|
85
|
+
def duplicate(new_name)
|
86
|
+
raise NameCannotContainSlash if new_name.match(/\//)
|
87
|
+
new_full_path = "#{@path}/#{new_name}"
|
88
|
+
connection.copy(full_path, new_full_path)
|
89
|
+
self.class.new(new_full_path, box)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Copy the entry to another dir. Returns an object representing the new
|
93
|
+
# copy.
|
94
|
+
def copy_to(dir)
|
95
|
+
raise NotADir unless dir.class == Rush::Dir
|
96
|
+
|
97
|
+
if box == dir.box
|
98
|
+
connection.copy(full_path, dir.full_path)
|
99
|
+
else
|
100
|
+
archive = connection.read_archive(full_path)
|
101
|
+
dir.box.connection.write_archive(archive, dir.full_path)
|
102
|
+
end
|
103
|
+
|
104
|
+
new_full_path = "#{dir.full_path}#{name}"
|
105
|
+
self.class.new(new_full_path, dir.box)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Move the entry to another dir. The object will be updated to show its new
|
109
|
+
# location.
|
110
|
+
def move_to(dir)
|
111
|
+
moved = copy_to(dir)
|
112
|
+
destroy
|
113
|
+
mimic(moved)
|
114
|
+
end
|
115
|
+
|
116
|
+
def mimic(from) # :nodoc:
|
117
|
+
@box = from.box
|
118
|
+
@path = from.path
|
119
|
+
@name = from.name
|
120
|
+
end
|
121
|
+
|
122
|
+
# Unix convention considers entries starting with a . to be hidden.
|
123
|
+
def hidden?
|
124
|
+
name.slice(0, 1) == '.'
|
125
|
+
end
|
126
|
+
|
127
|
+
# Destroy the entry. If it is a dir, everything inside it will also be destroyed.
|
128
|
+
def destroy
|
129
|
+
connection.destroy(full_path)
|
130
|
+
end
|
131
|
+
|
132
|
+
def ==(other) # :nodoc:
|
133
|
+
full_path == other.full_path and box == other.box
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def stat
|
139
|
+
connection.stat(full_path)
|
140
|
+
end
|
141
|
+
end
|
data/lib/rush/file.rb
ADDED
@@ -0,0 +1,73 @@
|
|
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
|
+
# Write to the file, overwriting whatever was already in it.
|
26
|
+
#
|
27
|
+
# Example: file.write "hello, world\n"
|
28
|
+
def write(new_contents)
|
29
|
+
connection.write_file(full_path, new_contents)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return an array of lines from the file, similar to stdlib's File#readlines.
|
33
|
+
def lines
|
34
|
+
contents.split("\n")
|
35
|
+
end
|
36
|
+
|
37
|
+
# Search the file's for a regular expression. Returns nil if no match, or
|
38
|
+
# each of the matching lines in its entirety.
|
39
|
+
#
|
40
|
+
# Example: box['/etc/hosts'].search(/localhost/) # -> [ "127.0.0.1 localhost\n", "::1 localhost\n" ]
|
41
|
+
def search(pattern)
|
42
|
+
matching_lines = lines.select { |line| line.match(pattern) }
|
43
|
+
matching_lines.size == 0 ? nil : matching_lines
|
44
|
+
end
|
45
|
+
|
46
|
+
# Search-and-replace file contents.
|
47
|
+
#
|
48
|
+
# Example: box['/etc/hosts'].replace_contents!(/localhost/, 'local.host')
|
49
|
+
def replace_contents!(pattern, replace_with)
|
50
|
+
write contents.gsub(pattern, replace_with)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return the file's contents, or if it doesn't exist, a blank string.
|
54
|
+
def contents_or_blank
|
55
|
+
contents rescue ""
|
56
|
+
end
|
57
|
+
|
58
|
+
# Count the number of lines in the file.
|
59
|
+
def line_count
|
60
|
+
lines.size
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return an array of lines, or an empty array if the file does not exist.
|
64
|
+
def lines_or_empty
|
65
|
+
lines rescue []
|
66
|
+
end
|
67
|
+
|
68
|
+
include Rush::Commands
|
69
|
+
|
70
|
+
def entries
|
71
|
+
[ self ]
|
72
|
+
end
|
73
|
+
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
|
data/lib/rush/local.rb
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
# Rush::Box uses a connection object to execute all rush commands. If the box
|
5
|
+
# is local, Rush::Connection::Local is created. The local connection is the
|
6
|
+
# heart of rush's internals. (Users of the rush shell or library need never
|
7
|
+
# access the connection object directly, so the docs herein are intended for
|
8
|
+
# developers wishing to modify rush.)
|
9
|
+
#
|
10
|
+
# The local connection has a series of methods which do the actual work of
|
11
|
+
# modifying files, getting process lists, and so on. RushServer creates a
|
12
|
+
# local connection to handle incoming requests; the translation from a raw hash
|
13
|
+
# of parameters to an executed method is handled by
|
14
|
+
# Rush::Connection::Local#receive.
|
15
|
+
class Rush::Connection::Local
|
16
|
+
# Write raw bytes to a file.
|
17
|
+
def write_file(full_path, contents)
|
18
|
+
::File.open(full_path, 'w') do |f|
|
19
|
+
f.write contents
|
20
|
+
end
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
# Read raw bytes from a file.
|
25
|
+
def file_contents(full_path)
|
26
|
+
::File.read(full_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Destroy a file or dir.
|
30
|
+
def destroy(full_path)
|
31
|
+
raise "No." if full_path == '/'
|
32
|
+
FileUtils.rm_rf(full_path)
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Create a dir.
|
37
|
+
def create_dir(full_path)
|
38
|
+
FileUtils.mkdir_p(full_path)
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
class NameAlreadyExists < Exception; end
|
43
|
+
class NameCannotContainSlash < Exception; end
|
44
|
+
class NotADir < Exception; end
|
45
|
+
|
46
|
+
# Rename an entry within a dir.
|
47
|
+
def rename(path, name, new_name)
|
48
|
+
raise NameCannotContainSlash if new_name.match(/\//)
|
49
|
+
old_full_path = "#{path}/#{name}"
|
50
|
+
new_full_path = "#{path}/#{new_name}"
|
51
|
+
raise NameAlreadyExists if ::File.exists?(new_full_path)
|
52
|
+
FileUtils.mv(old_full_path, new_full_path)
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Copy ane entry from one path to another.
|
57
|
+
def copy(src, dst)
|
58
|
+
FileUtils.cp_r(src, dst)
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
# Create an in-memory archive (tgz) of a file or dir, which can be
|
63
|
+
# transmitted to another server for a copy or move. Note that archive
|
64
|
+
# operations have the dir name implicit in the archive.
|
65
|
+
def read_archive(full_path)
|
66
|
+
`cd #{::File.dirname(full_path)}; tar c #{::File.basename(full_path)}`
|
67
|
+
end
|
68
|
+
|
69
|
+
# Extract an in-memory archive to a dir.
|
70
|
+
def write_archive(archive, dir)
|
71
|
+
IO.popen("cd #{dir}; tar x", "w") do |p|
|
72
|
+
p.write archive
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Get an index of files from the given path with the glob. Could return
|
77
|
+
# nested values if the glob contains a doubleglob. The return value is an
|
78
|
+
# array of full paths, with directories listed first.
|
79
|
+
def index(base_path, glob)
|
80
|
+
glob = '*' if glob == '' or glob.nil?
|
81
|
+
dirs = []
|
82
|
+
files = []
|
83
|
+
::Dir.chdir(base_path) do
|
84
|
+
::Dir.glob(glob).each do |fname|
|
85
|
+
if ::File.directory?(fname)
|
86
|
+
dirs << fname + '/'
|
87
|
+
else
|
88
|
+
files << fname
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
dirs + files
|
93
|
+
end
|
94
|
+
|
95
|
+
# Fetch stats (size, ctime, etc) on an entry. Size will not be accurate for dirs.
|
96
|
+
def stat(full_path)
|
97
|
+
s = ::File.stat(full_path)
|
98
|
+
{
|
99
|
+
:size => s.size,
|
100
|
+
:ctime => s.ctime,
|
101
|
+
:atime => s.atime,
|
102
|
+
:mtime => s.mtime,
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
# Fetch the size of a dir, since a standard file stat does not include the
|
107
|
+
# size of the contents.
|
108
|
+
def size(full_path)
|
109
|
+
`du -sb #{full_path}`.match(/(\d+)/)[1].to_i
|
110
|
+
end
|
111
|
+
|
112
|
+
# Get the list of processes as an array of hashes.
|
113
|
+
def processes
|
114
|
+
if ::File.directory? "/proc"
|
115
|
+
linux_processes
|
116
|
+
else
|
117
|
+
os_x_processes
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Process list on Linux using /proc.
|
122
|
+
def linux_processes
|
123
|
+
list = []
|
124
|
+
::Dir["/proc/*/stat"].select { |file| file =~ /\/proc\/\d+\// }.each do |file|
|
125
|
+
begin
|
126
|
+
list << read_proc_file(file)
|
127
|
+
rescue
|
128
|
+
# process died between the dir listing and accessing the file
|
129
|
+
end
|
130
|
+
end
|
131
|
+
list
|
132
|
+
end
|
133
|
+
|
134
|
+
# Read a single file in /proc and store the parsed values in a hash suitable
|
135
|
+
# for use in the Rush::Process#new.
|
136
|
+
def read_proc_file(file)
|
137
|
+
data = ::File.read(file).split(" ")
|
138
|
+
uid = ::File.stat(file).uid
|
139
|
+
pid = data[0]
|
140
|
+
command = data[1].match(/^\((.*)\)$/)[1]
|
141
|
+
cmdline = ::File.read("/proc/#{pid}/cmdline").gsub(/\0/, ' ')
|
142
|
+
utime = data[13].to_i
|
143
|
+
ktime = data[14].to_i
|
144
|
+
vss = data[22].to_i / 1024
|
145
|
+
rss = data[23].to_i * 4
|
146
|
+
time = utime + ktime
|
147
|
+
|
148
|
+
{
|
149
|
+
:pid => pid,
|
150
|
+
:uid => uid,
|
151
|
+
:command => command,
|
152
|
+
:cmdline => cmdline,
|
153
|
+
:mem => rss,
|
154
|
+
:cpu => time,
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
# Process list on OS X or other unixes without a /proc.
|
159
|
+
def os_x_processes
|
160
|
+
raw = os_x_raw_ps.split("\n").slice(1, 99999)
|
161
|
+
raw.map do |line|
|
162
|
+
parse_ps(line)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# ps command used to generate list of processes on non-/proc unixes.
|
167
|
+
def os_x_raw_ps
|
168
|
+
`COLUMNS=9999 ps ax -o "pid uid rss cpu command"`
|
169
|
+
end
|
170
|
+
|
171
|
+
# Parse a single line of the ps command and return the values in a hash
|
172
|
+
# suitable for use in the Rush::Process#new.
|
173
|
+
def parse_ps(line)
|
174
|
+
m = line.split(" ", 5)
|
175
|
+
params = {}
|
176
|
+
params[:pid] = m[0]
|
177
|
+
params[:uid] = m[1]
|
178
|
+
params[:rss] = m[2]
|
179
|
+
params[:cpu] = m[3]
|
180
|
+
params[:cmdline] = m[4]
|
181
|
+
params[:command] = params[:cmdline].split(" ").first
|
182
|
+
params
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns true if the specified pid is running.
|
186
|
+
def process_alive(pid)
|
187
|
+
`ps -p #{pid} | wc -l`.to_i >= 2
|
188
|
+
end
|
189
|
+
|
190
|
+
# Terminate a process, by pid.
|
191
|
+
def kill_process(pid)
|
192
|
+
::Process.kill('TERM', pid.to_i)
|
193
|
+
end
|
194
|
+
|
195
|
+
####################################
|
196
|
+
|
197
|
+
# Raised when the action passed in by RushServer is not known.
|
198
|
+
class UnknownAction < Exception; end
|
199
|
+
|
200
|
+
# RushServer uses this method to transform a hash (:action plus parameters
|
201
|
+
# specific to that action type) into a method call on the connection. The
|
202
|
+
# returned value must be text so that it can be transmitted across the wire
|
203
|
+
# as an HTTP response.
|
204
|
+
def receive(params)
|
205
|
+
case params[:action]
|
206
|
+
when 'write_file' then write_file(params[:full_path], params[:payload])
|
207
|
+
when 'file_contents' then file_contents(params[:full_path])
|
208
|
+
when 'destroy' then destroy(params[:full_path])
|
209
|
+
when 'create_dir' then create_dir(params[:full_path])
|
210
|
+
when 'rename' then rename(params[:path], params[:name], params[:new_name])
|
211
|
+
when 'copy' then copy(params[:src], params[:dst])
|
212
|
+
when 'read_archive' then read_archive(params[:full_path])
|
213
|
+
when 'write_archive' then write_archive(params[:payload], params[:dir])
|
214
|
+
when 'index' then index(params[:base_path], params[:glob]).join("\n") + "\n"
|
215
|
+
when 'stat' then YAML.dump(stat(params[:full_path]))
|
216
|
+
when 'size' then size(params[:full_path])
|
217
|
+
when 'processes' then YAML.dump(processes)
|
218
|
+
when 'process_alive' then process_alive(params[:pid]) ? '1' : '0'
|
219
|
+
when 'kill_process' then kill_process(params[:pid])
|
220
|
+
else
|
221
|
+
raise UnknownAction
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|