wesabe-robot-army 0.1.1 → 0.1.7

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.
@@ -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