aleksi-rush 0.6.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/README.rdoc +89 -0
  2. data/Rakefile +59 -0
  3. data/VERSION +1 -0
  4. data/bin/rush +13 -0
  5. data/bin/rushd +7 -0
  6. data/lib/rush.rb +87 -0
  7. data/lib/rush/access.rb +130 -0
  8. data/lib/rush/array_ext.rb +19 -0
  9. data/lib/rush/box.rb +115 -0
  10. data/lib/rush/commands.rb +55 -0
  11. data/lib/rush/config.rb +154 -0
  12. data/lib/rush/dir.rb +160 -0
  13. data/lib/rush/embeddable_shell.rb +26 -0
  14. data/lib/rush/entry.rb +189 -0
  15. data/lib/rush/exceptions.rb +39 -0
  16. data/lib/rush/file.rb +85 -0
  17. data/lib/rush/find_by.rb +39 -0
  18. data/lib/rush/fixnum_ext.rb +18 -0
  19. data/lib/rush/head_tail.rb +11 -0
  20. data/lib/rush/local.rb +398 -0
  21. data/lib/rush/process.rb +59 -0
  22. data/lib/rush/process_set.rb +62 -0
  23. data/lib/rush/remote.rb +158 -0
  24. data/lib/rush/search_results.rb +58 -0
  25. data/lib/rush/server.rb +117 -0
  26. data/lib/rush/shell.rb +187 -0
  27. data/lib/rush/ssh_tunnel.rb +122 -0
  28. data/lib/rush/string_ext.rb +3 -0
  29. data/spec/access_spec.rb +134 -0
  30. data/spec/array_ext_spec.rb +15 -0
  31. data/spec/base.rb +24 -0
  32. data/spec/box_spec.rb +80 -0
  33. data/spec/commands_spec.rb +47 -0
  34. data/spec/config_spec.rb +108 -0
  35. data/spec/dir_spec.rb +164 -0
  36. data/spec/embeddable_shell_spec.rb +17 -0
  37. data/spec/entry_spec.rb +133 -0
  38. data/spec/file_spec.rb +83 -0
  39. data/spec/find_by_spec.rb +58 -0
  40. data/spec/fixnum_ext_spec.rb +19 -0
  41. data/spec/local_spec.rb +364 -0
  42. data/spec/process_set_spec.rb +50 -0
  43. data/spec/process_spec.rb +73 -0
  44. data/spec/remote_spec.rb +140 -0
  45. data/spec/rush_spec.rb +28 -0
  46. data/spec/search_results_spec.rb +44 -0
  47. data/spec/shell_spec.rb +23 -0
  48. data/spec/ssh_tunnel_spec.rb +122 -0
  49. data/spec/string_ext_spec.rb +23 -0
  50. metadata +142 -0
data/lib/rush/shell.rb ADDED
@@ -0,0 +1,187 @@
1
+ require 'readline'
2
+
3
+ module Rush
4
+ # Rush::Shell is used to create an interactive shell. It is invoked by the rush binary.
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
@@ -0,0 +1,3 @@
1
+ class String
2
+ include Rush::HeadTail
3
+ end
@@ -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