Permalink
Browse files

start refactoring the task helper actions

git-svn-id: http://svn.rubyonrails.org/rails/tools/capistrano@6288 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent acdd65f commit 472431004c5ffde7d1b0d2127269e98c6c05da03 @jamis jamis committed Mar 2, 2007
View
@@ -19,7 +19,6 @@ desc "Build documentation"
task :doc => [ :rdoc ]
Rake::TestTask.new do |t|
- t.ruby_opts << "-rubygems"
t.test_files = Dir["test/**/*_test.rb"]
t.verbose = true
end
View
@@ -13,42 +13,6 @@ module Capistrano
# directly--rather, you create a new Configuration instance, and access the
# new actor via Configuration#actor.
class Actor
- class <<self
- attr_accessor :default_io_proc
- end
-
- self.default_io_proc = Proc.new do |ch, stream, out|
- level = stream == :err ? :important : :info
- ch[:actor].logger.send(level, out, "#{stream} :: #{ch[:host]}")
- end
-
- def initialize(config) #:nodoc:
- @configuration = config
- @tasks = {}
- @task_call_frames = []
- @sessions = {}
- @factory = self.class.connection_factory.new(configuration)
- end
-
- # Execute the given command on all servers that are the target of the
- # current task. If a block is given, it is invoked for all output
- # generated by the command, and should accept three parameters: the SSH
- # channel (which may be used to send data back to the remote process),
- # the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
- # stdout), and the data that was received.
- #
- # If +pretend+ mode is active, this does nothing.
- def run(cmd, options={}, &block)
- block ||= default_io_proc
- logger.debug "executing #{cmd.strip.inspect}"
-
- execute_on_servers(options) do |servers|
- # execute the command on each server in parallel
- command = self.class.command_factory.new(servers, cmd, block, options, self)
- command.process! # raises an exception if command fails on any server
- end
- end
-
# Streams the result of the command from all servers that are the target of the
# current task. All these streams will be joined into a single one,
# so you can, say, watch 10 log files as though they were one. Do note that this
@@ -78,56 +42,6 @@ def delete(path, options={})
run(cmd, options)
end
- # Store the given data at the given location on all servers targetted by
- # the current task. If <tt>:mode</tt> is specified it is used to set the
- # mode on the file.
- def put(data, path, options={})
- if Capistrano::SFTP
- execute_on_servers(options) do |servers|
- transfer = self.class.transfer_factory.new(servers, self, path, :data => data,
- :mode => options[:mode])
- transfer.process!
- end
- else
- # Poor-man's SFTP... just run a cat on the remote end, and send data
- # to it.
-
- cmd = "cat > #{path}"
- cmd << " && chmod #{options[:mode].to_s(8)} #{path}" if options[:mode]
- run(cmd, options.merge(:data => data + "\n\4")) do |ch, stream, out|
- logger.important out, "#{stream} :: #{ch[:host]}" if stream == :err
- end
- end
- end
-
- # Get file remote_path from FIRST server targetted by
- # the current task and transfer it to local machine as path. It will use
- # SFTP if Net::SFTP is installed; otherwise it will fall back to using
- # 'cat', which may cause corruption in binary files.
- #
- # get "#{deploy_to}/current/log/production.log", "log/production.log.web"
- def get(remote_path, path, options = {})
- if Capistrano::SFTP && options.fetch(:sftp, true)
- execute_on_servers(options.merge(:once => true)) do |servers|
- logger.debug "downloading #{servers.first}:#{remote_path} to #{path}"
- sftp = sessions[servers.first].sftp
- sftp.connect unless sftp.state == :open
- sftp.get_file remote_path, path
- logger.trace "download finished"
- end
- else
- logger.important "Net::SFTP is not available; using remote 'cat' to get file, which may cause file corruption"
- File.open(path, "w") do |destination|
- run "cat #{remote_path}", :once => true do |ch, stream, data|
- case stream
- when :out then destination << data
- when :err then raise "error while downloading #{remote_path}: #{data.inspect}"
- end
- end
- end
- end
- end
-
# Executes the given command on the first server targetted by the current
# task, collects it's stdout into a string, and returns the string.
def capture(command, options={})
@@ -141,42 +55,6 @@ def capture(command, options={})
output
end
- # Like #run, but executes the command via <tt>sudo</tt>. This assumes that
- # the sudo password (if required) is the same as the password for logging
- # in to the server.
- #
- # Also, this module accepts a <tt>:sudo</tt> configuration variable,
- # which (if specified) will be used as the full path to the sudo
- # executable on the remote machine:
- #
- # set :sudo, "/opt/local/bin/sudo"
- def sudo(command, options={}, &block)
- block ||= default_io_proc
-
- # in order to prevent _each host_ from prompting when the password was
- # wrong, let's track which host prompted first and only allow subsequent
- # prompts from that host.
- prompt_host = nil
- user = options[:as].nil? ? '' : "-u #{options[:as]}"
-
- run "#{sudo_command} #{user} #{command}", options do |ch, stream, out|
- if out =~ /^Password:/
- ch.send_data "#{password}\n"
- elsif out =~ /try again/
- if prompt_host.nil? || prompt_host == ch[:host]
- prompt_host = ch[:host]
- logger.important out, "#{stream} :: #{ch[:host]}"
- # reset the password to it's original value and prepare for another
- # pass (the reset allows the password prompt to be attempted again
- # if the password variable was originally a proc (the default)
- set :password, self[:original_value][:password] || self[:password]
- end
- else
- block.call(ch, stream, out)
- end
- end
- end
-
# Renders an ERb template and returns the result. This is useful for
# dynamically building documents to store on the remote servers.
#
@@ -230,41 +108,5 @@ def render(*args)
raise ArgumentError, "no file or template given for rendering"
end
end
-
- # An instance-level reader for the class' #default_io_proc attribute.
- def default_io_proc
- self.class.default_io_proc
- end
-
- # Used to force connections to be made to the current task's servers.
- # Connections are normally made lazily in Capistrano--you can use this
- # to force them open before performing some operation that might be
- # time-sensitive.
- def connect!(options={})
- execute_on_servers(options) { }
- end
-
- def metaclass
- class << self; self; end
- end
-
- private
-
- def sudo_command
- configuration[:sudo] || "sudo"
- end
-
- def define_method(name, &block)
- metaclass.send(:define_method, name, &block)
- end
-
- def method_missing(sym, *args, &block)
- if @configuration.respond_to?(sym)
- @configuration.send(sym, *args, &block)
- else
- super
- end
- end
-
end
end
View
@@ -7,18 +7,27 @@ class Error < RuntimeError; end
attr_reader :command, :sessions, :options
- def initialize(command, sessions, options={}, &block) #:nodoc:
+ def self.process(command, sessions, options={}, &block)
+ new(command, sessions, options, &block).process!
+ end
+
+ # Instantiates a new command object. The +command+ must be a string
+ # containing the command to execute. +sessions+ is an array of Net::SSH
+ # session instances, and +options+ must be a hash containing any of the
+ # following keys:
+ #
+ # * +logger+: (optional), a Capistrano::Logger instance
+ # * +data+: (optional), a string to be sent to the command via it's stdin
+ # * +env+: (optional), a string or hash to be interpreted as environment
+ # variables that should be defined for this command invocation.
+ def initialize(command, sessions, options={}, &block)
@command = extract_environment(options) + command.strip.gsub(/\r?\n/, "\\\n")
@sessions = sessions
@options = options
@callback = block
@channels = open_channels
end
- def logger #:nodoc:
- options[:logger]
- end
-
# Processes the command in parallel on all specified hosts. If the command
# fails (non-zero return code) on any of the hosts, this will raise a
# RuntimeError.
@@ -59,6 +68,10 @@ def stop!
private
+ def logger
+ options[:logger]
+ end
+
def open_channels
sessions.map do |session|
session.open_channel do |channel|
@@ -1,12 +1,15 @@
require 'capistrano/logger'
require 'capistrano/extensions'
+
require 'capistrano/configuration/connections'
require 'capistrano/configuration/execution'
require 'capistrano/configuration/loading'
require 'capistrano/configuration/namespaces'
require 'capistrano/configuration/roles'
require 'capistrano/configuration/variables'
+require 'capistrano/configuration/actions/invocation'
+
module Capistrano
# Represents a specific Capistrano configuration. A Configuration instance
# may be used to load multiple recipe files, define and describe tasks,
@@ -22,5 +25,8 @@ def initialize #:nodoc:
# The includes must come at the bottom, since they may redefine methods
# defined in the base class.
include Connections, Execution, Loading, Namespaces, Roles, Variables
+
+ # Mix in the actions
+ include Actions::FileTransfer, Actions::Invocation
end
end
@@ -0,0 +1,37 @@
+require 'capistrano/upload'
+
+module Capistrano
+ class Configuration
+ module Actions
+ module FileTransfer
+
+ # Store the given data at the given location on all servers targetted
+ # by the current task. If <tt>:mode</tt> is specified it is used to
+ # set the mode on the file.
+ def put(data, path, options={})
+ execute_on_servers(options) do |servers|
+ targets = servers.map { |s| sessions[s.host] }
+ Upload.process(targets, path, :data => data, :mode => options[:mode], :logger => logger)
+ end
+ end
+
+ # Get file remote_path from FIRST server targetted by
+ # the current task and transfer it to local machine as path. It will use
+ # SFTP if Net::SFTP is installed; otherwise it will fall back to using
+ # 'cat', which may cause corruption in binary files.
+ #
+ # get "#{deploy_to}/current/log/production.log", "log/production.log.web"
+ def get(remote_path, path, options = {})
+ execute_on_servers(options.merge(:once => true)) do |servers|
+ logger.info "downloading `#{servers.first.host}:#{remote_path}' to `#{path}'"
+ sftp = sessions[servers.first.host].sftp
+ sftp.connect unless sftp.state == :open
+ sftp.get_file remote_path, path
+ logger.debug "download finished"
+ end
+ end
+
+ end
+ end
+ end
+end
@@ -0,0 +1,86 @@
+require 'capistrano/command'
+
+module Capistrano
+ class Configuration
+ module Actions
+ module Invocation
+ def self.included(base)
+ base.extend(ClassMethods)
+
+ base.default_io_proc = Proc.new do |ch, stream, out|
+ level = stream == :err ? :important : :info
+ ch[:options][:logger].send(level, out, "#{stream} :: #{ch[:host]}")
+ end
+ end
+
+ module ClassMethods
+ attr_accessor :default_io_proc
+ end
+
+ # Execute the given command on all servers that are the target of the
+ # current task. If a block is given, it is invoked for all output
+ # generated by the command, and should accept three parameters: the SSH
+ # channel (which may be used to send data back to the remote process),
+ # the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
+ # stdout), and the data that was received.
+ #
+ # If +pretend+ mode is active, this does nothing.
+ def run(cmd, options={}, &block)
+ block ||= self.class.default_io_proc
+ logger.debug "executing #{cmd.strip.inspect}"
+
+ execute_on_servers(options) do |servers|
+ targets = servers.map { |s| sessions[s.host] }
+ Command.process(cmd, targets, options.merge(:logger => logger), &block)
+ end
+ end
+
+ # Like #run, but executes the command via <tt>sudo</tt>. This assumes
+ # that the sudo password (if required) is the same as the password for
+ # logging in to the server.
+ #
+ # Also, this module accepts a <tt>:sudo</tt> configuration variable,
+ # which (if specified) will be used as the full path to the sudo
+ # executable on the remote machine:
+ #
+ # set :sudo, "/opt/local/bin/sudo"
+ def sudo(command, options={}, &block)
+ block ||= self.class.default_io_proc
+
+ options = options.dup
+ as = options.delete(:as)
+
+ user = as && "-u #{as}"
+ command = [fetch(:sudo, "sudo"), user, command].compact.join(" ")
+
+ run(command, options, &sudo_behavior_callback(block))
+ end
+
+ # Returns a Proc object that defines the behavior of the sudo
+ # callback. The returned Proc will defer to the +fallback+ argument
+ # (which should also be a Proc) for any output it does not
+ # explicitly handle.
+ def sudo_behavior_callback(fallback) #:nodoc:
+ # in order to prevent _each host_ from prompting when the password
+ # was wrong, let's track which host prompted first and only allow
+ # subsequent prompts from that host.
+ prompt_host = nil
+
+ Proc.new do |ch, stream, out|
+ if out =~ /^Password:/
+ ch.send_data "#{self[:password]}\n"
+ elsif out =~ /try again/
+ if prompt_host.nil? || prompt_host == ch[:host]
+ prompt_host = ch[:host]
+ logger.important out, "#{stream} :: #{ch[:host]}"
+ reset! :password
+ end
+ else
+ fallback.call(ch, stream, out)
+ end
+ end
+ end
+ end
+ end
+ end
+end
View
@@ -30,6 +30,10 @@ class Upload
# the requested data.
class Error < RuntimeError; end
+ def self.process(sessions, filename, options)
+ new(sessions, filename, options).process!
+ end
+
attr_reader :sessions, :filename, :options
attr_reader :failed, :completed
Oops, something went wrong.

0 comments on commit 4724310

Please sign in to comment.