capistrano 1.4.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. data/CHANGELOG +140 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README +22 -14
  4. data/bin/cap +1 -8
  5. data/bin/capify +77 -0
  6. data/examples/sample.rb +10 -109
  7. data/lib/capistrano.rb +1 -0
  8. data/lib/capistrano/callback.rb +41 -0
  9. data/lib/capistrano/cli.rb +17 -317
  10. data/lib/capistrano/cli/execute.rb +82 -0
  11. data/lib/capistrano/cli/help.rb +102 -0
  12. data/lib/capistrano/cli/help.txt +53 -0
  13. data/lib/capistrano/cli/options.rb +183 -0
  14. data/lib/capistrano/cli/ui.rb +28 -0
  15. data/lib/capistrano/command.rb +62 -29
  16. data/lib/capistrano/configuration.rb +25 -226
  17. data/lib/capistrano/configuration/actions/file_transfer.rb +35 -0
  18. data/lib/capistrano/configuration/actions/inspect.rb +46 -0
  19. data/lib/capistrano/configuration/actions/invocation.rb +127 -0
  20. data/lib/capistrano/configuration/callbacks.rb +148 -0
  21. data/lib/capistrano/configuration/connections.rb +159 -0
  22. data/lib/capistrano/configuration/execution.rb +126 -0
  23. data/lib/capistrano/configuration/loading.rb +112 -0
  24. data/lib/capistrano/configuration/namespaces.rb +190 -0
  25. data/lib/capistrano/configuration/roles.rb +51 -0
  26. data/lib/capistrano/configuration/servers.rb +75 -0
  27. data/lib/capistrano/configuration/variables.rb +127 -0
  28. data/lib/capistrano/errors.rb +15 -0
  29. data/lib/capistrano/extensions.rb +27 -8
  30. data/lib/capistrano/gateway.rb +54 -29
  31. data/lib/capistrano/logger.rb +11 -11
  32. data/lib/capistrano/recipes/compat.rb +32 -0
  33. data/lib/capistrano/recipes/deploy.rb +483 -0
  34. data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
  35. data/lib/capistrano/recipes/deploy/local_dependency.rb +46 -0
  36. data/lib/capistrano/recipes/deploy/remote_dependency.rb +65 -0
  37. data/lib/capistrano/recipes/deploy/scm.rb +19 -0
  38. data/lib/capistrano/recipes/deploy/scm/base.rb +180 -0
  39. data/lib/capistrano/recipes/deploy/scm/bzr.rb +86 -0
  40. data/lib/capistrano/recipes/deploy/scm/cvs.rb +151 -0
  41. data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
  42. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +129 -0
  43. data/lib/capistrano/recipes/deploy/scm/perforce.rb +126 -0
  44. data/lib/capistrano/recipes/deploy/scm/subversion.rb +103 -0
  45. data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
  46. data/lib/capistrano/recipes/deploy/strategy/base.rb +64 -0
  47. data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
  48. data/lib/capistrano/recipes/deploy/strategy/copy.rb +143 -0
  49. data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
  50. data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
  51. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +47 -0
  52. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
  53. data/lib/capistrano/recipes/standard.rb +26 -276
  54. data/lib/capistrano/recipes/templates/maintenance.rhtml +1 -1
  55. data/lib/capistrano/recipes/upgrade.rb +33 -0
  56. data/lib/capistrano/server_definition.rb +51 -0
  57. data/lib/capistrano/shell.rb +125 -81
  58. data/lib/capistrano/ssh.rb +80 -36
  59. data/lib/capistrano/task_definition.rb +69 -0
  60. data/lib/capistrano/upload.rb +146 -0
  61. data/lib/capistrano/version.rb +13 -17
  62. data/test/cli/execute_test.rb +132 -0
  63. data/test/cli/help_test.rb +139 -0
  64. data/test/cli/options_test.rb +226 -0
  65. data/test/cli/ui_test.rb +28 -0
  66. data/test/cli_test.rb +17 -0
  67. data/test/command_test.rb +284 -25
  68. data/test/configuration/actions/file_transfer_test.rb +40 -0
  69. data/test/configuration/actions/inspect_test.rb +62 -0
  70. data/test/configuration/actions/invocation_test.rb +195 -0
  71. data/test/configuration/callbacks_test.rb +206 -0
  72. data/test/configuration/connections_test.rb +288 -0
  73. data/test/configuration/execution_test.rb +159 -0
  74. data/test/configuration/loading_test.rb +119 -0
  75. data/test/configuration/namespace_dsl_test.rb +283 -0
  76. data/test/configuration/roles_test.rb +47 -0
  77. data/test/configuration/servers_test.rb +90 -0
  78. data/test/configuration/variables_test.rb +180 -0
  79. data/test/configuration_test.rb +60 -212
  80. data/test/deploy/scm/base_test.rb +55 -0
  81. data/test/deploy/strategy/copy_test.rb +146 -0
  82. data/test/extensions_test.rb +69 -0
  83. data/test/fixtures/cli_integration.rb +5 -0
  84. data/test/fixtures/custom.rb +2 -2
  85. data/test/gateway_test.rb +167 -0
  86. data/test/logger_test.rb +123 -0
  87. data/test/server_definition_test.rb +108 -0
  88. data/test/shell_test.rb +64 -0
  89. data/test/ssh_test.rb +67 -154
  90. data/test/task_definition_test.rb +101 -0
  91. data/test/upload_test.rb +131 -0
  92. data/test/utils.rb +31 -39
  93. data/test/version_test.rb +24 -0
  94. metadata +145 -98
  95. data/THANKS +0 -4
  96. data/lib/capistrano/actor.rb +0 -567
  97. data/lib/capistrano/generators/rails/deployment/deployment_generator.rb +0 -25
  98. data/lib/capistrano/generators/rails/deployment/templates/capistrano.rake +0 -49
  99. data/lib/capistrano/generators/rails/deployment/templates/deploy.rb +0 -122
  100. data/lib/capistrano/generators/rails/loader.rb +0 -20
  101. data/lib/capistrano/scm/base.rb +0 -61
  102. data/lib/capistrano/scm/baz.rb +0 -118
  103. data/lib/capistrano/scm/bzr.rb +0 -70
  104. data/lib/capistrano/scm/cvs.rb +0 -129
  105. data/lib/capistrano/scm/darcs.rb +0 -27
  106. data/lib/capistrano/scm/mercurial.rb +0 -83
  107. data/lib/capistrano/scm/perforce.rb +0 -139
  108. data/lib/capistrano/scm/subversion.rb +0 -128
  109. data/lib/capistrano/transfer.rb +0 -97
  110. data/lib/capistrano/utils.rb +0 -26
  111. data/test/actor_test.rb +0 -402
  112. data/test/scm/cvs_test.rb +0 -196
  113. data/test/scm/subversion_test.rb +0 -145
@@ -0,0 +1,35 @@
1
+ require 'capistrano/upload'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ module Actions
6
+ module FileTransfer
7
+
8
+ # Store the given data at the given location on all servers targetted
9
+ # by the current task. If <tt>:mode</tt> is specified it is used to
10
+ # set the mode on the file.
11
+ def put(data, path, options={})
12
+ execute_on_servers(options) do |servers|
13
+ targets = servers.map { |s| sessions[s] }
14
+ Upload.process(targets, path, :data => data, :mode => options[:mode], :logger => logger)
15
+ end
16
+ end
17
+
18
+ # Get file remote_path from FIRST server targetted by
19
+ # the current task and transfer it to local machine as path.
20
+ #
21
+ # get "#{deploy_to}/current/log/production.log", "log/production.log.web"
22
+ def get(remote_path, path, options = {})
23
+ execute_on_servers(options.merge(:once => true)) do |servers|
24
+ logger.info "downloading `#{servers.first.host}:#{remote_path}' to `#{path}'"
25
+ sftp = sessions[servers.first].sftp
26
+ sftp.connect unless sftp.state == :open
27
+ sftp.get_file remote_path, path
28
+ logger.debug "download finished"
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,46 @@
1
+ require 'capistrano/errors'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ module Actions
6
+ module Inspect
7
+
8
+ # Streams the result of the command from all servers that are the
9
+ # target of the current task. All these streams will be joined into a
10
+ # single one, so you can, say, watch 10 log files as though they were
11
+ # one. Do note that this is quite expensive from a bandwidth
12
+ # perspective, so use it with care.
13
+ #
14
+ # The command is invoked via #invoke_command.
15
+ #
16
+ # Usage:
17
+ #
18
+ # desc "Run a tail on multiple log files at the same time"
19
+ # task :tail_fcgi, :roles => :app do
20
+ # stream "tail -f #{shared_path}/log/fastcgi.crash.log"
21
+ # end
22
+ def stream(command, options={})
23
+ invoke_command(command, options) do |ch, stream, out|
24
+ puts out if stream == :out
25
+ warn "[err :: #{ch[:server]}] #{out}" if stream == :err
26
+ end
27
+ end
28
+
29
+ # Executes the given command on the first server targetted by the
30
+ # current task, collects it's stdout into a string, and returns the
31
+ # string. The command is invoked via #invoke_command.
32
+ def capture(command, options={})
33
+ output = ""
34
+ invoke_command(command, options.merge(:once => true)) do |ch, stream, data|
35
+ case stream
36
+ when :out then output << data
37
+ when :err then raise CaptureError, "error processing #{command.inspect}: #{data.inspect}"
38
+ end
39
+ end
40
+ output
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,127 @@
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
+ end
27
+
28
+ # Invokes the given command. If a +via+ key is given, it will be used
29
+ # to determine what method to use to invoke the command. It defaults
30
+ # to :run, but may be :sudo, or any other method that conforms to the
31
+ # same interface as run and sudo.
32
+ def invoke_command(cmd, options={}, &block)
33
+ options = options.dup
34
+ via = options.delete(:via) || :run
35
+ send(via, cmd, options, &block)
36
+ end
37
+
38
+ # Execute the given command on all servers that are the target of the
39
+ # current task. If a block is given, it is invoked for all output
40
+ # generated by the command, and should accept three parameters: the SSH
41
+ # channel (which may be used to send data back to the remote process),
42
+ # the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
43
+ # stdout), and the data that was received.
44
+ def run(cmd, options={}, &block)
45
+ block ||= self.class.default_io_proc
46
+ logger.debug "executing #{cmd.strip.inspect}"
47
+
48
+ options = add_default_command_options(options)
49
+
50
+ execute_on_servers(options) do |servers|
51
+ targets = servers.map { |s| sessions[s] }
52
+ Command.process(cmd, targets, options.merge(:logger => logger), &block)
53
+ end
54
+ end
55
+
56
+ # Like #run, but executes the command via <tt>sudo</tt>. This assumes
57
+ # that the sudo password (if required) is the same as the password for
58
+ # logging in to the server.
59
+ #
60
+ # Also, this module accepts a <tt>:sudo</tt> configuration variable,
61
+ # which (if specified) will be used as the full path to the sudo
62
+ # executable on the remote machine:
63
+ #
64
+ # set :sudo, "/opt/local/bin/sudo"
65
+ def sudo(command, options={}, &block)
66
+ block ||= self.class.default_io_proc
67
+
68
+ options = options.dup
69
+ as = options.delete(:as)
70
+
71
+ user = as && "-u #{as}"
72
+ command = [fetch(:sudo, "sudo"), user, command].compact.join(" ")
73
+
74
+ run(command, options, &sudo_behavior_callback(block))
75
+ end
76
+
77
+ # Returns a Proc object that defines the behavior of the sudo
78
+ # callback. The returned Proc will defer to the +fallback+ argument
79
+ # (which should also be a Proc) for any output it does not
80
+ # explicitly handle.
81
+ def sudo_behavior_callback(fallback) #:nodoc:
82
+ # in order to prevent _each host_ from prompting when the password
83
+ # was wrong, let's track which host prompted first and only allow
84
+ # subsequent prompts from that host.
85
+ prompt_host = nil
86
+
87
+ Proc.new do |ch, stream, out|
88
+ if out =~ /password:/i
89
+ ch.send_data "#{self[:password]}\n"
90
+ elsif out =~ /try again/
91
+ if prompt_host.nil? || prompt_host == ch[:server]
92
+ prompt_host = ch[:server]
93
+ logger.important out, "#{stream} :: #{ch[:server]}"
94
+ reset! :password
95
+ end
96
+ else
97
+ fallback.call(ch, stream, out)
98
+ end
99
+ end
100
+ end
101
+
102
+ # Merges the various default command options into the options hash and
103
+ # returns the result. The default command options that are understand
104
+ # are:
105
+ #
106
+ # * :default_environment: If the :env key already exists, the :env
107
+ # key is merged into default_environment and then added back into
108
+ # options.
109
+ # * :default_shell: if the :shell key already exists, it will be used.
110
+ # Otherwise, if the :default_shell key exists in the configuration,
111
+ # it will be used. Otherwise, no :shell key is added.
112
+ def add_default_command_options(options)
113
+ options = options.dup
114
+
115
+ env = self[:default_environment]
116
+ env = env.merge(options[:env]) if options[:env]
117
+ options[:env] = env unless env.empty?
118
+
119
+ shell = options[:shell] || self[:default_shell]
120
+ options[:shell] = shell if shell
121
+
122
+ options
123
+ end
124
+ end
125
+ end
126
+ end
127
+ 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 execute_task).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 execute_task_with_callbacks(task) #:nodoc:
22
+ before = find_hook(task, :before)
23
+ execute_task(before) if before
24
+
25
+ trigger :before, task
26
+
27
+ result = execute_task_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,159 @@
1
+ require 'capistrano/gateway'
2
+ require 'capistrano/ssh'
3
+
4
+ module Capistrano
5
+ class Configuration
6
+ module Connections
7
+ def self.included(base) #:nodoc:
8
+ base.send :alias_method, :initialize_without_connections, :initialize
9
+ base.send :alias_method, :initialize, :initialize_with_connections
10
+ end
11
+
12
+ # An adaptor for making the SSH interface look and act like that of the
13
+ # Gateway class.
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
+ # A hash of the SSH sessions that are currently open and available.
25
+ # Because sessions are constructed lazily, this will only contain
26
+ # connections to those servers that have been the targets of one or more
27
+ # executed tasks.
28
+ attr_reader :sessions
29
+
30
+ def initialize_with_connections(*args) #:nodoc:
31
+ initialize_without_connections(*args)
32
+ @sessions = {}
33
+ @failed_sessions = []
34
+ end
35
+
36
+ # Indicate that the given server could not be connected to.
37
+ def failed!(server)
38
+ @failed_sessions << server
39
+ end
40
+
41
+ # Query whether previous connection attempts to the given server have
42
+ # failed.
43
+ def has_failed?(server)
44
+ @failed_sessions.include?(server)
45
+ end
46
+
47
+ # Used to force connections to be made to the current task's servers.
48
+ # Connections are normally made lazily in Capistrano--you can use this
49
+ # to force them open before performing some operation that might be
50
+ # time-sensitive.
51
+ def connect!(options={})
52
+ execute_on_servers(options) { }
53
+ end
54
+
55
+ # Returns the object responsible for establishing new SSH connections.
56
+ # The factory will respond to #connect_to, which can be used to
57
+ # establish connections to servers defined via ServerDefinition objects.
58
+ def connection_factory
59
+ @connection_factory ||= begin
60
+ if exists?(:gateway)
61
+ logger.debug "establishing connection to gateway `#{fetch(:gateway)}'"
62
+ Gateway.new(ServerDefinition.new(fetch(:gateway)), self)
63
+ else
64
+ DefaultConnectionFactory.new(self)
65
+ end
66
+ end
67
+ end
68
+
69
+ # Ensures that there are active sessions for each server in the list.
70
+ def establish_connections_to(servers)
71
+ failed_servers = []
72
+
73
+ # This attemps to work around the problem where SFTP uploads hang
74
+ # for some people. A bit of investigating seemed to reveal that the
75
+ # hang only occurred when the SSH connections were established async,
76
+ # so this setting allows people to at least work around the problem.
77
+ if fetch(:synchronous_connect, false)
78
+ logger.trace "synchronous_connect: true"
79
+ Array(servers).each { |server| safely_establish_connection_to(server, failed_servers) }
80
+ else
81
+ # force the connection factory to be instantiated synchronously,
82
+ # otherwise we wind up with multiple gateway instances, because
83
+ # each connection is done in parallel.
84
+ connection_factory
85
+
86
+ threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) }
87
+ threads.each { |t| t.join }
88
+ end
89
+
90
+ if failed_servers.any?
91
+ errors = failed_servers.map { |h| "#{h[:server]} (#{h[:error].class}: #{h[:error].message})" }
92
+ error = ConnectionError.new("connection failed for: #{errors.join(', ')}")
93
+ error.hosts = failed_servers.map { |h| h[:server] }
94
+ raise error
95
+ end
96
+ end
97
+
98
+ # Determines the set of servers within the current task's scope and
99
+ # establishes connections to them, and then yields that list of
100
+ # servers.
101
+ def execute_on_servers(options={})
102
+ raise ArgumentError, "expected a block" unless block_given?
103
+
104
+ if task = current_task
105
+ servers = find_servers_for_task(task, options)
106
+
107
+ if servers.empty?
108
+ raise Capistrano::NoMatchingServersError, "`#{task.fully_qualified_name}' is only run for servers matching #{task.options.inspect}, but no servers matched"
109
+ end
110
+
111
+ if task.continue_on_error?
112
+ servers.delete_if { |s| has_failed?(s) }
113
+ return if servers.empty?
114
+ end
115
+ else
116
+ servers = find_servers(options)
117
+ raise Capistrano::NoMatchingServersError, "no servers found to match #{options.inspect}" if servers.empty?
118
+ end
119
+
120
+ servers = [servers.first] if options[:once]
121
+ logger.trace "servers: #{servers.map { |s| s.host }.inspect}"
122
+
123
+ # establish connections to those servers, as necessary
124
+ begin
125
+ establish_connections_to(servers)
126
+ rescue ConnectionError => error
127
+ raise error unless task && task.continue_on_error?
128
+ error.hosts.each do |h|
129
+ servers.delete(h)
130
+ failed!(h)
131
+ end
132
+ end
133
+
134
+ begin
135
+ yield servers
136
+ rescue RemoteError => error
137
+ raise error unless task && task.continue_on_error?
138
+ error.hosts.each { |h| failed!(h) }
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ # We establish the connection by creating a thread in a new method--this
145
+ # prevents problems with the thread's scope seeing the wrong 'server'
146
+ # variable if the thread just happens to take too long to start up.
147
+ def establish_connection_to(server, failures=nil)
148
+ Thread.new { safely_establish_connection_to(server, failures) }
149
+ end
150
+
151
+ def safely_establish_connection_to(server, failures=nil)
152
+ sessions[server] ||= connection_factory.connect_to(server)
153
+ rescue Exception => err
154
+ raise unless failures
155
+ failures << { :server => server, :error => err }
156
+ end
157
+ end
158
+ end
159
+ end