fotonauts-capistrano 2.5.2

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