adamwiggins-rush 0.6.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/README.rdoc +87 -0
- data/Rakefile +61 -0
- data/VERSION +1 -0
- data/bin/rush +13 -0
- data/bin/rushd +7 -0
- data/lib/rush/access.rb +130 -0
- data/lib/rush/array_ext.rb +19 -0
- data/lib/rush/box.rb +112 -0
- data/lib/rush/commands.rb +55 -0
- data/lib/rush/config.rb +154 -0
- data/lib/rush/dir.rb +160 -0
- data/lib/rush/embeddable_shell.rb +26 -0
- data/lib/rush/entry.rb +185 -0
- data/lib/rush/exceptions.rb +31 -0
- data/lib/rush/file.rb +85 -0
- data/lib/rush/find_by.rb +39 -0
- data/lib/rush/fixnum_ext.rb +18 -0
- data/lib/rush/head_tail.rb +11 -0
- data/lib/rush/local.rb +402 -0
- data/lib/rush/process.rb +59 -0
- data/lib/rush/process_set.rb +62 -0
- data/lib/rush/remote.rb +156 -0
- data/lib/rush/search_results.rb +58 -0
- data/lib/rush/server.rb +117 -0
- data/lib/rush/shell.rb +187 -0
- data/lib/rush/ssh_tunnel.rb +122 -0
- data/lib/rush/string_ext.rb +3 -0
- data/lib/rush.rb +87 -0
- data/spec/access_spec.rb +134 -0
- data/spec/array_ext_spec.rb +15 -0
- data/spec/base.rb +24 -0
- data/spec/box_spec.rb +64 -0
- data/spec/commands_spec.rb +47 -0
- data/spec/config_spec.rb +108 -0
- data/spec/dir_spec.rb +164 -0
- data/spec/embeddable_shell_spec.rb +17 -0
- data/spec/entry_spec.rb +133 -0
- data/spec/file_spec.rb +83 -0
- data/spec/find_by_spec.rb +58 -0
- data/spec/fixnum_ext_spec.rb +19 -0
- data/spec/local_spec.rb +352 -0
- data/spec/process_set_spec.rb +50 -0
- data/spec/process_spec.rb +73 -0
- data/spec/remote_spec.rb +140 -0
- data/spec/rush_spec.rb +28 -0
- data/spec/search_results_spec.rb +44 -0
- data/spec/shell_spec.rb +23 -0
- data/spec/ssh_tunnel_spec.rb +122 -0
- data/spec/string_ext_spec.rb +23 -0
- metadata +141 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
# An instance of this class is returned by Rush::Commands#search. It contains
|
2
|
+
# both the list of entries which matched the search, as well as the raw line
|
3
|
+
# matches. These methods get equivalent functionality to "grep -l" and "grep -h".
|
4
|
+
#
|
5
|
+
# SearchResults mixes in Rush::Commands so that you can chain multiple searches
|
6
|
+
# or do file operations on the resulting entries.
|
7
|
+
#
|
8
|
+
# Examples:
|
9
|
+
#
|
10
|
+
# myproj['**/*.rb'].search(/class/).entries.size
|
11
|
+
# myproj['**/*.rb'].search(/class/).lines.size
|
12
|
+
# myproj['**/*.rb'].search(/class/).copy_to other_dir
|
13
|
+
class Rush::SearchResults
|
14
|
+
attr_reader :entries, :lines, :entries_with_lines, :pattern
|
15
|
+
|
16
|
+
# Make a blank container. Track the pattern so that we can colorize the
|
17
|
+
# output to show what was matched.
|
18
|
+
def initialize(pattern)
|
19
|
+
# Duplication of data, but this lets us return everything in the exact
|
20
|
+
# order it was received.
|
21
|
+
@pattern = pattern
|
22
|
+
@entries = []
|
23
|
+
@entries_with_lines = {}
|
24
|
+
@lines = []
|
25
|
+
end
|
26
|
+
|
27
|
+
# Add a Rush::Entry and the array of string matches.
|
28
|
+
def add(entry, lines)
|
29
|
+
# this assumes that entry is unique
|
30
|
+
@entries << entry
|
31
|
+
@entries_with_lines[entry] = lines
|
32
|
+
@lines += lines
|
33
|
+
end
|
34
|
+
|
35
|
+
include Rush::Commands
|
36
|
+
|
37
|
+
def each(&block)
|
38
|
+
@entries.each(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
include Enumerable
|
42
|
+
|
43
|
+
def colorize(line)
|
44
|
+
lowlight + line.gsub(/(#{pattern.source})/, "#{hilight}\\1#{lowlight}") + normal
|
45
|
+
end
|
46
|
+
|
47
|
+
def hilight
|
48
|
+
"\e[34;1m"
|
49
|
+
end
|
50
|
+
|
51
|
+
def lowlight
|
52
|
+
"\e[37;2m"
|
53
|
+
end
|
54
|
+
|
55
|
+
def normal
|
56
|
+
"\e[0m"
|
57
|
+
end
|
58
|
+
end
|
data/lib/rush/server.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'mongrel'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
# Mongrel handler that translates the incoming HTTP request into a
|
6
|
+
# Rush::Connection::Local call. The results are sent back across the wire to
|
7
|
+
# be decoded by Rush::Connection::Remote on the other side.
|
8
|
+
class RushHandler < Mongrel::HttpHandler
|
9
|
+
def process(request, response)
|
10
|
+
params = {}
|
11
|
+
request.params['QUERY_STRING'].split("?").last.split("&").each do |tuple|
|
12
|
+
key, value = tuple.split("=")
|
13
|
+
params[key.to_sym] = value
|
14
|
+
end
|
15
|
+
|
16
|
+
unless authorize(request.params['HTTP_AUTHORIZATION'])
|
17
|
+
response.start(401) do |head, out|
|
18
|
+
end
|
19
|
+
else
|
20
|
+
payload = request.body.read
|
21
|
+
|
22
|
+
without_action = params
|
23
|
+
without_action.delete(params[:action])
|
24
|
+
|
25
|
+
msg = sprintf "%-20s", params[:action]
|
26
|
+
msg += without_action.inspect
|
27
|
+
msg += " + #{payload.size} bytes of payload" if payload.size > 0
|
28
|
+
log msg
|
29
|
+
|
30
|
+
params[:payload] = payload
|
31
|
+
|
32
|
+
begin
|
33
|
+
result = box.connection.receive(params)
|
34
|
+
|
35
|
+
response.start(200) do |head, out|
|
36
|
+
out.write result
|
37
|
+
end
|
38
|
+
rescue Rush::Exception => e
|
39
|
+
response.start(400) do |head, out|
|
40
|
+
out.write "#{e.class}\n#{e.message}\n"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
rescue Exception => e
|
45
|
+
log e.full_display
|
46
|
+
end
|
47
|
+
|
48
|
+
def authorize(auth)
|
49
|
+
unless m = auth.match(/^Basic (.+)$/)
|
50
|
+
log "Request with no authorization data"
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
|
54
|
+
decoded = Base64.decode64(m[1])
|
55
|
+
user, password = decoded.split(':', 2)
|
56
|
+
|
57
|
+
if user.nil? or user.length == 0 or password.nil? or password.length == 0
|
58
|
+
log "Malformed user or password"
|
59
|
+
return false
|
60
|
+
end
|
61
|
+
|
62
|
+
if password == config.passwords[user]
|
63
|
+
return true
|
64
|
+
else
|
65
|
+
log "Access denied to #{user}"
|
66
|
+
return false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def box
|
71
|
+
@box ||= Rush::Box.new('localhost')
|
72
|
+
end
|
73
|
+
|
74
|
+
def config
|
75
|
+
@config ||= Rush::Config.new
|
76
|
+
end
|
77
|
+
|
78
|
+
def log(msg)
|
79
|
+
File.open('rushd.log', 'a') do |f|
|
80
|
+
f.puts "#{Time.now.strftime('%Y-%m-%d %H:%I:%S')} :: #{msg}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# A container class to run the Mongrel server for rushd.
|
86
|
+
class RushServer
|
87
|
+
def run
|
88
|
+
host = "127.0.0.1"
|
89
|
+
port = Rush::Config::DefaultPort
|
90
|
+
|
91
|
+
rushd = RushHandler.new
|
92
|
+
rushd.log "rushd listening on #{host}:#{port}"
|
93
|
+
|
94
|
+
h = Mongrel::HttpServer.new(host, port)
|
95
|
+
h.register("/", rushd)
|
96
|
+
h.run.join
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class Exception
|
101
|
+
def full_display
|
102
|
+
out = []
|
103
|
+
out << "Exception #{self.class} => #{self}"
|
104
|
+
out << "Backtrace:"
|
105
|
+
out << self.filtered_backtrace.collect do |t|
|
106
|
+
" #{t}"
|
107
|
+
end
|
108
|
+
out << ""
|
109
|
+
out.join("\n")
|
110
|
+
end
|
111
|
+
|
112
|
+
def filtered_backtrace
|
113
|
+
backtrace.reject do |bt|
|
114
|
+
bt.match(/^\/usr\//)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/rush/shell.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
# Rush::Shell is used to create an interactive shell. It is invoked by the rush binary.
|
4
|
+
module Rush
|
5
|
+
class Shell
|
6
|
+
attr_accessor :suppress_output
|
7
|
+
# Set up the user's environment, including a pure binding into which
|
8
|
+
# env.rb and commands.rb are mixed.
|
9
|
+
def initialize
|
10
|
+
root = Rush::Dir.new('/')
|
11
|
+
home = Rush::Dir.new(ENV['HOME']) if ENV['HOME']
|
12
|
+
pwd = Rush::Dir.new(ENV['PWD']) if ENV['PWD']
|
13
|
+
|
14
|
+
@config = Rush::Config.new
|
15
|
+
|
16
|
+
@config.load_history.each do |item|
|
17
|
+
Readline::HISTORY.push(item)
|
18
|
+
end
|
19
|
+
|
20
|
+
Readline.basic_word_break_characters = ""
|
21
|
+
Readline.completion_append_character = nil
|
22
|
+
Readline.completion_proc = completion_proc
|
23
|
+
|
24
|
+
@box = Rush::Box.new
|
25
|
+
@pure_binding = @box.instance_eval "binding"
|
26
|
+
$last_res = nil
|
27
|
+
|
28
|
+
eval @config.load_env, @pure_binding
|
29
|
+
|
30
|
+
commands = @config.load_commands
|
31
|
+
Rush::Dir.class_eval commands
|
32
|
+
Array.class_eval commands
|
33
|
+
end
|
34
|
+
|
35
|
+
# Run a single command.
|
36
|
+
def execute(cmd)
|
37
|
+
res = eval(cmd, @pure_binding)
|
38
|
+
$last_res = res
|
39
|
+
eval("_ = $last_res", @pure_binding)
|
40
|
+
print_result res
|
41
|
+
rescue Rush::Exception => e
|
42
|
+
puts "Exception #{e.class} -> #{e.message}"
|
43
|
+
rescue ::Exception => e
|
44
|
+
puts "Exception #{e.class} -> #{e.message}"
|
45
|
+
e.backtrace.each do |t|
|
46
|
+
puts " #{::File.expand_path(t)}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Run the interactive shell using readline.
|
51
|
+
def run
|
52
|
+
loop do
|
53
|
+
cmd = Readline.readline('rush> ')
|
54
|
+
|
55
|
+
finish if cmd.nil? or cmd == 'exit'
|
56
|
+
next if cmd == ""
|
57
|
+
Readline::HISTORY.push(cmd)
|
58
|
+
|
59
|
+
execute(cmd)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Save history to ~/.rush/history when the shell exists.
|
64
|
+
def finish
|
65
|
+
@config.save_history(Readline::HISTORY.to_a)
|
66
|
+
puts
|
67
|
+
exit
|
68
|
+
end
|
69
|
+
|
70
|
+
# Nice printing of different return types, particularly Rush::SearchResults.
|
71
|
+
def print_result(res)
|
72
|
+
return if self.suppress_output
|
73
|
+
if res.kind_of? String
|
74
|
+
puts res
|
75
|
+
elsif res.kind_of? Rush::SearchResults
|
76
|
+
widest = res.entries.map { |k| k.full_path.length }.max
|
77
|
+
res.entries_with_lines.each do |entry, lines|
|
78
|
+
print entry.full_path
|
79
|
+
print ' ' * (widest - entry.full_path.length + 2)
|
80
|
+
print "=> "
|
81
|
+
print res.colorize(lines.first.strip.head(30))
|
82
|
+
print "..." if lines.first.strip.length > 30
|
83
|
+
if lines.size > 1
|
84
|
+
print " (plus #{lines.size - 1} more matches)"
|
85
|
+
end
|
86
|
+
print "\n"
|
87
|
+
end
|
88
|
+
puts "#{res.entries.size} matching files with #{res.lines.size} matching lines"
|
89
|
+
elsif res.respond_to? :each
|
90
|
+
counts = {}
|
91
|
+
res.each do |item|
|
92
|
+
puts item
|
93
|
+
counts[item.class] ||= 0
|
94
|
+
counts[item.class] += 1
|
95
|
+
end
|
96
|
+
if counts == {}
|
97
|
+
puts "=> (empty set)"
|
98
|
+
else
|
99
|
+
count_s = counts.map do |klass, count|
|
100
|
+
"#{count} x #{klass}"
|
101
|
+
end.join(', ')
|
102
|
+
puts "=> #{count_s}"
|
103
|
+
end
|
104
|
+
else
|
105
|
+
puts "=> #{res.inspect}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def path_parts(input) # :nodoc:
|
110
|
+
case input
|
111
|
+
when /((?:@{1,2}|\$|)\w+(?:\[[^\]]+\])*)([\[\/])(['"])([^\3]*)$/
|
112
|
+
$~.to_a.slice(1, 4).push($~.pre_match)
|
113
|
+
when /((?:@{1,2}|\$|)\w+(?:\[[^\]]+\])*)(\.)(\w*)$/
|
114
|
+
$~.to_a.slice(1, 3).push($~.pre_match)
|
115
|
+
when /((?:@{1,2}|\$|)\w+)$/
|
116
|
+
$~.to_a.slice(1, 1).push(nil).push($~.pre_match)
|
117
|
+
else
|
118
|
+
[ nil, nil, nil ]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def complete_method(receiver, dot, partial_name, pre)
|
123
|
+
path = eval("#{receiver}.full_path", @pure_binding) rescue nil
|
124
|
+
box = eval("#{receiver}.box", @pure_binding) rescue nil
|
125
|
+
if path and box
|
126
|
+
(box[path].methods - Object.methods).select do |e|
|
127
|
+
e.match(/^#{Regexp.escape(partial_name)}/)
|
128
|
+
end.map do |e|
|
129
|
+
(pre || '') + receiver + dot + e
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def complete_path(possible_var, accessor, quote, partial_path, pre) # :nodoc:
|
135
|
+
original_var, fixed_path = possible_var, ''
|
136
|
+
if /^(.+\/)([^\/]*)$/ === partial_path
|
137
|
+
fixed_path, partial_path = $~.captures
|
138
|
+
possible_var += "['#{fixed_path}']"
|
139
|
+
end
|
140
|
+
full_path = eval("#{possible_var}.full_path", @pure_binding) rescue nil
|
141
|
+
box = eval("#{possible_var}.box", @pure_binding) rescue nil
|
142
|
+
if full_path and box
|
143
|
+
Rush::Dir.new(full_path, box).entries.select do |e|
|
144
|
+
e.name.match(/^#{Regexp.escape(partial_path)}/)
|
145
|
+
end.map do |e|
|
146
|
+
(pre || '') + original_var + accessor + quote + fixed_path + e.name + (e.dir? ? "/" : "")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def complete_variable(partial_name, pre)
|
152
|
+
lvars = eval('local_variables', @pure_binding)
|
153
|
+
gvars = eval('global_variables', @pure_binding)
|
154
|
+
ivars = eval('instance_variables', @pure_binding)
|
155
|
+
(lvars + gvars + ivars).select do |e|
|
156
|
+
e.match(/^#{Regexp.escape(partial_name)}/)
|
157
|
+
end.map do |e|
|
158
|
+
(pre || '') + e
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Try to do tab completion on dir square brackets and slash accessors.
|
163
|
+
#
|
164
|
+
# Example:
|
165
|
+
#
|
166
|
+
# dir['subd # presing tab here will produce dir['subdir/ if subdir exists
|
167
|
+
# dir/'subd # presing tab here will produce dir/'subdir/ if subdir exists
|
168
|
+
#
|
169
|
+
# This isn't that cool yet, because it can't do multiple levels of subdirs.
|
170
|
+
# It does work remotely, though, which is pretty sweet.
|
171
|
+
def completion_proc
|
172
|
+
proc do |input|
|
173
|
+
receiver, accessor, *rest = path_parts(input)
|
174
|
+
if receiver
|
175
|
+
case accessor
|
176
|
+
when /^[\[\/]$/
|
177
|
+
complete_path(receiver, accessor, *rest)
|
178
|
+
when /^\.$/
|
179
|
+
complete_method(receiver, accessor, *rest)
|
180
|
+
when nil
|
181
|
+
complete_variable(receiver, *rest)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Internal class for managing an ssh tunnel, across which relatively insecure
|
2
|
+
# HTTP commands can be sent by Rush::Connection::Remote.
|
3
|
+
class Rush::SshTunnel
|
4
|
+
def initialize(real_host)
|
5
|
+
@real_host = real_host
|
6
|
+
end
|
7
|
+
|
8
|
+
def host
|
9
|
+
'localhost'
|
10
|
+
end
|
11
|
+
|
12
|
+
def port
|
13
|
+
@port
|
14
|
+
end
|
15
|
+
|
16
|
+
def ensure_tunnel(options={})
|
17
|
+
return if @port and tunnel_alive?
|
18
|
+
|
19
|
+
@port = config.tunnels[@real_host]
|
20
|
+
|
21
|
+
if !@port or !tunnel_alive?
|
22
|
+
setup_everything(options)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def setup_everything(options={})
|
27
|
+
display "Connecting to #{@real_host}..."
|
28
|
+
push_credentials
|
29
|
+
launch_rushd
|
30
|
+
establish_tunnel(options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def push_credentials
|
34
|
+
display "Pushing credentials"
|
35
|
+
config.ensure_credentials_exist
|
36
|
+
ssh_append_to_credentials(config.credentials_file.contents.strip)
|
37
|
+
end
|
38
|
+
|
39
|
+
def ssh_append_to_credentials(string)
|
40
|
+
# the following horror is exactly why rush is needed
|
41
|
+
passwords_file = "~/.rush/passwords"
|
42
|
+
string = "'#{string}'"
|
43
|
+
ssh "M=`grep #{string} #{passwords_file} 2>/dev/null | wc -l`; if [ $M = 0 ]; then mkdir -p .rush; chmod 700 .rush; echo #{string} >> #{passwords_file}; chmod 600 #{passwords_file}; fi"
|
44
|
+
end
|
45
|
+
|
46
|
+
def launch_rushd
|
47
|
+
display "Launching rushd"
|
48
|
+
ssh("if [ `ps aux | grep rushd | grep -v grep | wc -l` -ge 1 ]; then exit; fi; rushd > /dev/null 2>&1 &")
|
49
|
+
end
|
50
|
+
|
51
|
+
def establish_tunnel(options={})
|
52
|
+
display "Establishing ssh tunnel"
|
53
|
+
@port = next_available_port
|
54
|
+
|
55
|
+
make_ssh_tunnel(options)
|
56
|
+
|
57
|
+
tunnels = config.tunnels
|
58
|
+
tunnels[@real_host] = @port
|
59
|
+
config.save_tunnels tunnels
|
60
|
+
|
61
|
+
sleep 0.5
|
62
|
+
end
|
63
|
+
|
64
|
+
def tunnel_options
|
65
|
+
{
|
66
|
+
:local_port => @port,
|
67
|
+
:remote_port => Rush::Config::DefaultPort,
|
68
|
+
:ssh_host => @real_host,
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def tunnel_alive?
|
73
|
+
`#{tunnel_count_command}`.to_i > 0
|
74
|
+
end
|
75
|
+
|
76
|
+
def tunnel_count_command
|
77
|
+
"ps x | grep '#{ssh_tunnel_command_without_stall}' | grep -v grep | wc -l"
|
78
|
+
end
|
79
|
+
|
80
|
+
class SshFailed < Exception; end
|
81
|
+
class NoPortSelectedYet < Exception; end
|
82
|
+
|
83
|
+
def ssh(command)
|
84
|
+
raise SshFailed unless system("ssh #{@real_host} '#{command}'")
|
85
|
+
end
|
86
|
+
|
87
|
+
def make_ssh_tunnel(options={})
|
88
|
+
raise SshFailed unless system(ssh_tunnel_command(options))
|
89
|
+
end
|
90
|
+
|
91
|
+
def ssh_tunnel_command_without_stall
|
92
|
+
options = tunnel_options
|
93
|
+
raise NoPortSelectedYet unless options[:local_port]
|
94
|
+
"ssh -f -L #{options[:local_port]}:127.0.0.1:#{options[:remote_port]} #{options[:ssh_host]}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def ssh_stall_command(options={})
|
98
|
+
if options[:timeout] == :infinite
|
99
|
+
"while [ 1 ]; do sleep 1000; done"
|
100
|
+
elsif options[:timeout].to_i > 10
|
101
|
+
"sleep #{options[:timeout].to_i}"
|
102
|
+
else
|
103
|
+
"sleep 9000"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def ssh_tunnel_command(options={})
|
108
|
+
ssh_tunnel_command_without_stall + ' "' + ssh_stall_command(options) + '"'
|
109
|
+
end
|
110
|
+
|
111
|
+
def next_available_port
|
112
|
+
(config.tunnels.values.max || Rush::Config::DefaultPort) + 1
|
113
|
+
end
|
114
|
+
|
115
|
+
def config
|
116
|
+
@config ||= Rush::Config.new
|
117
|
+
end
|
118
|
+
|
119
|
+
def display(msg)
|
120
|
+
puts msg
|
121
|
+
end
|
122
|
+
end
|
data/lib/rush.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
# The top-level Rush module has some convenience methods for accessing the
|
4
|
+
# local box.
|
5
|
+
module Rush
|
6
|
+
# Access the root filesystem of the local box. Example:
|
7
|
+
#
|
8
|
+
# Rush['/etc/hosts'].contents
|
9
|
+
#
|
10
|
+
def self.[](key)
|
11
|
+
box[key]
|
12
|
+
end
|
13
|
+
|
14
|
+
# Create a dir object from the path of a provided file. Example:
|
15
|
+
#
|
16
|
+
# Rush.dir(__FILE__).files
|
17
|
+
#
|
18
|
+
def self.dir(filename)
|
19
|
+
box[::File.expand_path(::File.dirname(filename)) + '/']
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create a dir object based on the shell's current working directory at the
|
23
|
+
# time the program was run. Example:
|
24
|
+
#
|
25
|
+
# Rush.launch_dir.files
|
26
|
+
#
|
27
|
+
def self.launch_dir
|
28
|
+
box[::Dir.pwd + '/']
|
29
|
+
end
|
30
|
+
|
31
|
+
# Run a bash command in the root of the local machine. Equivalent to
|
32
|
+
# Rush::Box.new.bash.
|
33
|
+
def self.bash(command, options={})
|
34
|
+
box.bash(command, options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Pull the process list for the local machine. Example:
|
38
|
+
#
|
39
|
+
# Rush.processes.filter(:cmdline => /ruby/)
|
40
|
+
#
|
41
|
+
def self.processes
|
42
|
+
box.processes
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get the process object for this program's PID. Example:
|
46
|
+
#
|
47
|
+
# puts "I'm using #{Rush.my_process.mem} blocks of memory"
|
48
|
+
#
|
49
|
+
def self.my_process
|
50
|
+
box.processes.filter(:pid => ::Process.pid).first
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create a box object for localhost.
|
54
|
+
def self.box
|
55
|
+
@@box = Rush::Box.new
|
56
|
+
end
|
57
|
+
|
58
|
+
# Quote a path for use in backticks, say.
|
59
|
+
def self.quote(path)
|
60
|
+
path.gsub(/(?=[^a-zA-Z0-9_.\/\-\x7F-\xFF\n])/n, '\\').gsub(/\n/, "'\n'").sub(/^$/, "''")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Rush::Connection; end
|
65
|
+
|
66
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
67
|
+
|
68
|
+
require 'rush/exceptions'
|
69
|
+
require 'rush/config'
|
70
|
+
require 'rush/commands'
|
71
|
+
require 'rush/access'
|
72
|
+
require 'rush/entry'
|
73
|
+
require 'rush/file'
|
74
|
+
require 'rush/dir'
|
75
|
+
require 'rush/search_results'
|
76
|
+
require 'rush/head_tail'
|
77
|
+
require 'rush/find_by'
|
78
|
+
require 'rush/string_ext'
|
79
|
+
require 'rush/fixnum_ext'
|
80
|
+
require 'rush/array_ext'
|
81
|
+
require 'rush/process'
|
82
|
+
require 'rush/process_set'
|
83
|
+
require 'rush/local'
|
84
|
+
require 'rush/remote'
|
85
|
+
require 'rush/ssh_tunnel'
|
86
|
+
require 'rush/box'
|
87
|
+
require 'rush/embeddable_shell'
|
data/spec/access_spec.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
describe Rush::Access do
|
4
|
+
before do
|
5
|
+
@access = Rush::Access.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "has roles: user, group, other" do
|
9
|
+
@access.class.roles == %w(user group other)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "has permissions: read, write, execute" do
|
13
|
+
@access.class.permissions == %w(read write execute)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "gets parts from a one-part symbol like :user" do
|
17
|
+
@access.parts_from(:user).should == %w(user)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "gets parts from a two-part symbol like :read_write" do
|
21
|
+
@access.parts_from(:read_write).should == %w(read write)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "allows use of 'and' in multipart symbols, like :user_and_group" do
|
25
|
+
@access.parts_from(:user_and_group).should == %w(user group)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "extract_list verifies that all the parts among the valid choices" do
|
29
|
+
@access.should_receive(:parts_from).with(:red_green).and_return(%w(red green))
|
30
|
+
@access.extract_list('type', :red_green, %w(red blue green)).should == %w(red green)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "extract_list raises a BadAccessSpecifier when there is part not in the list of choices" do
|
34
|
+
lambda do
|
35
|
+
@access.extract_list('role', :user_bork, %w(user group))
|
36
|
+
end.should raise_error(Rush::BadAccessSpecifier, "Unrecognized role: bork")
|
37
|
+
end
|
38
|
+
|
39
|
+
it "sets one value in the matrix of permissions and roles" do
|
40
|
+
@access.set_matrix(%w(read), %w(user))
|
41
|
+
@access.user_can_read.should == true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "sets two values in the matrix of permissions and roles" do
|
45
|
+
@access.set_matrix(%w(read), %w(user group))
|
46
|
+
@access.user_can_read.should == true
|
47
|
+
@access.group_can_read.should == true
|
48
|
+
end
|
49
|
+
|
50
|
+
it "sets four values in the matrix of permissions and roles" do
|
51
|
+
@access.set_matrix(%w(read write), %w(user group))
|
52
|
+
@access.user_can_read.should == true
|
53
|
+
@access.group_can_read.should == true
|
54
|
+
@access.user_can_write.should == true
|
55
|
+
@access.group_can_write.should == true
|
56
|
+
end
|
57
|
+
|
58
|
+
it "parse options hash" do
|
59
|
+
@access.parse(:user_can => :read)
|
60
|
+
@access.user_can_read.should == true
|
61
|
+
end
|
62
|
+
|
63
|
+
it "generates octal permissions from its member vars" do
|
64
|
+
@access.user_can_read = true
|
65
|
+
@access.octal_permissions.should == 0400
|
66
|
+
end
|
67
|
+
|
68
|
+
it "generates octal permissions from its member vars" do
|
69
|
+
@access.user_can_read = true
|
70
|
+
@access.user_can_write = true
|
71
|
+
@access.user_can_execute = true
|
72
|
+
@access.group_can_read = true
|
73
|
+
@access.group_can_execute = true
|
74
|
+
@access.octal_permissions.should == 0750
|
75
|
+
end
|
76
|
+
|
77
|
+
it "applies its settings to a file" do
|
78
|
+
file = "/tmp/rush_spec_#{Process.pid}"
|
79
|
+
begin
|
80
|
+
system "rm -rf #{file}; touch #{file}; chmod 770 #{file}"
|
81
|
+
@access.user_can_read = true
|
82
|
+
@access.apply(file)
|
83
|
+
`ls -l #{file}`.should match(/^-r--------/)
|
84
|
+
ensure
|
85
|
+
system "rm -rf #{file}; touch #{file}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
it "serializes itself to a hash" do
|
90
|
+
@access.user_can_read = true
|
91
|
+
@access.to_hash.should == {
|
92
|
+
:user_can_read => 1, :user_can_write => 0, :user_can_execute => 0,
|
93
|
+
:group_can_read => 0, :group_can_write => 0, :group_can_execute => 0,
|
94
|
+
:other_can_read => 0, :other_can_write => 0, :other_can_execute => 0,
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
it "unserializes from a hash" do
|
99
|
+
@access.from_hash(:user_can_read => '1')
|
100
|
+
@access.user_can_read.should == true
|
101
|
+
end
|
102
|
+
|
103
|
+
it "initializes from a serialized hash" do
|
104
|
+
@access.class.should_receive(:new).and_return(@access)
|
105
|
+
@access.class.from_hash(:user_can_read => '1').should == @access
|
106
|
+
@access.user_can_read.should == true
|
107
|
+
end
|
108
|
+
|
109
|
+
it "initializes from a parsed options hash" do
|
110
|
+
@access.class.should_receive(:new).and_return(@access)
|
111
|
+
@access.class.parse(:user_and_group_can => :read).should == @access
|
112
|
+
@access.user_can_read.should == true
|
113
|
+
end
|
114
|
+
|
115
|
+
it "converts and octal integer into an array of integers" do
|
116
|
+
@access.octal_integer_array(0740).should == [ 7, 4, 0 ]
|
117
|
+
end
|
118
|
+
|
119
|
+
it "filters out anything above the top three digits (File.stat returns some extra data there)" do
|
120
|
+
@access.octal_integer_array(0100644).should == [ 6, 4, 4 ]
|
121
|
+
end
|
122
|
+
|
123
|
+
it "taskes permissions from an octal representation" do
|
124
|
+
@access.from_octal(0644)
|
125
|
+
@access.user_can_read.should == true
|
126
|
+
@access.user_can_write.should == true
|
127
|
+
@access.user_can_execute.should == false
|
128
|
+
end
|
129
|
+
|
130
|
+
it "computes a display hash by dropping false keys and converting the 1s to trues" do
|
131
|
+
@access.should_receive(:to_hash).and_return(:red => 1, :green => 0, :blue => 1)
|
132
|
+
@access.display_hash.should == { :red => true, :blue => true }
|
133
|
+
end
|
134
|
+
end
|