wulffeld-capistrano 2.5.8
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.
- data/CHANGELOG.rdoc +761 -0
- data/Manifest +104 -0
- data/README.rdoc +66 -0
- data/Rakefile +34 -0
- data/bin/cap +4 -0
- data/bin/capify +78 -0
- data/examples/sample.rb +14 -0
- data/lib/capistrano.rb +2 -0
- data/lib/capistrano/callback.rb +45 -0
- data/lib/capistrano/cli.rb +47 -0
- data/lib/capistrano/cli/execute.rb +84 -0
- data/lib/capistrano/cli/help.rb +125 -0
- data/lib/capistrano/cli/help.txt +75 -0
- data/lib/capistrano/cli/options.rb +224 -0
- data/lib/capistrano/cli/ui.rb +40 -0
- data/lib/capistrano/command.rb +283 -0
- data/lib/capistrano/configuration.rb +43 -0
- data/lib/capistrano/configuration/actions/file_transfer.rb +47 -0
- data/lib/capistrano/configuration/actions/inspect.rb +46 -0
- data/lib/capistrano/configuration/actions/invocation.rb +293 -0
- data/lib/capistrano/configuration/callbacks.rb +148 -0
- data/lib/capistrano/configuration/connections.rb +200 -0
- data/lib/capistrano/configuration/execution.rb +132 -0
- data/lib/capistrano/configuration/loading.rb +197 -0
- data/lib/capistrano/configuration/namespaces.rb +197 -0
- data/lib/capistrano/configuration/roles.rb +73 -0
- data/lib/capistrano/configuration/servers.rb +85 -0
- data/lib/capistrano/configuration/variables.rb +127 -0
- data/lib/capistrano/errors.rb +15 -0
- data/lib/capistrano/extensions.rb +57 -0
- data/lib/capistrano/logger.rb +59 -0
- data/lib/capistrano/processable.rb +53 -0
- data/lib/capistrano/recipes/compat.rb +32 -0
- data/lib/capistrano/recipes/deploy.rb +562 -0
- data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
- data/lib/capistrano/recipes/deploy/local_dependency.rb +54 -0
- data/lib/capistrano/recipes/deploy/remote_dependency.rb +105 -0
- data/lib/capistrano/recipes/deploy/scm.rb +19 -0
- data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
- data/lib/capistrano/recipes/deploy/scm/base.rb +196 -0
- data/lib/capistrano/recipes/deploy/scm/bzr.rb +83 -0
- data/lib/capistrano/recipes/deploy/scm/cvs.rb +152 -0
- data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
- data/lib/capistrano/recipes/deploy/scm/git.rb +302 -0
- data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
- data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
- data/lib/capistrano/recipes/deploy/scm/perforce.rb +133 -0
- data/lib/capistrano/recipes/deploy/scm/subversion.rb +121 -0
- data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
- data/lib/capistrano/recipes/deploy/strategy/base.rb +79 -0
- data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
- data/lib/capistrano/recipes/deploy/strategy/copy.rb +210 -0
- data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
- data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
- data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +56 -0
- data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/recipes/standard.rb +37 -0
- data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/recipes/upgrade.rb +33 -0
- data/lib/capistrano/role.rb +102 -0
- data/lib/capistrano/server_definition.rb +56 -0
- data/lib/capistrano/shell.rb +260 -0
- data/lib/capistrano/ssh.rb +99 -0
- data/lib/capistrano/task_definition.rb +70 -0
- data/lib/capistrano/transfer.rb +216 -0
- data/lib/capistrano/version.rb +18 -0
- data/setup.rb +1346 -0
- data/test/cli/execute_test.rb +132 -0
- data/test/cli/help_test.rb +165 -0
- data/test/cli/options_test.rb +317 -0
- data/test/cli/ui_test.rb +28 -0
- data/test/cli_test.rb +17 -0
- data/test/command_test.rb +286 -0
- data/test/configuration/actions/file_transfer_test.rb +61 -0
- data/test/configuration/actions/inspect_test.rb +65 -0
- data/test/configuration/actions/invocation_test.rb +224 -0
- data/test/configuration/callbacks_test.rb +220 -0
- data/test/configuration/connections_test.rb +349 -0
- data/test/configuration/execution_test.rb +175 -0
- data/test/configuration/loading_test.rb +132 -0
- data/test/configuration/namespace_dsl_test.rb +311 -0
- data/test/configuration/roles_test.rb +144 -0
- data/test/configuration/servers_test.rb +121 -0
- data/test/configuration/variables_test.rb +184 -0
- data/test/configuration_test.rb +88 -0
- data/test/deploy/local_dependency_test.rb +76 -0
- data/test/deploy/remote_dependency_test.rb +114 -0
- data/test/deploy/scm/accurev_test.rb +23 -0
- data/test/deploy/scm/base_test.rb +55 -0
- data/test/deploy/scm/git_test.rb +167 -0
- data/test/deploy/scm/mercurial_test.rb +129 -0
- data/test/deploy/strategy/copy_test.rb +258 -0
- data/test/extensions_test.rb +69 -0
- data/test/fixtures/cli_integration.rb +5 -0
- data/test/fixtures/config.rb +5 -0
- data/test/fixtures/custom.rb +3 -0
- data/test/logger_test.rb +123 -0
- data/test/role_test.rb +11 -0
- data/test/server_definition_test.rb +121 -0
- data/test/shell_test.rb +90 -0
- data/test/ssh_test.rb +104 -0
- data/test/task_definition_test.rb +101 -0
- data/test/transfer_test.rb +160 -0
- data/test/utils.rb +38 -0
- metadata +207 -0
|
@@ -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,200 @@
|
|
|
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
|
+
if ENV['HOSTFILTER']
|
|
135
|
+
logger.info "skipping `#{task.fully_qualified_name}' because no servers matched"
|
|
136
|
+
return
|
|
137
|
+
else
|
|
138
|
+
raise Capistrano::NoMatchingServersError, "`#{task.fully_qualified_name}' is only run for servers matching #{task.options.inspect}, but no servers matched"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if task.continue_on_error?
|
|
143
|
+
servers.delete_if { |s| has_failed?(s) }
|
|
144
|
+
return if servers.empty?
|
|
145
|
+
end
|
|
146
|
+
else
|
|
147
|
+
servers = find_servers(options)
|
|
148
|
+
raise Capistrano::NoMatchingServersError, "no servers found to match #{options.inspect}" if servers.empty?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
servers = [servers.first] if options[:once]
|
|
152
|
+
logger.trace "servers: #{servers.map { |s| s.host }.inspect}"
|
|
153
|
+
|
|
154
|
+
max_hosts = (options[:max_hosts] || (task && task.max_hosts) || servers.size).to_i
|
|
155
|
+
is_subset = max_hosts < servers.size
|
|
156
|
+
|
|
157
|
+
# establish connections to those servers in groups of max_hosts, as necessary
|
|
158
|
+
servers.each_slice(max_hosts) do |servers_slice|
|
|
159
|
+
begin
|
|
160
|
+
establish_connections_to(servers_slice)
|
|
161
|
+
rescue ConnectionError => error
|
|
162
|
+
raise error unless task && task.continue_on_error?
|
|
163
|
+
error.hosts.each do |h|
|
|
164
|
+
servers_slice.delete(h)
|
|
165
|
+
failed!(h)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
begin
|
|
170
|
+
yield servers_slice
|
|
171
|
+
rescue RemoteError => error
|
|
172
|
+
raise error unless task && task.continue_on_error?
|
|
173
|
+
error.hosts.each { |h| failed!(h) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# if dealing with a subset (e.g., :max_hosts is less than the
|
|
177
|
+
# number of servers available) teardown the subset of connections
|
|
178
|
+
# that were just made, so that we can make room for the next subset.
|
|
179
|
+
teardown_connections_to(servers_slice) if is_subset
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
# We establish the connection by creating a thread in a new method--this
|
|
186
|
+
# prevents problems with the thread's scope seeing the wrong 'server'
|
|
187
|
+
# variable if the thread just happens to take too long to start up.
|
|
188
|
+
def establish_connection_to(server, failures=nil)
|
|
189
|
+
Thread.new { safely_establish_connection_to(server, failures) }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def safely_establish_connection_to(server, failures=nil)
|
|
193
|
+
sessions[server] ||= connection_factory.connect_to(server)
|
|
194
|
+
rescue Exception => err
|
|
195
|
+
raise unless failures
|
|
196
|
+
failures << { :server => server, :error => err }
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require 'capistrano/errors'
|
|
2
|
+
|
|
3
|
+
module Capistrano
|
|
4
|
+
class Configuration
|
|
5
|
+
module Execution
|
|
6
|
+
def self.included(base) #:nodoc:
|
|
7
|
+
base.send :alias_method, :initialize_without_execution, :initialize
|
|
8
|
+
base.send :alias_method, :initialize, :initialize_with_execution
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# The call stack of the tasks. The currently executing task may inspect
|
|
12
|
+
# this to see who its caller was. The current task is always the last
|
|
13
|
+
# element of this stack.
|
|
14
|
+
attr_reader :task_call_frames
|
|
15
|
+
|
|
16
|
+
# The stack of tasks that have registered rollback handlers within the
|
|
17
|
+
# current transaction. If this is nil, then there is no transaction
|
|
18
|
+
# that is currently active.
|
|
19
|
+
attr_reader :rollback_requests
|
|
20
|
+
|
|
21
|
+
# A struct for representing a single instance of an invoked task.
|
|
22
|
+
TaskCallFrame = Struct.new(:task, :rollback)
|
|
23
|
+
|
|
24
|
+
def initialize_with_execution(*args) #:nodoc:
|
|
25
|
+
initialize_without_execution(*args)
|
|
26
|
+
@task_call_frames = []
|
|
27
|
+
end
|
|
28
|
+
private :initialize_with_execution
|
|
29
|
+
|
|
30
|
+
# Returns true if there is a transaction currently active.
|
|
31
|
+
def transaction?
|
|
32
|
+
!rollback_requests.nil?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Invoke a set of tasks in a transaction. If any task fails (raises an
|
|
36
|
+
# exception), all tasks executed within the transaction are inspected to
|
|
37
|
+
# see if they have an associated on_rollback hook, and if so, that hook
|
|
38
|
+
# is called.
|
|
39
|
+
def transaction
|
|
40
|
+
raise ArgumentError, "expected a block" unless block_given?
|
|
41
|
+
raise ScriptError, "transaction must be called from within a task" if task_call_frames.empty?
|
|
42
|
+
|
|
43
|
+
return yield if transaction?
|
|
44
|
+
|
|
45
|
+
logger.info "transaction: start"
|
|
46
|
+
begin
|
|
47
|
+
@rollback_requests = []
|
|
48
|
+
yield
|
|
49
|
+
logger.info "transaction: commit"
|
|
50
|
+
rescue Object => e
|
|
51
|
+
rollback!
|
|
52
|
+
raise
|
|
53
|
+
ensure
|
|
54
|
+
@rollback_requests = nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Specifies an on_rollback hook for the currently executing task. If this
|
|
59
|
+
# or any subsequent task then fails, and a transaction is active, this
|
|
60
|
+
# hook will be executed.
|
|
61
|
+
def on_rollback(&block)
|
|
62
|
+
if transaction?
|
|
63
|
+
# don't note a new rollback request if one has already been set
|
|
64
|
+
rollback_requests << task_call_frames.last unless task_call_frames.last.rollback
|
|
65
|
+
task_call_frames.last.rollback = block
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns the TaskDefinition object for the currently executing task.
|
|
70
|
+
# It returns nil if there is no task being executed.
|
|
71
|
+
def current_task
|
|
72
|
+
return nil if task_call_frames.empty?
|
|
73
|
+
task_call_frames.last.task
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Executes the task with the given name, without invoking any associated
|
|
77
|
+
# callbacks.
|
|
78
|
+
def execute_task(task)
|
|
79
|
+
logger.debug "executing `#{task.fully_qualified_name}'"
|
|
80
|
+
push_task_call_frame(task)
|
|
81
|
+
invoke_task_directly(task)
|
|
82
|
+
ensure
|
|
83
|
+
pop_task_call_frame
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Attempts to locate the task at the given fully-qualified path, and
|
|
87
|
+
# execute it. If no such task exists, a Capistrano::NoSuchTaskError will
|
|
88
|
+
# be raised.
|
|
89
|
+
def find_and_execute_task(path, hooks={})
|
|
90
|
+
task = find_task(path) or raise NoSuchTaskError, "the task `#{path}' does not exist"
|
|
91
|
+
|
|
92
|
+
trigger(hooks[:before], task) if hooks[:before]
|
|
93
|
+
result = execute_task(task)
|
|
94
|
+
trigger(hooks[:after], task) if hooks[:after]
|
|
95
|
+
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
protected
|
|
100
|
+
|
|
101
|
+
def rollback!
|
|
102
|
+
# throw the task back on the stack so that roles are properly
|
|
103
|
+
# interpreted in the scope of the task in question.
|
|
104
|
+
rollback_requests.reverse.each do |frame|
|
|
105
|
+
begin
|
|
106
|
+
push_task_call_frame(frame.task)
|
|
107
|
+
logger.important "rolling back", frame.task.fully_qualified_name
|
|
108
|
+
frame.rollback.call
|
|
109
|
+
rescue Object => e
|
|
110
|
+
logger.info "exception while rolling back: #{e.class}, #{e.message}", frame.task.fully_qualified_name
|
|
111
|
+
ensure
|
|
112
|
+
pop_task_call_frame
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def push_task_call_frame(task)
|
|
118
|
+
frame = TaskCallFrame.new(task)
|
|
119
|
+
task_call_frames.push frame
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def pop_task_call_frame
|
|
123
|
+
task_call_frames.pop
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Invokes the task's body directly, without setting up the call frame.
|
|
127
|
+
def invoke_task_directly(task)
|
|
128
|
+
task.namespace.instance_eval(&task.body)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|