require 'capistrano/errors'
require 'capistrano/processable'
module Capistrano
# This class encapsulates a single command to be executed on a set of remote
# machines, in parallel.
class Command
include Processable
class Tree
attr_reader :configuration
attr_reader :branches
attr_reader :fallback
include Enumerable
class Branch
attr_accessor :command, :callback
attr_reader :options
def initialize(command, options, callback)
@command = command.strip.gsub(/\r?\n/, "\\\n")
@callback = callback || Capistrano::Configuration.default_io_proc
@options = options
@skip = false
end
def last?
options[:last]
end
def skip?
@skip
end
def skip!
@skip = true
end
def match(server)
true
end
def to_s
command.inspect
end
end
class ConditionBranch < Branch
attr_accessor :configuration
attr_accessor :condition
class Evaluator
attr_reader :configuration, :condition, :server
def initialize(config, condition, server)
@configuration = config
@condition = condition
@server = server
end
def in?(role)
configuration.roles[role].include?(server)
end
def result
eval(condition, binding)
end
def method_missing(sym, *args, &block)
if server.respond_to?(sym)
server.send(sym, *args, &block)
elsif configuration.respond_to?(sym)
configuration.send(sym, *args, &block)
else
super
end
end
end
def initialize(configuration, condition, command, options, callback)
@configuration = configuration
@condition = condition
super(command, options, callback)
end
def match(server)
Evaluator.new(configuration, condition, server).result
end
def to_s
"#{condition.inspect} :: #{command.inspect}"
end
end
def initialize(config)
@configuration = config
@branches = []
yield self if block_given?
end
def when(condition, command, options={}, &block)
branches << ConditionBranch.new(configuration, condition, command, options, block)
end
def else(command, &block)
@fallback = Branch.new(command, {}, block)
end
def branches_for(server)
seen_last = false
matches = branches.select do |branch|
success = !seen_last && !branch.skip? && branch.match(server)
seen_last = success && branch.last?
success
end
matches << fallback if matches.empty? && fallback
return matches
end
def each
branches.each { |branch| yield branch }
yield fallback if fallback
return self
end
end
attr_reader :tree, :sessions, :options
def self.process(tree, sessions, options={})
new(tree, sessions, options).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(tree, sessions, options={}, &block)
if String === tree
tree = Tree.new(nil) { |t| t.else(tree, &block) }
elsif block
raise ArgumentError, "block given with tree argument"
end
@tree = tree
@sessions = sessions
@options = options
@channels = open_channels
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
# Capistrano::CommandError.
def process!
loop do
break unless process_iteration { @channels.any? { |ch| !ch[:closed] } }
end
logger.trace "command finished" if logger
if (failed = @channels.select { |ch| ch[:status] != 0 }).any?
commands = failed.inject({}) { |map, ch| (map[ch[:command]] ||= []) << ch[:server]; map }
message = commands.map { |command, list| "#{command.inspect} on #{list.join(',')}" }.join("; ")
error = CommandError.new("failed: #{message}")
error.hosts = commands.values.flatten
raise error
end
self
end
# Force the command to stop processing, by closing all open channels
# associated with this command.
def stop!
@channels.each do |ch|
ch.close unless ch[:closed]
end
end
private
def logger
options[:logger]
end
def open_channels
sessions.map do |session|
server = session.xserver
tree.branches_for(server).map do |branch|
session.open_channel do |channel|
channel[:server] = server
channel[:host] = server.host
channel[:options] = options
channel[:branch] = branch
request_pty_if_necessary(channel) do |ch, success|
if success
logger.trace "executing command", ch[:server] if logger
cmd = replace_placeholders(channel[:branch].command, ch)
if options[:shell] == false
shell = nil
else
shell = "#{options[:shell] || "sh"} -c"
cmd = cmd.gsub(/[$\\`"]/) { |m| "\\#{m}" }
cmd = "\"#{cmd}\""
end
command_line = [environment, shell, cmd].compact.join(" ")
ch[:command] = command_line
ch.exec(command_line)
ch.send_data(options[:data]) if options[:data]
else
# just log it, don't actually raise an exception, since the
# process method will see that the status is not zero and will
# raise an exception then.
logger.important "could not open channel", ch[:server] if logger
ch.close
end
end
channel.on_data do |ch, data|
ch[:branch].callback[ch, :out, data]
end
channel.on_extended_data do |ch, type, data|
ch[:branch].callback[ch, :err, data]
end
channel.on_request("exit-status") do |ch, data|
ch[:status] = data.read_long
end
channel.on_close do |ch|
ch[:closed] = true
end
end
end
end.flatten
end
def request_pty_if_necessary(channel)
if options[:pty]
channel.request_pty do |ch, success|
yield ch, success
end
else
yield channel, true
end
end
def replace_placeholders(command, channel)
command.gsub(/\$CAPISTRANO:HOST\$/, channel[:host])
end
# prepare a space-separated sequence of variables assignments
# intended to be prepended to a command, so the shell sets
# the environment before running the command.
# i.e.: options[:env] = {'PATH' => '/opt/ruby/bin:$PATH',
# 'TEST' => '( "quoted" )'}
# environment returns:
# "env TEST=(\ \"quoted\"\ ) PATH=/opt/ruby/bin:$PATH"
def environment
return if options[:env].nil? || options[:env].empty?
@environment ||= if String === options[:env]
"env #{options[:env]}"
else
options[:env].inject("env") do |string, (name, value)|
value = value.to_s.gsub(/[ "]/) { |m| "\\#{m}" }
string << " #{name}=#{value}"
end
end
end
end
end