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