wesabe-robot-army 0.1.1 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'robot-army')
3
+
4
+ class Whoami < RobotArmy::TaskMaster
5
+ desc 'test', "Tests whoami"
6
+ method_options :root => :boolean, :host => :string
7
+ def test(options={})
8
+ self.host = options['host']
9
+ puts options['root'] ?
10
+ sudo{ `whoami` } :
11
+ remote{ `whoami` }
12
+ end
13
+ end
data/lib/robot-army.rb CHANGED
@@ -2,29 +2,35 @@ require 'rubygems'
2
2
  require 'open3'
3
3
  require 'base64'
4
4
  require 'thor'
5
- gem 'ruby2ruby', '=1.1.9'
5
+
6
+ gem 'ParseTree', '>=3'
7
+ require 'parse_tree'
8
+
9
+ gem 'ruby2ruby', '>=1.2.0'
6
10
  require 'ruby2ruby'
11
+ require 'parse_tree_extensions'
12
+
7
13
  require 'fileutils'
8
14
 
9
15
  module RobotArmy
10
16
  # Gets the upstream messenger.
11
- #
17
+ #
12
18
  # @return [RobotArmy::Messenger]
13
19
  # A messenger connection pointing upstream.
14
- #
20
+ #
15
21
  def self.upstream
16
22
  @upstream
17
23
  end
18
-
24
+
19
25
  # Sets the upstream messenger.
20
- #
26
+ #
21
27
  # @param messenger [RobotArmy::Messenger]
22
28
  # A messenger connection pointing upstream.
23
- #
29
+ #
24
30
  def self.upstream=(messenger)
25
31
  @upstream = messenger
26
32
  end
27
-
33
+
28
34
  class ConnectionNotOpen < StandardError; end
29
35
  class Warning < StandardError; end
30
36
  class HostArityError < StandardError; end
@@ -35,41 +41,61 @@ module RobotArmy
35
41
  end
36
42
  class RobotArmy::Exit < Exception
37
43
  attr_accessor :status
38
-
44
+
39
45
  def initialize(status=0)
40
46
  @status = status
41
47
  end
42
48
  end
43
-
49
+
44
50
  CHARACTERS = %w[a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9]
45
-
51
+
46
52
  # Generates a random string of lowercase letters and numbers.
47
- #
53
+ #
48
54
  # @param length [Fixnum]
49
55
  # The length of the string to generate.
50
- #
56
+ #
51
57
  # @return [String]
52
58
  # The random string.
53
- #
59
+ #
54
60
  def self.random_string(length=16)
55
61
  (0...length).map{ CHARACTERS[rand(CHARACTERS.size)] }.join
56
62
  end
57
- end
58
63
 
59
- %w[loader dependency_loader io
60
- officer_loader soldier officer
61
- messenger task_master proxy
62
- connection officer_connection
63
- marshal_ext gate_keeper ruby2ruby_ext].each do |file|
64
- require File.join(File.dirname(__FILE__), 'robot-army', file)
64
+ def self.ask_for_password(host, data={})
65
+ require 'highline'
66
+ HighLine.new.ask("[sudo] password for #{data[:user]}@#{host}: ") {|q| q.echo = false}
67
+ end
65
68
  end
66
69
 
70
+ $LOAD_PATH << File.dirname(__FILE__)
71
+
72
+ require 'robot-army/loader'
73
+ require 'robot-army/dependency_loader'
74
+ require 'robot-army/io'
75
+ require 'robot-army/officer_loader'
76
+ require 'robot-army/soldier'
77
+ require 'robot-army/officer'
78
+ require 'robot-army/messenger'
79
+ require 'robot-army/task_master'
80
+ require 'robot-army/proxy'
81
+ require 'robot-army/eval_builder'
82
+ require 'robot-army/eval_command'
83
+ require 'robot-army/remote_evaler'
84
+ require 'robot-army/keychain'
85
+ require 'robot-army/connection'
86
+ require 'robot-army/officer_connection'
87
+ require 'robot-army/marshal_ext'
88
+ require 'robot-army/gate_keeper'
89
+ require 'robot-army/at_exit'
90
+ require 'robot-army/ruby2ruby_ext'
91
+
67
92
  at_exit do
93
+ RobotArmy::AtExit.shared_instance.do_exit
68
94
  RobotArmy::GateKeeper.shared_instance.close
69
95
  end
70
96
 
71
97
  def debug(*whatever)
72
98
  File.open('/tmp/robot-army.log', 'a') do |f|
73
99
  f.puts "[#{Process.pid}] #{whatever.join(' ')}"
74
- end if $TESTING
100
+ end if $TESTING || $ROBOT_ARMY_DEBUG
75
101
  end
@@ -0,0 +1,19 @@
1
+ class RobotArmy::AtExit
2
+ def at_exit(&block)
3
+ callbacks << block
4
+ end
5
+
6
+ def do_exit
7
+ callbacks.pop.call while callbacks.last
8
+ end
9
+
10
+ def self.shared_instance
11
+ @shared_instance ||= new
12
+ end
13
+
14
+ private
15
+
16
+ def callbacks
17
+ @callbacks ||= []
18
+ end
19
+ end
@@ -0,0 +1,84 @@
1
+ class RobotArmy::EvalBuilder
2
+ def self.build(command)
3
+ new.build(command)
4
+ end
5
+
6
+ def build(command)
7
+ proc, procargs, context, dependencies =
8
+ command.proc, command.args, command.context, command.dependencies
9
+
10
+ options = {}
11
+ proxies = {context.hash => context}
12
+
13
+ # fix stack traces
14
+ file, line = eval('[__FILE__, __LINE__]', proc.binding)
15
+
16
+ # include local variables
17
+ local_variables = eval('local_variables', proc.binding)
18
+ locals, lproxies = dump_values(local_variables) { |name,| eval(name, proc.binding) }
19
+ proxies.merge! lproxies
20
+
21
+ # include arguments
22
+ args, aproxies = dump_values(proc.arguments) { |_, i| procargs[i] }
23
+ proxies.merge! aproxies
24
+
25
+ # include dependency loader
26
+ dep_loading = "Marshal.load(#{Marshal.dump(dependencies).inspect}).load!"
27
+
28
+ # get the code for the proc
29
+ proc = "proc{ #{proc.to_ruby_without_proc_wrapper} }"
30
+ messenger = "RobotArmy::Messenger.new($stdin, $stdout)"
31
+ context = "RobotArmy::Proxy.new(#{messenger}, #{context.hash.inspect})"
32
+
33
+ code = %{
34
+ #{dep_loading} # load dependencies
35
+ #{(locals+args).join("\n")} # all local variables
36
+ #{context}.__proxy_instance_eval(&#{proc}) # run the block
37
+ }
38
+
39
+ options[:file] = file
40
+ options[:line] = line
41
+ options[:code] = code
42
+ options[:user] = command.user if command.user
43
+
44
+ return options, proxies
45
+ end
46
+
47
+ private
48
+
49
+ # Dumps the values associated with the given names for transport.
50
+ #
51
+ # @param names [Array[String]]
52
+ # The names of the variables to dump.
53
+ #
54
+ # @yield [name, index]
55
+ # Yields the name and its index and expects
56
+ # to get the corresponding value.
57
+ #
58
+ # @yieldparam [String] name
59
+ # The name of the value for the block to return.
60
+ #
61
+ # @yieldparam [Fixnum] index
62
+ # The index of the value for the block to return.
63
+ #
64
+ # @return [(Array[Object], Hash[Fixnum => Object])]
65
+ # The pair +values+ and +proxies+.
66
+ #
67
+ def dump_values(names)
68
+ proxies = {}
69
+ values = []
70
+
71
+ names.each_with_index do |name, i|
72
+ value = yield name, i
73
+ if value.marshalable?
74
+ dump = Marshal.dump(value)
75
+ values << "#{name} = Marshal.load(#{dump.inspect})"
76
+ else
77
+ proxies[value.hash] = value
78
+ values << "#{name} = #{RobotArmy::Proxy.generator_for(value)}"
79
+ end
80
+ end
81
+
82
+ return values, proxies
83
+ end
84
+ end
@@ -0,0 +1,17 @@
1
+ class RobotArmy::EvalCommand
2
+ def initialize
3
+ yield self if block_given?
4
+ end
5
+
6
+ attr_accessor :user
7
+
8
+ attr_accessor :proc
9
+
10
+ attr_accessor :args
11
+
12
+ attr_accessor :context
13
+
14
+ attr_accessor :dependencies
15
+
16
+ attr_accessor :keychain
17
+ end
@@ -0,0 +1,10 @@
1
+ class RobotArmy::Keychain
2
+ def get_password_for_user_on_host(user, host)
3
+ read_with_prompt("[sudo] password for #{user}@#{host}: ")
4
+ end
5
+
6
+ def read_with_prompt(prompt)
7
+ require 'highline'
8
+ HighLine.new.ask(prompt) {|q| q.echo = false}
9
+ end
10
+ end
@@ -1,17 +1,17 @@
1
1
  module Marshal
2
2
  class <<self
3
3
  # Determines whether a given object can be dumped.
4
- #
4
+ #
5
5
  # @param object Object
6
6
  # The object to check.
7
- #
7
+ #
8
8
  # @return [Boolean]
9
- # +true+ if dumping the object does not raise an error,
9
+ # +true+ if dumping the object does not raise an error,
10
10
  # +false+ if a +TypeError+ is raised.
11
- #
11
+ #
12
12
  # @raise Exception
13
13
  # Whatever +Marshal.dump+ might raise that isn't a +TypeError+.
14
- #
14
+ #
15
15
  def can_dump?(object)
16
16
  begin
17
17
  Marshal.dump(object)
@@ -25,9 +25,28 @@ end
25
25
 
26
26
  class Object
27
27
  # Syntactic sugar for +Marshal.can_dump?+.
28
- #
28
+ #
29
29
  # @see Marshal.can_dump?
30
30
  def marshalable?
31
- Marshal.can_dump?(self)
31
+ Marshal.can_dump?(self) &&
32
+ instance_variables.all? do |ivar|
33
+ instance_variable_get(ivar).marshalable?
34
+ end
35
+ end
36
+ end
37
+
38
+ class Array
39
+ def marshalable?
40
+ super &&
41
+ all? {|item| item.marshalable?}
42
+ end
43
+ end
44
+
45
+ class Hash
46
+ def marshalable?
47
+ super &&
48
+ keys.marshalable? &&
49
+ values.marshalable? &&
50
+ default.marshalable?
32
51
  end
33
52
  end
@@ -0,0 +1,59 @@
1
+ class RobotArmy::RemoteEvaler
2
+ attr_reader :connection, :command, :options, :proxies
3
+
4
+ def initialize(connection, command)
5
+ @connection = connection
6
+ @command = command
7
+ end
8
+
9
+ def execute_command
10
+ @options, @proxies = RobotArmy::EvalBuilder.build(command)
11
+ send_eval_command
12
+ return loop_until_done
13
+ end
14
+
15
+ private
16
+
17
+ def send_eval_command
18
+ debug("Evaling code remotely:\n#{options[:code]}")
19
+ connection.post(:command => :eval, :data => options)
20
+ end
21
+
22
+ def loop_until_done
23
+ catch :done do
24
+ loop { process_response(connection.messenger.get) }
25
+ end
26
+ end
27
+
28
+ def process_response(response)
29
+ case response[:status]
30
+ when 'proxy'
31
+ handle_proxy_response(response)
32
+ when 'password'
33
+ debug("Got password request: #{response[:data].inspect}")
34
+ connection.post :status => 'ok', :data => command.keychain.get_password_for_user_on_host(response[:data][:user], connection.host)
35
+ else
36
+ begin
37
+ throw :done, connection.handle_response(response)
38
+ rescue RobotArmy::Warning => e
39
+ $stderr.puts "WARNING: #{e.message}"
40
+ throw :done, nil
41
+ end
42
+ end
43
+ end
44
+
45
+ def handle_proxy_response(response)
46
+ begin
47
+ proxy = proxies[response[:data][:hash]]
48
+ data = proxy.send(*response[:data][:call])
49
+ if data.marshalable?
50
+ connection.post :status => 'ok', :data => data
51
+ else
52
+ proxies[data.hash] = data
53
+ connection.post :status => 'proxy', :data => data.hash
54
+ end
55
+ rescue Object => e
56
+ connection.post :status => 'error', :data => e
57
+ end
58
+ end
59
+ end
@@ -2,30 +2,18 @@ class Proc
2
2
  def arguments
3
3
  (to_ruby[/\Aproc \{ \|([^\|]+)\|/, 1] || '').split(/\s*,\s*/)
4
4
  end
5
-
6
- def to_ruby_with_body_flag(only_body=false)
7
- only_body ? to_method.to_ruby(true) : to_ruby_without_body_flag
5
+
6
+ def to_ruby_without_proc_wrapper
7
+ to_ruby[/\Aproc\s*\{\s*(\|[^\|]+\|)?\s*(.*?)\s*\}\Z/m, 2] || raise("Unable to parse proc's Ruby: #{to_ruby}")
8
8
  end
9
-
10
- alias :to_ruby_without_body_flag :to_ruby
11
- alias :to_ruby :to_ruby_with_body_flag
12
9
  end
13
10
 
14
11
  class Method
15
12
  def arguments
16
13
  (to_ruby[/\A(def [^\s\(]+)(?:\(([^\)]*)\))?/, 2] || '').split(/\s*,\s*/)
17
14
  end
18
-
19
- def to_ruby_with_body_flag(only_body=false)
20
- ruby = self.to_ruby_without_body_flag
21
- if only_body
22
- ruby.sub!(/\A(def [^\s\(]+)(?:\(([^\)]*)\))?/, '') # move args
23
- ruby.sub!(/end\Z/, '') # strip end
24
- end
25
- ruby.gsub!(/\s+$/, '') # trailing WS bugs me
26
- ruby
15
+
16
+ def to_ruby_without_method_declaration
17
+ to_ruby[/\Adef [^\s\(]+(?:\([^\)]*\))?\s*(.*?)\s*end\Z/m, 1] || raise("Unable to parse method's Ruby: #{to_ruby}")
27
18
  end
28
-
29
- alias :to_ruby_without_body_flag :to_ruby
30
- alias :to_ruby :to_ruby_with_body_flag
31
19
  end
@@ -1,46 +1,49 @@
1
1
  module RobotArmy
2
2
  # The place where the magic happens
3
- #
3
+ #
4
4
  # ==== Types (shortcuts for use in this file)
5
5
  # HostList:: <Array[String], String, nil>
6
6
  class TaskMaster < Thor
7
+
8
+ no_tasks do
9
+
7
10
  def initialize(*args)
8
11
  super
9
12
  @dep_loader = DependencyLoader.new
10
13
  end
11
-
14
+
12
15
  # Gets or sets a single host that instances of +RobotArmy::TaskMaster+ subclasses will use.
13
- #
16
+ #
14
17
  # @param host [String, :localhost]
15
18
  # The fully-qualified domain name to connect to.
16
- #
19
+ #
17
20
  # @return [String, :localhost]
18
21
  # The current value for the host.
19
- #
22
+ #
20
23
  # @raise RobotArmy::HostArityError
21
- # If you're using the getter form of this method and you've already
24
+ # If you're using the getter form of this method and you've already
22
25
  # set multiple hosts, an error will be raised.
23
- #
26
+ #
24
27
  def self.host(host=nil)
25
28
  if host
26
29
  @hosts = nil
27
30
  @host = host
28
31
  elsif @hosts
29
- raise RobotArmy::HostArityError,
32
+ raise RobotArmy::HostArityError,
30
33
  "There are #{@hosts.size} hosts, so calling host doesn't make sense"
31
34
  else
32
35
  @host
33
36
  end
34
37
  end
35
-
38
+
36
39
  # Gets or sets the hosts that instances of +RobotArmy::TaskMaster+ subclasses will use.
37
- #
40
+ #
38
41
  # @param hosts [Array[String]]
39
42
  # A list of fully-qualified domain names to connect to.
40
- #
43
+ #
41
44
  # @return [Array[String]]
42
45
  # The current list of hosts.
43
- #
46
+ #
44
47
  def self.hosts(hosts=nil)
45
48
  if hosts
46
49
  @host = nil
@@ -51,42 +54,42 @@ module RobotArmy
51
54
  @hosts || []
52
55
  end
53
56
  end
54
-
57
+
55
58
  # Gets the first host for this instance of +RobotArmy::TaskMaster+.
56
- #
59
+ #
57
60
  # @return [String, :localhost]
58
61
  # The host value to use.
59
- #
62
+ #
60
63
  # @raise RobotArmy::HostArityError
61
- # If you're using the getter form of this method and you've already
64
+ # If you're using the getter form of this method and you've already
62
65
  # set multiple hosts, an error will be raised.
63
- #
66
+ #
64
67
  def host
65
68
  if @host
66
69
  @host
67
70
  elsif @hosts
68
- raise RobotArmy::HostArityError,
71
+ raise RobotArmy::HostArityError,
69
72
  "There are #{@hosts.size} hosts, so calling host doesn't make sense"
70
73
  else
71
74
  self.class.host
72
75
  end
73
76
  end
74
-
77
+
75
78
  # Sets a single host for this instance of +RobotArmy::TaskMaster+.
76
- #
79
+ #
77
80
  # @param host [String, :localhost]
78
81
  # The host value to use.
79
- #
82
+ #
80
83
  def host=(host)
81
84
  @hosts = nil
82
85
  @host = host
83
86
  end
84
-
87
+
85
88
  # Gets the hosts for the instance of +RobotArmy::TaskMaster+.
86
- #
89
+ #
87
90
  # @return [Array[String]]
88
91
  # A list of hosts.
89
- #
92
+ #
90
93
  def hosts
91
94
  if @hosts
92
95
  @hosts
@@ -96,104 +99,104 @@ module RobotArmy
96
99
  self.class.hosts
97
100
  end
98
101
  end
99
-
102
+
100
103
  # Sets the hosts for this instance of +RobotArmy::TaskMaster+.
101
- #
104
+ #
102
105
  # @param hosts [Array[String]]
103
106
  # A list of hosts.
104
- #
107
+ #
105
108
  def hosts=(hosts)
106
109
  @host = nil
107
110
  @hosts = hosts
108
111
  end
109
-
112
+
110
113
  # Gets an open connection for the host this instance is configured to use.
111
- #
114
+ #
112
115
  # @return RobotArmy::Connection
113
116
  # An open connection with an active Ruby process.
114
- #
117
+ #
115
118
  def connection(host)
116
119
  RobotArmy::GateKeeper.shared_instance.connect(host)
117
120
  end
118
-
119
- # Runs a block of Ruby on the machine specified by a host string as root
121
+
122
+ # Runs a block of Ruby on the machine specified by a host string as root
120
123
  # and returns the return value of the block. Example:
121
- #
124
+ #
122
125
  # sudo { `shutdown -r now` }
123
- #
126
+ #
124
127
  # You may also specify a user other than root. In this case +sudo+ is the
125
128
  # same as +remote+:
126
- #
129
+ #
127
130
  # sudo(:user => 'www-data') { `/etc/init.d/apache2 restart` }
128
- #
131
+ #
129
132
  # @param host [String, :localhost]
130
- # The fully-qualified domain name of the machine to connect to, or
133
+ # The fully-qualified domain name of the machine to connect to, or
131
134
  # +:localhost+ if you want to use the same machine.
132
- #
135
+ #
133
136
  # @options options
134
137
  # :user -> String => shell user
135
- #
138
+ #
136
139
  # @raise Exception
137
140
  # Whatever is raised by the block.
138
- #
141
+ #
139
142
  # @return [Object]
140
143
  # Whatever is returned by the block.
141
- #
144
+ #
142
145
  # @see remote
143
146
  def sudo(hosts=self.hosts, options={}, &proc)
144
147
  options, hosts = hosts, self.hosts if hosts.is_a?(Hash)
145
148
  remote hosts, {:user => 'root'}.merge(options), &proc
146
149
  end
147
-
148
- # Runs a block of Ruby on the machine specified by a host string and
150
+
151
+ # Runs a block of Ruby on the machine specified by a host string and
149
152
  # returns the return value of the block. Example:
150
- #
153
+ #
151
154
  # remote { "foo" } # => "foo"
152
- #
153
- # Local variables accessible from the block are also passed along to the
155
+ #
156
+ # Local variables accessible from the block are also passed along to the
154
157
  # remote process:
155
- #
158
+ #
156
159
  # foo = "bar"
157
160
  # remote { foo } # => "bar"
158
- #
159
- # Objects which can't be marshalled, such as IO streams, will be proxied
161
+ #
162
+ # Objects which can't be marshalled, such as IO streams, will be proxied
160
163
  # instead:
161
- #
164
+ #
162
165
  # file = File.open("README.markdown", "r")
163
166
  # remote { file.gets } # => "Robot Army\n"
164
- #
167
+ #
165
168
  # @param hosts [HostList]
166
169
  # Which hosts to run the block on.
167
- #
170
+ #
168
171
  # @options options
169
172
  # :user -> String => shell user
170
- #
173
+ #
171
174
  # @raise Exception
172
175
  # Whatever is raised by the block.
173
- #
176
+ #
174
177
  # @return [Object]
175
178
  # Whatever is returned by the block.
176
- #
179
+ #
177
180
  def remote(hosts=self.hosts, options={}, &proc)
178
181
  options, hosts = hosts, self.hosts if hosts.is_a?(Hash)
179
182
  results = Array(hosts).map {|host| remote_eval({:host => host}.merge(options), &proc) }
180
183
  results.size == 1 ? results.first : results
181
184
  end
182
-
185
+
183
186
  # Copies src to dest on each host.
184
- #
187
+ #
185
188
  # @param src [String]
186
189
  # A local file to copy.
187
- #
190
+ #
188
191
  # @param dest [String]
189
192
  # The path of a remote file to copy to.
190
- #
193
+ #
191
194
  # @raise Errno::EACCES
192
195
  # If the destination path cannot be written to.
193
- #
196
+ #
194
197
  # @raise Errno::ENOENT
195
198
  # If the source path cannot be read.
196
- #
199
+ #
197
200
  def scp(src, dest, hosts=self.hosts)
198
201
  Array(hosts).each do |host|
199
202
  output = `scp -q #{src} #{"#{host}:" unless host == :localhost}#{dest} 2>&1`
@@ -204,35 +207,35 @@ module RobotArmy
204
207
  raise Errno::ENOENT, output.chomp
205
208
  end unless $?.exitstatus == 0
206
209
  end
207
-
210
+
208
211
  return nil
209
212
  end
210
-
213
+
211
214
  # Copies path to a temporary directory on each host.
212
- #
215
+ #
213
216
  # @param path [String]
214
217
  # A local file to copy.
215
- #
218
+ #
216
219
  # @param hosts [HostList]
217
220
  # Which hosts to connect to.
218
- #
221
+ #
219
222
  # @yield [path]
220
223
  # Yields the path of the newly copied file on each remote host.
221
- #
224
+ #
222
225
  # @yieldparam [String] path
223
- # The path of the file under in a new directory under a
226
+ # The path of the file under in a new directory under a
224
227
  # temporary directory on the remote host.
225
- #
228
+ #
226
229
  # @return [Array<String>]
227
230
  # An array of destination paths.
228
- #
231
+ #
229
232
  def cptemp(path, hosts=self.hosts, options={}, &block)
230
233
  hosts, options = self.hosts, hosts if hosts.is_a?(Hash)
231
-
234
+
232
235
  results = remote(hosts, options) do
233
236
  File.join(%x{mktemp -d -t robot-army.XXXX}.chomp, File.basename(path))
234
237
  end
235
-
238
+
236
239
  me = ENV['USER']
237
240
  host_and_path = Array(hosts).zip(Array(results))
238
241
  # copy them over
@@ -245,161 +248,70 @@ module RobotArmy
245
248
  results = host_and_path.map do |host, tmp|
246
249
  remote(host, options.merge(:args => [tmp]), &block)
247
250
  end if block
248
-
251
+
252
+ # delete it when we're done
253
+ RobotArmy::AtExit.shared_instance.at_exit do
254
+ host_and_path.each do |host, tmp|
255
+ remote(host, options) do
256
+ FileUtils.rm_rf(tmp)
257
+ end
258
+ end
259
+ end
260
+
249
261
  results.size == 1 ? results.first : results
250
262
  end
251
-
263
+
252
264
  # Add a gem dependency this TaskMaster checks for on each remote host.
253
- #
265
+ #
254
266
  # @param dep [String]
255
267
  # The name of the gem to check for.
256
- #
268
+ #
257
269
  # @param ver [String]
258
270
  # The version string of the gem to check for.
259
- #
271
+ #
260
272
  def dependency(dep, ver = nil)
261
273
  @dep_loader.add_dependency dep, ver
262
274
  end
263
-
275
+
264
276
  private
265
-
277
+
266
278
  def say(something)
267
279
  something = HighLine.new.color(something, :bold) if defined?(HighLine)
268
280
  puts "** #{something}"
269
281
  end
270
-
271
- # Dumps the values associated with the given names for transport.
272
- #
273
- # @param names [Array[String]]
274
- # The names of the variables to dump.
275
- #
276
- # @yield [name, index]
277
- # Yields the name and its index and expects
278
- # to get the corresponding value.
279
- #
280
- # @yieldparam [String] name
281
- # The name of the value for the block to return.
282
- #
283
- # @yieldparam [Fixnum] index
284
- # The index of the value for the block to return.
285
- #
286
- # @return [(Array[Object], Hash[Fixnum => Object])]
287
- # The pair +values+ and +proxies+.
288
- #
289
- def dump_values(names)
290
- proxies = {}
291
- values = []
292
-
293
- names.each_with_index do |name, i|
294
- value = yield name, i
295
- if value.marshalable?
296
- dump = Marshal.dump(value)
297
- values << "#{name} = Marshal.load(#{dump.inspect})"
298
- else
299
- proxies[value.hash] = value
300
- values << "#{name} = #{RobotArmy::Proxy.generator_for(value)}"
301
- end
302
- end
303
-
304
- return values, proxies
305
- end
306
-
282
+
307
283
  # Handles remotely eval'ing a Ruby Proc.
308
- #
284
+ #
309
285
  # @options options
310
286
  # :host -> [String, :localhost] => remote host
311
287
  # :user -> String => shell user
312
288
  # :password -> [String, nil] => sudo password
313
- #
289
+ #
314
290
  # @return Object
315
291
  # Whatever the block returns.
316
- #
292
+ #
317
293
  # @raise Exception
318
294
  # Whatever the block raises.
319
- #
295
+ #
320
296
  def remote_eval(options, &proc)
321
- host = options[:host]
322
- conn = connection(host)
323
- procargs = options[:args] || []
324
- proxies = { self.hash => self }
325
-
326
- ##
327
- ## build the code to send it
328
- ##
329
-
330
- # fix stack traces
331
- file, line = eval('[__FILE__, __LINE__]', proc.binding)
332
-
333
- # include local variables
334
- local_variables = eval('local_variables', proc.binding)
335
- locals, lproxies = dump_values(local_variables) { |name,| eval(name, proc.binding) }
336
- proxies.merge! lproxies
337
-
338
- # include arguments
339
- args, aproxies = dump_values(proc.arguments) { |_, i| procargs[i] }
340
- proxies.merge! aproxies
341
-
342
- # include dependency loader
343
- dep_loading = "Marshal.load(#{Marshal.dump(@dep_loader).inspect}).load!"
344
-
345
- # get the code for the proc
346
- proc = "proc{ #{proc.to_ruby(true)} }"
347
- messenger = "RobotArmy::Messenger.new($stdin, $stdout)"
348
- context = "RobotArmy::Proxy.new(#{messenger}, #{self.hash.inspect})"
349
-
350
- code = %{
351
- #{dep_loading} # load dependencies
352
- #{(locals+args).join("\n")} # all local variables
353
- #{context}.__proxy_instance_eval(&#{proc}) # run the block
354
- }
355
-
356
- options[:file] = file
357
- options[:line] = line
358
- options[:code] = code
359
-
360
- ##
361
- ## send the child a message
362
- ##
363
-
364
- conn.post(:command => :eval, :data => options)
365
-
366
- ##
367
- ## get and evaluate the response
368
- ##
369
-
370
- loop do
371
- # we want to loop until we get something other than "proxy"
372
- response = conn.messenger.get
373
- case response[:status]
374
- when 'proxy'
375
- begin
376
- proxy = proxies[response[:data][:hash]]
377
- data = proxy.send(*response[:data][:call])
378
- if data.marshalable?
379
- conn.post :status => 'ok', :data => data
380
- else
381
- proxies[data.hash] = data
382
- conn.post :status => 'proxy', :data => data.hash
383
- end
384
- rescue Object => e
385
- conn.post :status => 'error', :data => e
386
- end
387
- when 'password'
388
- conn.post :status => 'ok', :data => ask_for_password(host, response[:data])
389
- else
390
- begin
391
- return conn.handle_response(response)
392
- rescue RobotArmy::Warning => e
393
- $stderr.puts "WARNING: #{e.message}"
394
- return nil
395
- end
396
- end
397
- end
297
+ evaler = RemoteEvaler.new(connection(options[:host]), EvalCommand.new do |command|
298
+ command.user = options[:user]
299
+ command.proc = proc
300
+ command.args = options[:args] || []
301
+ command.context = self
302
+ command.dependencies = @dep_loader
303
+ command.keychain = keychain
304
+ end)
305
+
306
+ return evaler.execute_command
398
307
  end
399
-
400
- def ask_for_password(host, data={})
401
- require 'highline'
402
- HighLine.new.ask("[sudo] password for #{data[:user]}@#{host}: ") {|q| q.echo = false}
308
+
309
+ # Returns the default password manager. This should be
310
+ # overridden if you wish to have different sudo behavior.
311
+ def keychain
312
+ @keychain ||= Keychain.new
403
313
  end
404
314
  end
315
+
316
+ end
405
317
  end