fotonauts-capistrano 2.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. data/CHANGELOG.rdoc +728 -0
  2. data/Manifest +104 -0
  3. data/README.rdoc +66 -0
  4. data/Rakefile +34 -0
  5. data/bin/cap +4 -0
  6. data/bin/capify +78 -0
  7. data/capistrano.gemspec +49 -0
  8. data/examples/sample.rb +14 -0
  9. data/lib/capistrano.rb +2 -0
  10. data/lib/capistrano/callback.rb +45 -0
  11. data/lib/capistrano/cli.rb +47 -0
  12. data/lib/capistrano/cli/execute.rb +84 -0
  13. data/lib/capistrano/cli/help.rb +118 -0
  14. data/lib/capistrano/cli/help.txt +62 -0
  15. data/lib/capistrano/cli/options.rb +224 -0
  16. data/lib/capistrano/cli/ui.rb +40 -0
  17. data/lib/capistrano/command.rb +283 -0
  18. data/lib/capistrano/configuration.rb +43 -0
  19. data/lib/capistrano/configuration/actions/file_transfer.rb +47 -0
  20. data/lib/capistrano/configuration/actions/inspect.rb +46 -0
  21. data/lib/capistrano/configuration/actions/invocation.rb +193 -0
  22. data/lib/capistrano/configuration/callbacks.rb +148 -0
  23. data/lib/capistrano/configuration/connections.rb +196 -0
  24. data/lib/capistrano/configuration/execution.rb +132 -0
  25. data/lib/capistrano/configuration/loading.rb +197 -0
  26. data/lib/capistrano/configuration/namespaces.rb +197 -0
  27. data/lib/capistrano/configuration/roles.rb +65 -0
  28. data/lib/capistrano/configuration/servers.rb +85 -0
  29. data/lib/capistrano/configuration/variables.rb +127 -0
  30. data/lib/capistrano/errors.rb +15 -0
  31. data/lib/capistrano/extensions.rb +57 -0
  32. data/lib/capistrano/logger.rb +59 -0
  33. data/lib/capistrano/processable.rb +53 -0
  34. data/lib/capistrano/recipes/compat.rb +32 -0
  35. data/lib/capistrano/recipes/deploy.rb +555 -0
  36. data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
  37. data/lib/capistrano/recipes/deploy/local_dependency.rb +54 -0
  38. data/lib/capistrano/recipes/deploy/remote_dependency.rb +105 -0
  39. data/lib/capistrano/recipes/deploy/scm.rb +19 -0
  40. data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
  41. data/lib/capistrano/recipes/deploy/scm/base.rb +196 -0
  42. data/lib/capistrano/recipes/deploy/scm/bzr.rb +83 -0
  43. data/lib/capistrano/recipes/deploy/scm/cvs.rb +152 -0
  44. data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
  45. data/lib/capistrano/recipes/deploy/scm/git.rb +271 -0
  46. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
  47. data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
  48. data/lib/capistrano/recipes/deploy/scm/perforce.rb +133 -0
  49. data/lib/capistrano/recipes/deploy/scm/subversion.rb +121 -0
  50. data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
  51. data/lib/capistrano/recipes/deploy/strategy/base.rb +79 -0
  52. data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
  53. data/lib/capistrano/recipes/deploy/strategy/copy.rb +210 -0
  54. data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
  55. data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
  56. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +56 -0
  57. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
  58. data/lib/capistrano/recipes/standard.rb +37 -0
  59. data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
  60. data/lib/capistrano/recipes/upgrade.rb +33 -0
  61. data/lib/capistrano/role.rb +102 -0
  62. data/lib/capistrano/server_definition.rb +56 -0
  63. data/lib/capistrano/shell.rb +260 -0
  64. data/lib/capistrano/ssh.rb +79 -0
  65. data/lib/capistrano/task_definition.rb +70 -0
  66. data/lib/capistrano/transfer.rb +216 -0
  67. data/lib/capistrano/version.rb +18 -0
  68. data/setup.rb +1346 -0
  69. data/test/cli/execute_test.rb +132 -0
  70. data/test/cli/help_test.rb +165 -0
  71. data/test/cli/options_test.rb +317 -0
  72. data/test/cli/ui_test.rb +28 -0
  73. data/test/cli_test.rb +17 -0
  74. data/test/command_test.rb +286 -0
  75. data/test/configuration/actions/file_transfer_test.rb +61 -0
  76. data/test/configuration/actions/inspect_test.rb +65 -0
  77. data/test/configuration/actions/invocation_test.rb +210 -0
  78. data/test/configuration/callbacks_test.rb +220 -0
  79. data/test/configuration/connections_test.rb +348 -0
  80. data/test/configuration/execution_test.rb +175 -0
  81. data/test/configuration/loading_test.rb +132 -0
  82. data/test/configuration/namespace_dsl_test.rb +305 -0
  83. data/test/configuration/roles_test.rb +143 -0
  84. data/test/configuration/servers_test.rb +121 -0
  85. data/test/configuration/variables_test.rb +180 -0
  86. data/test/configuration_test.rb +88 -0
  87. data/test/deploy/local_dependency_test.rb +76 -0
  88. data/test/deploy/remote_dependency_test.rb +114 -0
  89. data/test/deploy/scm/accurev_test.rb +23 -0
  90. data/test/deploy/scm/base_test.rb +55 -0
  91. data/test/deploy/scm/git_test.rb +159 -0
  92. data/test/deploy/scm/mercurial_test.rb +129 -0
  93. data/test/deploy/scm/none_test.rb +35 -0
  94. data/test/deploy/strategy/copy_test.rb +258 -0
  95. data/test/extensions_test.rb +69 -0
  96. data/test/fixtures/cli_integration.rb +5 -0
  97. data/test/fixtures/config.rb +5 -0
  98. data/test/fixtures/custom.rb +3 -0
  99. data/test/logger_test.rb +123 -0
  100. data/test/role_test.rb +11 -0
  101. data/test/server_definition_test.rb +121 -0
  102. data/test/shell_test.rb +90 -0
  103. data/test/ssh_test.rb +102 -0
  104. data/test/task_definition_test.rb +101 -0
  105. data/test/transfer_test.rb +160 -0
  106. data/test/utils.rb +38 -0
  107. metadata +310 -0
@@ -0,0 +1,193 @@
1
+ require 'capistrano/command'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ module Actions
6
+ module Invocation
7
+ def self.included(base) #:nodoc:
8
+ base.extend(ClassMethods)
9
+
10
+ base.send :alias_method, :initialize_without_invocation, :initialize
11
+ base.send :alias_method, :initialize, :initialize_with_invocation
12
+
13
+ base.default_io_proc = Proc.new do |ch, stream, out|
14
+ level = stream == :err ? :important : :info
15
+ ch[:options][:logger].send(level, out, "#{stream} :: #{ch[:server]}")
16
+ end
17
+ end
18
+
19
+ module ClassMethods
20
+ attr_accessor :default_io_proc
21
+ end
22
+
23
+ def initialize_with_invocation(*args) #:nodoc:
24
+ initialize_without_invocation(*args)
25
+ set :default_environment, {}
26
+ set :default_run_options, {}
27
+ end
28
+
29
+ def parallel(options={})
30
+ raise ArgumentError, "parallel() requires a block" unless block_given?
31
+ tree = Command::Tree.new(self) { |t| yield t }
32
+ run_tree(tree)
33
+ end
34
+
35
+ # Invokes the given command. If a +via+ key is given, it will be used
36
+ # to determine what method to use to invoke the command. It defaults
37
+ # to :run, but may be :sudo, or any other method that conforms to the
38
+ # same interface as run and sudo.
39
+ def invoke_command(cmd, options={}, &block)
40
+ options = options.dup
41
+ via = options.delete(:via) || :run
42
+ send(via, cmd, options, &block)
43
+ end
44
+
45
+ # Execute the given command on all servers that are the target of the
46
+ # current task. If a block is given, it is invoked for all output
47
+ # generated by the command, and should accept three parameters: the SSH
48
+ # channel (which may be used to send data back to the remote process),
49
+ # the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
50
+ # stdout), and the data that was received.
51
+ def run(cmd, options={}, &block)
52
+ block ||= self.class.default_io_proc
53
+ tree = Command::Tree.new(self) { |t| t.else(cmd, &block) }
54
+ run_tree(tree, options)
55
+ end
56
+
57
+ def run_tree(tree, options={})
58
+ if tree.branches.empty? && tree.fallback
59
+ logger.debug "executing #{tree.fallback}"
60
+ elsif tree.branches.any?
61
+ logger.debug "executing multiple commands in parallel"
62
+ tree.each do |branch|
63
+ logger.trace "-> #{branch}"
64
+ end
65
+ else
66
+ raise ArgumentError, "attempt to execute without specifying a command"
67
+ end
68
+
69
+ return if dry_run || (debug && continue_execution(tree) == false)
70
+
71
+ options = add_default_command_options(options)
72
+
73
+ tree.each do |branch|
74
+ if branch.command.include?(sudo)
75
+ branch.callback = sudo_behavior_callback(branch.callback)
76
+ end
77
+ end
78
+
79
+ execute_on_servers(options) do |servers|
80
+ targets = servers.map { |s| sessions[s] }
81
+ Command.process(tree, targets, options.merge(:logger => logger))
82
+ end
83
+ end
84
+
85
+ # Returns the command string used by capistrano to invoke a comamnd via
86
+ # sudo.
87
+ #
88
+ # run "#{sudo :as => 'bob'} mkdir /path/to/dir"
89
+ #
90
+ # It can also be invoked like #run, but executing the command via sudo.
91
+ # This assumes that the sudo password (if required) is the same as the
92
+ # password for logging in to the server.
93
+ #
94
+ # sudo "mkdir /path/to/dir"
95
+ #
96
+ # Also, this method understands a <tt>:sudo</tt> configuration variable,
97
+ # which (if specified) will be used as the full path to the sudo
98
+ # executable on the remote machine:
99
+ #
100
+ # set :sudo, "/opt/local/bin/sudo"
101
+ def sudo(*parameters, &block)
102
+ options = parameters.last.is_a?(Hash) ? parameters.pop.dup : {}
103
+ command = parameters.first
104
+ user = options[:as] && "-u #{options.delete(:as)}"
105
+
106
+ sudo_prompt_option = "-p '#{sudo_prompt}'" unless sudo_prompt.empty?
107
+ sudo_command = [fetch(:sudo, "sudo"), sudo_prompt_option, user].compact.join(" ")
108
+
109
+ if command
110
+ command = sudo_command + " " + command
111
+ run(command, options, &block)
112
+ else
113
+ return sudo_command
114
+ end
115
+ end
116
+
117
+ # Returns a Proc object that defines the behavior of the sudo
118
+ # callback. The returned Proc will defer to the +fallback+ argument
119
+ # (which should also be a Proc) for any output it does not
120
+ # explicitly handle.
121
+ def sudo_behavior_callback(fallback) #:nodoc:
122
+ # in order to prevent _each host_ from prompting when the password
123
+ # was wrong, let's track which host prompted first and only allow
124
+ # subsequent prompts from that host.
125
+ prompt_host = nil
126
+
127
+ Proc.new do |ch, stream, out|
128
+ if out =~ /^#{Regexp.escape(sudo_prompt)}/
129
+ ch.send_data "#{self[:password]}\n"
130
+ elsif out =~ /^Sorry, try again/
131
+ if prompt_host.nil? || prompt_host == ch[:server]
132
+ prompt_host = ch[:server]
133
+ logger.important out, "#{stream} :: #{ch[:server]}"
134
+ reset! :password
135
+ end
136
+ elsif fallback
137
+ fallback.call(ch, stream, out)
138
+ end
139
+ end
140
+ end
141
+
142
+ # Merges the various default command options into the options hash and
143
+ # returns the result. The default command options that are understand
144
+ # are:
145
+ #
146
+ # * :default_environment: If the :env key already exists, the :env
147
+ # key is merged into default_environment and then added back into
148
+ # options.
149
+ # * :default_shell: if the :shell key already exists, it will be used.
150
+ # Otherwise, if the :default_shell key exists in the configuration,
151
+ # it will be used. Otherwise, no :shell key is added.
152
+ def add_default_command_options(options)
153
+ defaults = self[:default_run_options]
154
+ options = defaults.merge(options)
155
+
156
+ env = self[:default_environment]
157
+ env = env.merge(options[:env]) if options[:env]
158
+ options[:env] = env unless env.empty?
159
+
160
+ shell = options[:shell] || self[:default_shell]
161
+ options[:shell] = shell unless shell.nil?
162
+
163
+ options
164
+ end
165
+
166
+ # Returns the prompt text to use with sudo
167
+ def sudo_prompt
168
+ fetch(:sudo_prompt, "sudo password: ")
169
+ end
170
+
171
+ def continue_execution(tree)
172
+ if tree.branches.length == 1
173
+ continue_execution_for_branch(tree.branches.first)
174
+ else
175
+ tree.each { |branch| branch.skip! unless continue_execution_for_branch(branch) }
176
+ tree.any? { |branch| !branch.skip? }
177
+ end
178
+ end
179
+
180
+ def continue_execution_for_branch(branch)
181
+ case Capistrano::CLI.debug_prompt(branch)
182
+ when "y"
183
+ true
184
+ when "n"
185
+ false
186
+ when "a"
187
+ exit(-1)
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,148 @@
1
+ require 'capistrano/callback'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ module Callbacks
6
+ def self.included(base) #:nodoc:
7
+ %w(initialize invoke_task_directly).each do |method|
8
+ base.send :alias_method, "#{method}_without_callbacks", method
9
+ base.send :alias_method, method, "#{method}_with_callbacks"
10
+ end
11
+ end
12
+
13
+ # The hash of callbacks that have been registered for this configuration
14
+ attr_reader :callbacks
15
+
16
+ def initialize_with_callbacks(*args) #:nodoc:
17
+ initialize_without_callbacks(*args)
18
+ @callbacks = {}
19
+ end
20
+
21
+ def invoke_task_directly_with_callbacks(task) #:nodoc:
22
+ before = find_hook(task, :before)
23
+ execute_task(before) if before
24
+
25
+ trigger :before, task
26
+
27
+ result = invoke_task_directly_without_callbacks(task)
28
+
29
+ trigger :after, task
30
+
31
+ after = find_hook(task, :after)
32
+ execute_task(after) if after
33
+
34
+ return result
35
+ end
36
+
37
+ # Defines a callback to be invoked before the given task. You must
38
+ # specify the fully-qualified task name, both for the primary task, and
39
+ # for the task(s) to be executed before. Alternatively, you can pass a
40
+ # block to be executed before the given task.
41
+ #
42
+ # before "deploy:update_code", :record_difference
43
+ # before :deploy, "custom:log_deploy"
44
+ # before :deploy, :this, "then:this", "and:then:this"
45
+ # before :some_task do
46
+ # puts "an anonymous hook!"
47
+ # end
48
+ #
49
+ # This just provides a convenient interface to the more general #on method.
50
+ def before(task_name, *args, &block)
51
+ options = args.last.is_a?(Hash) ? args.pop : {}
52
+ args << options.merge(:only => task_name)
53
+ on :before, *args, &block
54
+ end
55
+
56
+ # Defines a callback to be invoked after the given task. You must
57
+ # specify the fully-qualified task name, both for the primary task, and
58
+ # for the task(s) to be executed after. Alternatively, you can pass a
59
+ # block to be executed after the given task.
60
+ #
61
+ # after "deploy:update_code", :log_difference
62
+ # after :deploy, "custom:announce"
63
+ # after :deploy, :this, "then:this", "and:then:this"
64
+ # after :some_task do
65
+ # puts "an anonymous hook!"
66
+ # end
67
+ #
68
+ # This just provides a convenient interface to the more general #on method.
69
+ def after(task_name, *args, &block)
70
+ options = args.last.is_a?(Hash) ? args.pop : {}
71
+ args << options.merge(:only => task_name)
72
+ on :after, *args, &block
73
+ end
74
+
75
+ # Defines one or more callbacks to be invoked in response to some event.
76
+ # Capistrano currently understands the following events:
77
+ #
78
+ # * :before, triggered before a task is invoked
79
+ # * :after, triggered after a task is invoked
80
+ # * :start, triggered before a top-level task is invoked via the command-line
81
+ # * :finish, triggered when a top-level task completes
82
+ # * :load, triggered after all recipes have loaded
83
+ # * :exit, triggered after all tasks have completed
84
+ #
85
+ # Specify the (fully-qualified) task names that you want invoked in
86
+ # response to the event. Alternatively, you can specify a block to invoke
87
+ # when the event is triggered. You can also pass a hash of options as the
88
+ # last parameter, which may include either of two keys:
89
+ #
90
+ # * :only, should specify an array of task names. Restricts this callback
91
+ # so that it will only fire when the event applies to those tasks.
92
+ # * :except, should specify an array of task names. Restricts this callback
93
+ # so that it will never fire when the event applies to those tasks.
94
+ #
95
+ # Usage:
96
+ #
97
+ # on :before, "some:hook", "another:hook", :only => "deploy:update"
98
+ # on :after, "some:hook", :except => "deploy:symlink"
99
+ # on :before, "global:hook"
100
+ # on :after, :only => :deploy do
101
+ # puts "after deploy here"
102
+ # end
103
+ def on(event, *args, &block)
104
+ options = args.last.is_a?(Hash) ? args.pop : {}
105
+ callbacks[event] ||= []
106
+
107
+ if args.empty? && block.nil?
108
+ raise ArgumentError, "please specify either a task name or a block to invoke"
109
+ elsif args.any? && block
110
+ raise ArgumentError, "please specify only a task name or a block, but not both"
111
+ elsif block
112
+ callbacks[event] << ProcCallback.new(block, options)
113
+ else
114
+ args.each do |name|
115
+ callbacks[event] << TaskCallback.new(self, name, options)
116
+ end
117
+ end
118
+ end
119
+
120
+ # Trigger the named event for the named task. All associated callbacks
121
+ # will be fired, in the order they were defined.
122
+ def trigger(event, task=nil)
123
+ pending = Array(callbacks[event]).select { |c| c.applies_to?(task) }
124
+ if pending.any?
125
+ msg = "triggering #{event} callbacks"
126
+ msg << " for `#{task.fully_qualified_name}'" if task
127
+ logger.trace(msg)
128
+ pending.each { |callback| callback.call }
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ # Looks for before_foo or after_foo tasks. This method of extending tasks
135
+ # is now discouraged (though not formally deprecated). You should use the
136
+ # before and after methods to declare hooks for such callbacks.
137
+ def find_hook(task, hook)
138
+ if task == task.namespace.default_task
139
+ result = task.namespace.search_task("#{hook}_#{task.namespace.name}")
140
+ return result if result
141
+ end
142
+
143
+ task.namespace.search_task("#{hook}_#{task.name}")
144
+ end
145
+
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,196 @@
1
+ require 'enumerator'
2
+ require 'net/ssh/gateway'
3
+ require 'capistrano/ssh'
4
+ require 'capistrano/errors'
5
+
6
+ module Capistrano
7
+ class Configuration
8
+ module Connections
9
+ def self.included(base) #:nodoc:
10
+ base.send :alias_method, :initialize_without_connections, :initialize
11
+ base.send :alias_method, :initialize, :initialize_with_connections
12
+ end
13
+
14
+ class DefaultConnectionFactory #:nodoc:
15
+ def initialize(options)
16
+ @options = options
17
+ end
18
+
19
+ def connect_to(server)
20
+ SSH.connect(server, @options)
21
+ end
22
+ end
23
+
24
+ class GatewayConnectionFactory #:nodoc:
25
+ def initialize(gateway, options)
26
+ @options = options
27
+ @options[:logger].debug "Creating gateway using #{[*gateway].join(', ')}" if @options[:logger]
28
+ Thread.abort_on_exception = true
29
+ @gateways = [*gateway].collect { |g| ServerDefinition.new(g) }
30
+ tunnel = SSH.connection_strategy(@gateways[0], @options) do |host, user, connect_options|
31
+ Net::SSH::Gateway.new(host, user, connect_options)
32
+ end
33
+ @gateway = (@gateways[1..-1]).inject(tunnel) do |tunnel, destination|
34
+ @options[:logger].debug "Creating tunnel to #{destination}" if @options[:logger]
35
+ local_host = ServerDefinition.new("127.0.0.1", :user => destination.user, :port => tunnel.open(destination.host, (destination.port || 22)))
36
+ SSH.connection_strategy(local_host, @options) do |host, user, connect_options|
37
+ Net::SSH::Gateway.new(host, user, connect_options)
38
+ end
39
+ end
40
+ end
41
+
42
+ def connect_to(server)
43
+ @options[:logger].debug "establishing connection to `#{server}' via gateway" if @options[:logger]
44
+ local_host = ServerDefinition.new("127.0.0.1", :user => server.user, :port => @gateway.open(server.host, server.port || 22))
45
+ session = SSH.connect(local_host, @options)
46
+ session.xserver = server
47
+ session
48
+ end
49
+ end
50
+
51
+ # A hash of the SSH sessions that are currently open and available.
52
+ # Because sessions are constructed lazily, this will only contain
53
+ # connections to those servers that have been the targets of one or more
54
+ # executed tasks.
55
+ attr_reader :sessions
56
+
57
+ def initialize_with_connections(*args) #:nodoc:
58
+ initialize_without_connections(*args)
59
+ @sessions = {}
60
+ @failed_sessions = []
61
+ end
62
+
63
+ # Indicate that the given server could not be connected to.
64
+ def failed!(server)
65
+ @failed_sessions << server
66
+ end
67
+
68
+ # Query whether previous connection attempts to the given server have
69
+ # failed.
70
+ def has_failed?(server)
71
+ @failed_sessions.include?(server)
72
+ end
73
+
74
+ # Used to force connections to be made to the current task's servers.
75
+ # Connections are normally made lazily in Capistrano--you can use this
76
+ # to force them open before performing some operation that might be
77
+ # time-sensitive.
78
+ def connect!(options={})
79
+ execute_on_servers(options) { }
80
+ end
81
+
82
+ # Returns the object responsible for establishing new SSH connections.
83
+ # The factory will respond to #connect_to, which can be used to
84
+ # establish connections to servers defined via ServerDefinition objects.
85
+ def connection_factory
86
+ @connection_factory ||= begin
87
+ if exists?(:gateway)
88
+ logger.debug "establishing connection to gateway `#{fetch(:gateway)}'"
89
+ GatewayConnectionFactory.new(fetch(:gateway), self)
90
+ else
91
+ DefaultConnectionFactory.new(self)
92
+ end
93
+ end
94
+ end
95
+
96
+ # Ensures that there are active sessions for each server in the list.
97
+ def establish_connections_to(servers)
98
+ failed_servers = []
99
+
100
+ # force the connection factory to be instantiated synchronously,
101
+ # otherwise we wind up with multiple gateway instances, because
102
+ # each connection is done in parallel.
103
+ connection_factory
104
+
105
+ threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
106
+ threads.each { |t| t.join }
107
+
108
+ if failed_servers.any?
109
+ errors = failed_servers.map { |h| "#{h[:server]} (#{h[:error].class}: #{h[:error].message})" }
110
+ error = ConnectionError.new("connection failed for: #{errors.join(', ')}")
111
+ error.hosts = failed_servers.map { |h| h[:server] }
112
+ raise error
113
+ end
114
+ end
115
+
116
+ # Destroys sessions for each server in the list.
117
+ def teardown_connections_to(servers)
118
+ servers.each do |server|
119
+ @sessions[server].close
120
+ @sessions.delete(server)
121
+ end
122
+ end
123
+
124
+ # Determines the set of servers within the current task's scope and
125
+ # establishes connections to them, and then yields that list of
126
+ # servers.
127
+ def execute_on_servers(options={})
128
+ raise ArgumentError, "expected a block" unless block_given?
129
+
130
+ if task = current_task
131
+ servers = find_servers_for_task(task, options)
132
+
133
+ if servers.empty?
134
+ logger.info "skipping `#{task.fully_qualified_name}' because no servers matched"
135
+ return
136
+ end
137
+
138
+ if task.continue_on_error?
139
+ servers.delete_if { |s| has_failed?(s) }
140
+ return if servers.empty?
141
+ end
142
+ else
143
+ servers = find_servers(options)
144
+ raise Capistrano::NoMatchingServersError, "no servers found to match #{options.inspect}" if servers.empty?
145
+ end
146
+
147
+ servers = [servers.first] if options[:once]
148
+ logger.trace "servers: #{servers.map { |s| s.host }.inspect}"
149
+
150
+ max_hosts = (options[:max_hosts] || (task && task.max_hosts) || servers.size).to_i
151
+ is_subset = max_hosts < servers.size
152
+
153
+ # establish connections to those servers in groups of max_hosts, as necessary
154
+ servers.each_slice(max_hosts) do |servers_slice|
155
+ begin
156
+ establish_connections_to(servers_slice)
157
+ rescue ConnectionError => error
158
+ raise error unless task && task.continue_on_error?
159
+ error.hosts.each do |h|
160
+ servers_slice.delete(h)
161
+ failed!(h)
162
+ end
163
+ end
164
+
165
+ begin
166
+ yield servers_slice
167
+ rescue RemoteError => error
168
+ raise error unless task && task.continue_on_error?
169
+ error.hosts.each { |h| failed!(h) }
170
+ end
171
+
172
+ # if dealing with a subset (e.g., :max_hosts is less than the
173
+ # number of servers available) teardown the subset of connections
174
+ # that were just made, so that we can make room for the next subset.
175
+ teardown_connections_to(servers_slice) if is_subset
176
+ end
177
+ end
178
+
179
+ private
180
+
181
+ # We establish the connection by creating a thread in a new method--this
182
+ # prevents problems with the thread's scope seeing the wrong 'server'
183
+ # variable if the thread just happens to take too long to start up.
184
+ def establish_connection_to(server, failures=nil)
185
+ Thread.new { safely_establish_connection_to(server, failures) }
186
+ end
187
+
188
+ def safely_establish_connection_to(server, failures=nil)
189
+ sessions[server] ||= connection_factory.connect_to(server)
190
+ rescue Exception => err
191
+ raise unless failures
192
+ failures << { :server => server, :error => err }
193
+ end
194
+ end
195
+ end
196
+ end