capistrano 2.2.0 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +29 -0
- data/README +4 -7
- data/bin/cap +0 -0
- data/bin/capify +0 -0
- data/lib/capistrano/command.rb +32 -38
- data/lib/capistrano/configuration/actions/file_transfer.rb +21 -13
- data/lib/capistrano/configuration/actions/invocation.rb +1 -1
- data/lib/capistrano/configuration/connections.rb +30 -20
- data/lib/capistrano/errors.rb +1 -1
- data/lib/capistrano/processable.rb +53 -0
- data/lib/capistrano/recipes/deploy/remote_dependency.rb +6 -0
- data/lib/capistrano/recipes/deploy/scm/git.rb +26 -11
- data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
- data/lib/capistrano/recipes/deploy/strategy/base.rb +6 -0
- data/lib/capistrano/recipes/deploy/strategy/copy.rb +74 -3
- data/lib/capistrano/recipes/deploy.rb +15 -13
- data/lib/capistrano/role.rb +0 -14
- data/lib/capistrano/server_definition.rb +5 -0
- data/lib/capistrano/shell.rb +21 -17
- data/lib/capistrano/ssh.rb +24 -58
- data/lib/capistrano/transfer.rb +216 -0
- data/lib/capistrano/version.rb +1 -1
- data/test/cli/execute_test.rb +1 -1
- data/test/cli/help_test.rb +1 -1
- data/test/cli/options_test.rb +1 -1
- data/test/cli/ui_test.rb +1 -1
- data/test/cli_test.rb +1 -1
- data/test/command_test.rb +31 -51
- data/test/configuration/actions/file_transfer_test.rb +21 -19
- data/test/configuration/actions/inspect_test.rb +1 -1
- data/test/configuration/actions/invocation_test.rb +6 -6
- data/test/configuration/callbacks_test.rb +1 -1
- data/test/configuration/connections_test.rb +11 -12
- data/test/configuration/execution_test.rb +1 -1
- data/test/configuration/loading_test.rb +1 -1
- data/test/configuration/namespace_dsl_test.rb +1 -1
- data/test/configuration/roles_test.rb +1 -1
- data/test/configuration/servers_test.rb +1 -1
- data/test/configuration/variables_test.rb +1 -1
- data/test/configuration_test.rb +1 -1
- data/test/deploy/scm/accurev_test.rb +1 -1
- data/test/deploy/scm/base_test.rb +1 -1
- data/test/deploy/scm/git_test.rb +10 -6
- data/test/deploy/scm/mercurial_test.rb +1 -1
- data/test/deploy/strategy/copy_test.rb +120 -27
- data/test/extensions_test.rb +1 -1
- data/test/logger_test.rb +1 -1
- data/test/server_definition_test.rb +1 -1
- data/test/shell_test.rb +27 -1
- data/test/ssh_test.rb +27 -21
- data/test/task_definition_test.rb +1 -1
- data/test/transfer_test.rb +160 -0
- data/test/utils.rb +30 -34
- data/test/version_test.rb +1 -1
- metadata +26 -14
- data/lib/capistrano/gateway.rb +0 -131
- data/lib/capistrano/upload.rb +0 -152
- data/test/gateway_test.rb +0 -167
- data/test/upload_test.rb +0 -131
@@ -89,6 +89,15 @@ ensure
|
|
89
89
|
ENV[name] = saved
|
90
90
|
end
|
91
91
|
|
92
|
+
# If :run_method is :sudo (or :use_sudo is true), this executes the given command
|
93
|
+
# via +sudo+. Otherwise is uses +run+. Further, if sudo is being used and :runner
|
94
|
+
# is set, the command will be executed as the user given by :runner.
|
95
|
+
def try_sudo(command)
|
96
|
+
as = fetch(:runner, "app")
|
97
|
+
via = fetch(:run_method, :sudo)
|
98
|
+
invoke_command(command, :via => via, :as => as)
|
99
|
+
end
|
100
|
+
|
92
101
|
# =========================================================================
|
93
102
|
# These are the tasks that are available to help with deploying web apps,
|
94
103
|
# and specifically, Rails applications. You can have cap give you a summary
|
@@ -122,7 +131,7 @@ namespace :deploy do
|
|
122
131
|
task :setup, :except => { :no_release => true } do
|
123
132
|
dirs = [deploy_to, releases_path, shared_path]
|
124
133
|
dirs += %w(system log pids).map { |d| File.join(shared_path, d) }
|
125
|
-
|
134
|
+
try_sudo "umask 02 && mkdir -p #{dirs.join(' ')}"
|
126
135
|
end
|
127
136
|
|
128
137
|
desc <<-DESC
|
@@ -246,9 +255,7 @@ namespace :deploy do
|
|
246
255
|
set :use_sudo, false
|
247
256
|
DESC
|
248
257
|
task :restart, :roles => :app, :except => { :no_release => true } do
|
249
|
-
|
250
|
-
via = fetch(:run_method, :sudo)
|
251
|
-
invoke_command "#{current_path}/script/process/reaper", :via => via, :as => as
|
258
|
+
try_sudo "#{current_path}/script/process/reaper"
|
252
259
|
end
|
253
260
|
|
254
261
|
desc <<-DESC
|
@@ -337,7 +344,7 @@ namespace :deploy do
|
|
337
344
|
directories = (releases - releases.last(count)).map { |release|
|
338
345
|
File.join(releases_path, release) }.join(" ")
|
339
346
|
|
340
|
-
|
347
|
+
try_sudo "rm -rf #{directories}"
|
341
348
|
end
|
342
349
|
end
|
343
350
|
|
@@ -406,9 +413,7 @@ namespace :deploy do
|
|
406
413
|
the :use_sudo variable to false.
|
407
414
|
DESC
|
408
415
|
task :start, :roles => :app do
|
409
|
-
|
410
|
-
via = fetch(:run_method, :sudo)
|
411
|
-
invoke_command "sh -c 'cd #{current_path} && nohup script/spin'", :via => via, :as => as
|
416
|
+
try_sudo "sh -c 'cd #{current_path} && nohup script/spin'"
|
412
417
|
end
|
413
418
|
|
414
419
|
desc <<-DESC
|
@@ -423,11 +428,8 @@ namespace :deploy do
|
|
423
428
|
the :use_sudo variable to false.
|
424
429
|
DESC
|
425
430
|
task :stop, :roles => :app do
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
invoke_command "if [ -f #{current_path}/tmp/pids/dispatch.spawner.pid ]; then #{current_path}/script/process/reaper -a kill -r dispatch.spawner.pid; fi", :via => via, :as => as
|
430
|
-
invoke_command "#{current_path}/script/process/reaper -a kill", :via => via, :as => as
|
431
|
+
try_sudo "if [ -f #{current_path}/tmp/pids/dispatch.spawner.pid ]; then #{current_path}/script/process/reaper -a kill -r dispatch.spawner.pid; fi"
|
432
|
+
try_sudo "#{current_path}/script/process/reaper -a kill"
|
431
433
|
end
|
432
434
|
|
433
435
|
namespace :pending do
|
data/lib/capistrano/role.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
module Capistrano
|
3
2
|
class Role
|
4
3
|
include Enumerable
|
@@ -34,27 +33,14 @@ module Capistrano
|
|
34
33
|
servers.empty?
|
35
34
|
end
|
36
35
|
|
37
|
-
# Resets the cache, so that proc values may be recalculated.
|
38
|
-
# There should be a command in Configuration::Roles to do this,
|
39
|
-
# but I haven't needed it yet, and I'm not sure yet
|
40
|
-
# what to call that command. Suggestions?
|
41
|
-
def reset!
|
42
|
-
@dynamic_servers.each { |item| item.reset! }
|
43
|
-
end
|
44
|
-
|
45
|
-
# Clears everything. I still thing this should be 'clear!', but that's not
|
46
|
-
# the way Array does it.
|
47
36
|
def clear
|
48
37
|
@dynamic_servers.clear
|
49
38
|
@static_servers.clear
|
50
39
|
end
|
51
40
|
|
52
|
-
# Mostly for documentation purposes. Doesn't seem to do anything.
|
53
41
|
protected
|
54
42
|
|
55
43
|
# This is the combination of a block, a hash of options, and a cached value.
|
56
|
-
# It is protected because it is an implementation detail -- the original
|
57
|
-
# implementation was two lists (blocks and cached results of calling them).
|
58
44
|
class DynamicServerList
|
59
45
|
def initialize (block, options)
|
60
46
|
@block = block
|
@@ -7,6 +7,11 @@ module Capistrano
|
|
7
7
|
attr_reader :port
|
8
8
|
attr_reader :options
|
9
9
|
|
10
|
+
# The default user name to use when a user name is not explicitly provided
|
11
|
+
def self.default_user
|
12
|
+
ENV['USER'] || ENV['USERNAME'] || "not-specified"
|
13
|
+
end
|
14
|
+
|
10
15
|
def initialize(string, options={})
|
11
16
|
@user, @host, @port = string.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3]
|
12
17
|
|
data/lib/capistrano/shell.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'thread'
|
2
|
+
require 'capistrano/processable'
|
2
3
|
|
3
4
|
module Capistrano
|
4
5
|
# The Capistrano::Shell class is the guts of the "shell" task. It implements
|
@@ -6,6 +7,8 @@ module Capistrano
|
|
6
7
|
# commands. It makes for a GREAT way to monitor systems, and perform quick
|
7
8
|
# maintenance on one or more machines.
|
8
9
|
class Shell
|
10
|
+
include Processable
|
11
|
+
|
9
12
|
# A Readline replacement for platforms where readline is either
|
10
13
|
# unavailable, or has not been installed.
|
11
14
|
class ReadlineFallback #:nodoc:
|
@@ -142,11 +145,13 @@ HELP
|
|
142
145
|
# be invoked. Otherwise, it is executed as a command on all associated
|
143
146
|
# servers.
|
144
147
|
def exec(command)
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
148
|
+
@mutex.synchronize do
|
149
|
+
if command[0] == ?!
|
150
|
+
exec_tasks(command[1..-1].split)
|
151
|
+
else
|
152
|
+
servers = connect(configuration.current_task)
|
153
|
+
exec_command(command, servers)
|
154
|
+
end
|
150
155
|
end
|
151
156
|
ensure
|
152
157
|
STDOUT.flush
|
@@ -169,7 +174,8 @@ HELP
|
|
169
174
|
command = command.gsub(/\bsudo\b/, "sudo -p '#{configuration.sudo_prompt}'")
|
170
175
|
processor = configuration.sudo_behavior_callback(Configuration.default_io_proc)
|
171
176
|
sessions = servers.map { |server| configuration.sessions[server] }
|
172
|
-
|
177
|
+
options = configuration.add_default_command_options({})
|
178
|
+
cmd = Command.new(command, sessions, options.merge(:logger => configuration.logger), &processor)
|
173
179
|
previous = trap("INT") { cmd.stop! }
|
174
180
|
cmd.process!
|
175
181
|
rescue Capistrano::Error => error
|
@@ -196,17 +202,10 @@ HELP
|
|
196
202
|
|
197
203
|
@mutex = Mutex.new
|
198
204
|
@bgthread = Thread.new do
|
199
|
-
|
200
|
-
|
201
|
-
if ready.empty?
|
202
|
-
sleep 0.1
|
203
|
-
else
|
204
|
-
@mutex.synchronize do
|
205
|
-
ready.each { |session| session.connection.process(true) }
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
205
|
+
loop do
|
206
|
+
@mutex.synchronize { process_iteration(0.1) }
|
209
207
|
end
|
208
|
+
end
|
210
209
|
end
|
211
210
|
|
212
211
|
# Set the given option to +value+.
|
@@ -244,7 +243,7 @@ HELP
|
|
244
243
|
old_var, ENV[env_var] = ENV[env_var], (scope_value == "all" ? nil : scope_value) if env_var
|
245
244
|
if command
|
246
245
|
begin
|
247
|
-
|
246
|
+
exec(command)
|
248
247
|
ensure
|
249
248
|
ENV[env_var] = old_var if env_var
|
250
249
|
end
|
@@ -253,4 +252,9 @@ HELP
|
|
253
252
|
end
|
254
253
|
end
|
255
254
|
end
|
255
|
+
|
256
|
+
# All open sessions, needed to satisfy the Command::Processable include
|
257
|
+
def sessions
|
258
|
+
configuration.sessions.values
|
259
|
+
end
|
256
260
|
end
|
data/lib/capistrano/ssh.rb
CHANGED
@@ -1,61 +1,12 @@
|
|
1
1
|
begin
|
2
2
|
require 'rubygems'
|
3
|
-
gem 'net-ssh', "
|
3
|
+
gem 'net-ssh', ">= 1.99.1"
|
4
4
|
rescue LoadError, NameError
|
5
5
|
end
|
6
6
|
|
7
7
|
require 'net/ssh'
|
8
8
|
|
9
9
|
module Capistrano
|
10
|
-
unless ENV['SKIP_VERSION_CHECK']
|
11
|
-
require 'capistrano/version'
|
12
|
-
require 'net/ssh/version'
|
13
|
-
ssh_version = [Net::SSH::Version::MAJOR, Net::SSH::Version::MINOR, Net::SSH::Version::TINY]
|
14
|
-
if !Version.check(Version::SSH_REQUIRED, ssh_version)
|
15
|
-
raise "You have Net::SSH #{ssh_version.join(".")}, but you need at least #{Version::SSH_REQUIRED.join(".")}"
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
# Now, Net::SSH is kind of silly, and tries to lazy-load everything. This
|
20
|
-
# wreaks havoc with the parallel connection trick that Capistrano wants to
|
21
|
-
# use, so we're going to do something hideously ugly here and force all the
|
22
|
-
# files that Net::SSH uses to load RIGHT NOW, rather than lazily.
|
23
|
-
|
24
|
-
net_ssh_dependencies = %w(connection/services connection/channel connection/driver
|
25
|
-
service/agentforward/services service/agentforward/driver
|
26
|
-
service/process/driver util/prompter
|
27
|
-
service/forward/services service/forward/driver service/forward/local-network-handler service/forward/remote-network-handler
|
28
|
-
service/shell/services service/shell/driver
|
29
|
-
lenient-host-key-verifier
|
30
|
-
transport/compress/services transport/compress/zlib-compressor transport/compress/none-compressor transport/compress/zlib-decompressor transport/compress/none-decompressor
|
31
|
-
transport/kex/services transport/kex/dh transport/kex/dh-gex
|
32
|
-
transport/ossl/services
|
33
|
-
transport/ossl/hmac/services transport/ossl/hmac/sha1 transport/ossl/hmac/sha1-96 transport/ossl/hmac/md5 transport/ossl/hmac/md5-96 transport/ossl/hmac/none
|
34
|
-
transport/ossl/cipher-factory transport/ossl/hmac-factory transport/ossl/buffer-factory transport/ossl/key-factory transport/ossl/digest-factory
|
35
|
-
transport/identity-cipher transport/packet-stream transport/version-negotiator transport/algorithm-negotiator transport/session
|
36
|
-
userauth/methods/services userauth/methods/password userauth/methods/keyboard-interactive userauth/methods/publickey userauth/methods/hostbased
|
37
|
-
userauth/services userauth/agent userauth/userkeys userauth/driver
|
38
|
-
transport/services service/services
|
39
|
-
)
|
40
|
-
|
41
|
-
net_ssh_dependencies << "userauth/pageant" if File::ALT_SEPARATOR
|
42
|
-
net_ssh_dependencies.each do |path|
|
43
|
-
begin
|
44
|
-
require "net/ssh/#{path}"
|
45
|
-
rescue LoadError
|
46
|
-
# Ignore load errors from this, since some files are in the list which
|
47
|
-
# do not exist in different (supported) versions of Net::SSH. We know
|
48
|
-
# (by this point) that Net::SSH is installed, though, since we do a
|
49
|
-
# require 'net/ssh' at the very top of this file, and we know the
|
50
|
-
# installed version meets the minimum version requirements because of
|
51
|
-
# the version check, also at the top of this file. So, if we get a
|
52
|
-
# LoadError, it's simply because the file in question does not exist in
|
53
|
-
# the version of Net::SSH that is installed.
|
54
|
-
#
|
55
|
-
# Whew!
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
10
|
# A helper class for dealing with SSH connections.
|
60
11
|
class SSH
|
61
12
|
# Patch an accessor onto an SSH connection so that we can record the server
|
@@ -88,13 +39,30 @@ module Capistrano
|
|
88
39
|
# constructor. Values in +options+ are then merged into it, and any
|
89
40
|
# connection information in +server+ is added last, so that +server+ info
|
90
41
|
# takes precedence over +options+, which takes precendence over ssh_options.
|
91
|
-
def self.connect(server, options={}
|
42
|
+
def self.connect(server, options={})
|
43
|
+
connection_strategy(server, options) do |host, user, connection_options|
|
44
|
+
connection = Net::SSH.start(host, user, connection_options)
|
45
|
+
Server.apply_to(connection, server)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Abstracts the logic for establishing an SSH connection (which includes
|
50
|
+
# testing for connection failures and retrying with a password, and so forth,
|
51
|
+
# mostly made complicated because of the fact that some of these variables
|
52
|
+
# might be lazily evaluated and try to do something like prompt the user,
|
53
|
+
# which should only happen when absolutely necessary.
|
54
|
+
#
|
55
|
+
# This will yield the hostname, username, and a hash of connection options
|
56
|
+
# to the given block, which should return a new connection.
|
57
|
+
def self.connection_strategy(server, options={}, &block)
|
92
58
|
methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ]
|
93
59
|
password_value = nil
|
94
|
-
|
95
|
-
ssh_options
|
96
|
-
|
97
|
-
ssh_options[:port]
|
60
|
+
|
61
|
+
ssh_options = (server.options[:ssh_options] || {}).merge(options[:ssh_options] || {})
|
62
|
+
user = server.user || options[:user] || ssh_options[:username] || ServerDefinition.default_user
|
63
|
+
ssh_options[:port] = server.port || options[:port] || ssh_options[:port] || DEFAULT_PORT
|
64
|
+
|
65
|
+
ssh_options.delete(:username)
|
98
66
|
|
99
67
|
begin
|
100
68
|
connection_options = ssh_options.merge(
|
@@ -102,9 +70,7 @@ module Capistrano
|
|
102
70
|
:auth_methods => ssh_options[:auth_methods] || methods.shift
|
103
71
|
)
|
104
72
|
|
105
|
-
|
106
|
-
Server.apply_to(connection, server)
|
107
|
-
|
73
|
+
yield server.host, user, connection_options
|
108
74
|
rescue Net::SSH::AuthenticationFailed
|
109
75
|
raise if methods.empty? || ssh_options[:auth_methods]
|
110
76
|
password_value = options[:password]
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'net/scp'
|
2
|
+
require 'net/sftp'
|
3
|
+
|
4
|
+
require 'capistrano/processable'
|
5
|
+
|
6
|
+
module Capistrano
|
7
|
+
class Transfer
|
8
|
+
include Processable
|
9
|
+
|
10
|
+
def self.process(direction, from, to, sessions, options={}, &block)
|
11
|
+
new(direction, from, to, sessions, options, &block).process!
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :sessions
|
15
|
+
attr_reader :options
|
16
|
+
attr_reader :callback
|
17
|
+
|
18
|
+
attr_reader :transport
|
19
|
+
attr_reader :direction
|
20
|
+
attr_reader :from
|
21
|
+
attr_reader :to
|
22
|
+
|
23
|
+
attr_reader :logger
|
24
|
+
attr_reader :transfers
|
25
|
+
|
26
|
+
def initialize(direction, from, to, sessions, options={}, &block)
|
27
|
+
@direction = direction
|
28
|
+
@from = from
|
29
|
+
@to = to
|
30
|
+
@sessions = sessions
|
31
|
+
@options = options
|
32
|
+
@callback = callback
|
33
|
+
|
34
|
+
@transport = options.fetch(:via, :sftp)
|
35
|
+
@logger = options.delete(:logger)
|
36
|
+
|
37
|
+
@session_map = {}
|
38
|
+
|
39
|
+
prepare_transfers
|
40
|
+
end
|
41
|
+
|
42
|
+
def process!
|
43
|
+
loop do
|
44
|
+
begin
|
45
|
+
break unless process_iteration { active? }
|
46
|
+
rescue Exception => error
|
47
|
+
if error.respond_to?(:session)
|
48
|
+
handle_error(error)
|
49
|
+
else
|
50
|
+
raise
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
failed = transfers.select { |txfr| txfr[:failed] }
|
56
|
+
if failed.any?
|
57
|
+
hosts = failed.map { |txfr| txfr[:server] }
|
58
|
+
errors = failed.map { |txfr| "#{txfr[:error]} (#{txfr[:error].message})" }.uniq.join(", ")
|
59
|
+
error = TransferError.new("#{operation} via #{transport} failed on #{hosts.join(',')}: #{errors}")
|
60
|
+
error.hosts = hosts
|
61
|
+
|
62
|
+
logger.important(error.message) if logger
|
63
|
+
raise error
|
64
|
+
end
|
65
|
+
|
66
|
+
logger.debug "#{transport} #{operation} complete" if logger
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def active?
|
71
|
+
transfers.any? { |transfer| transfer.active? }
|
72
|
+
end
|
73
|
+
|
74
|
+
def operation
|
75
|
+
"#{direction}load"
|
76
|
+
end
|
77
|
+
|
78
|
+
def sanitized_from
|
79
|
+
if from.responds_to?(:read)
|
80
|
+
"#<#{from.class}>"
|
81
|
+
else
|
82
|
+
from
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def sanitized_to
|
87
|
+
if to.responds_to?(:read)
|
88
|
+
"#<#{to.class}>"
|
89
|
+
else
|
90
|
+
to
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def session_map
|
97
|
+
@session_map
|
98
|
+
end
|
99
|
+
|
100
|
+
def prepare_transfers
|
101
|
+
logger.info "#{transport} #{operation} #{from} -> #{to}" if logger
|
102
|
+
|
103
|
+
@transfers = sessions.map do |session|
|
104
|
+
session_from = normalize(from, session)
|
105
|
+
session_to = normalize(to, session)
|
106
|
+
|
107
|
+
session_map[session] = case transport
|
108
|
+
when :sftp
|
109
|
+
prepare_sftp_transfer(session_from, session_to, session)
|
110
|
+
when :scp
|
111
|
+
prepare_scp_transfer(session_from, session_to, session)
|
112
|
+
else
|
113
|
+
raise ArgumentError, "unsupported transport type: #{transport.inspect}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def prepare_scp_transfer(from, to, session)
|
119
|
+
real_callback = callback || Proc.new do |channel, name, sent, total|
|
120
|
+
logger.trace "[#{channel[:host]}] #{name}" if logger && sent == 0
|
121
|
+
end
|
122
|
+
|
123
|
+
channel = case direction
|
124
|
+
when :up
|
125
|
+
session.scp.upload(from, to, options, &real_callback)
|
126
|
+
when :down
|
127
|
+
session.scp.download(from, to, options, &real_callback)
|
128
|
+
else
|
129
|
+
raise ArgumentError, "unsupported transfer direction: #{direction.inspect}"
|
130
|
+
end
|
131
|
+
|
132
|
+
channel[:server] = session.xserver
|
133
|
+
channel[:host] = session.xserver.host
|
134
|
+
|
135
|
+
return channel
|
136
|
+
end
|
137
|
+
|
138
|
+
class SFTPTransferWrapper
|
139
|
+
attr_reader :operation
|
140
|
+
|
141
|
+
def initialize(session, &callback)
|
142
|
+
session.sftp(false).connect do |sftp|
|
143
|
+
@operation = callback.call(sftp)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def active?
|
148
|
+
@operation.nil? || @operation.active?
|
149
|
+
end
|
150
|
+
|
151
|
+
def [](key)
|
152
|
+
@operation[key]
|
153
|
+
end
|
154
|
+
|
155
|
+
def []=(key, value)
|
156
|
+
@operation[key] = value
|
157
|
+
end
|
158
|
+
|
159
|
+
def abort!
|
160
|
+
@operation.abort!
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def prepare_sftp_transfer(from, to, session)
|
165
|
+
SFTPTransferWrapper.new(session) do |sftp|
|
166
|
+
real_callback = Proc.new do |event, op, *args|
|
167
|
+
if callback
|
168
|
+
callback.call(event, op, *args)
|
169
|
+
elsif event == :open
|
170
|
+
logger.trace "[#{op[:host]}] #{args[0].remote}"
|
171
|
+
elsif event == :finish
|
172
|
+
logger.trace "[#{op[:host]}] done"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
opts = options.dup
|
177
|
+
opts[:properties] = (opts[:properties] || {}).merge(
|
178
|
+
:server => session.xserver,
|
179
|
+
:host => session.xserver.host)
|
180
|
+
|
181
|
+
case direction
|
182
|
+
when :up
|
183
|
+
sftp.upload(from, to, opts, &real_callback)
|
184
|
+
when :down
|
185
|
+
sftp.download(from, to, opts, &real_callback)
|
186
|
+
else
|
187
|
+
raise ArgumentError, "unsupported transfer direction: #{direction.inspect}"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def normalize(argument, session)
|
193
|
+
if argument.is_a?(String)
|
194
|
+
argument.gsub(/\$CAPISTRANO:HOST\$/, session.xserver.host)
|
195
|
+
elsif argument.respond_to?(:read)
|
196
|
+
pos = argument.pos
|
197
|
+
clone = StringIO.new(argument.read)
|
198
|
+
clone.pos = argument.pos = pos
|
199
|
+
clone
|
200
|
+
else
|
201
|
+
argument
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def handle_error(error)
|
206
|
+
transfer = session_map[error.session]
|
207
|
+
transfer[:error] = error
|
208
|
+
transfer[:failed] = true
|
209
|
+
|
210
|
+
case transport
|
211
|
+
when :sftp then transfer.abort!
|
212
|
+
when :scp then transfer.close
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
data/lib/capistrano/version.rb
CHANGED
data/test/cli/execute_test.rb
CHANGED
data/test/cli/help_test.rb
CHANGED
data/test/cli/options_test.rb
CHANGED
data/test/cli/ui_test.rb
CHANGED
data/test/cli_test.rb
CHANGED