mirror of
https://github.com/capistrano/capistrano
synced 2023-03-27 23:21:18 -04:00
refactor capistrano errors into a single module, with a single superclass. Add #invoke helper action for programmatically selecting between run and sudo. Refactor the #stream and #capture helper actions.
git-svn-id: http://svn.rubyonrails.org/rails/tools/capistrano@6289 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
472431004c
commit
b5daf9f58b
19 changed files with 160 additions and 71 deletions
|
@ -13,48 +13,6 @@ module Capistrano
|
|||
# directly--rather, you create a new Configuration instance, and access the
|
||||
# new actor via Configuration#actor.
|
||||
class Actor
|
||||
# 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
|
||||
# is quite expensive from a bandwidth perspective, so use it with care.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# desc "Run a tail on multiple log files at the same time"
|
||||
# task :tail_fcgi, :roles => :app do
|
||||
# stream "tail -f #{shared_path}/log/fastcgi.crash.log"
|
||||
# end
|
||||
def stream(command)
|
||||
run(command) do |ch, stream, out|
|
||||
puts out if stream == :out
|
||||
if stream == :err
|
||||
puts "[err : #{ch[:host]}] #{out}"
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Deletes the given file from all servers targetted by the current task.
|
||||
# If <tt>:recursive => true</tt> is specified, it may be used to remove
|
||||
# directories.
|
||||
def delete(path, options={})
|
||||
cmd = "rm -%sf #{path}" % (options[:recursive] ? "r" : "")
|
||||
run(cmd, options)
|
||||
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={})
|
||||
output = ""
|
||||
run(command, options.merge(:once => true)) do |ch, stream, data|
|
||||
case stream
|
||||
when :out then output << data
|
||||
when :err then raise "error processing #{command.inspect}: #{data.inspect}"
|
||||
end
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
# Renders an ERb template and returns the result. This is useful for
|
||||
# dynamically building documents to store on the remote servers.
|
||||
#
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
require 'capistrano/errors'
|
||||
|
||||
module Capistrano
|
||||
|
||||
# This class encapsulates a single command to be executed on a set of remote
|
||||
# machines, in parallel.
|
||||
class Command
|
||||
class Error < RuntimeError; end
|
||||
|
||||
attr_reader :command, :sessions, :options
|
||||
|
||||
def self.process(command, sessions, options={}, &block)
|
||||
|
@ -30,7 +30,7 @@ module Capistrano
|
|||
|
||||
# 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.
|
||||
# Capistrano::CommandError.
|
||||
def process!
|
||||
since = Time.now
|
||||
loop do
|
||||
|
@ -52,7 +52,7 @@ module Capistrano
|
|||
logger.trace "command finished" if logger
|
||||
|
||||
if failed = @channels.detect { |ch| ch[:status] != 0 }
|
||||
raise Error, "command #{command.inspect} failed on #{failed[:host]}"
|
||||
raise CommandError, "command #{command.inspect} failed on #{failed[:host]}"
|
||||
end
|
||||
|
||||
self
|
||||
|
|
|
@ -8,6 +8,8 @@ require 'capistrano/configuration/namespaces'
|
|||
require 'capistrano/configuration/roles'
|
||||
require 'capistrano/configuration/variables'
|
||||
|
||||
require 'capistrano/configuration/actions/file_transfer'
|
||||
require 'capistrano/configuration/actions/inspect'
|
||||
require 'capistrano/configuration/actions/invocation'
|
||||
|
||||
module Capistrano
|
||||
|
@ -27,6 +29,6 @@ module Capistrano
|
|||
include Connections, Execution, Loading, Namespaces, Roles, Variables
|
||||
|
||||
# Mix in the actions
|
||||
include Actions::FileTransfer, Actions::Invocation
|
||||
include Actions::FileTransfer, Actions::Inspect, Actions::Invocation
|
||||
end
|
||||
end
|
||||
|
|
46
lib/capistrano/configuration/actions/inspect.rb
Normal file
46
lib/capistrano/configuration/actions/inspect.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
require 'capistrano/errors'
|
||||
|
||||
module Capistrano
|
||||
class Configuration
|
||||
module Actions
|
||||
module Inspect
|
||||
|
||||
# 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 is quite expensive from a bandwidth
|
||||
# perspective, so use it with care.
|
||||
#
|
||||
# The command is invoked via #invoke.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# desc "Run a tail on multiple log files at the same time"
|
||||
# task :tail_fcgi, :roles => :app do
|
||||
# stream "tail -f #{shared_path}/log/fastcgi.crash.log"
|
||||
# end
|
||||
def stream(command, options={})
|
||||
invoke(command, options) do |ch, stream, out|
|
||||
puts out if stream == :out
|
||||
warn "[err :: #{ch[:host]}] #{out}" if stream == :err
|
||||
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. The command is invoked via #invoke.
|
||||
def capture(command, options={})
|
||||
output = ""
|
||||
invoke(command, options.merge(:once => true)) do |ch, stream, data|
|
||||
case stream
|
||||
when :out then output << data
|
||||
when :err then raise CaptureError, "error processing #{command.inspect}: #{data.inspect}"
|
||||
end
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,7 +4,7 @@ module Capistrano
|
|||
class Configuration
|
||||
module Actions
|
||||
module Invocation
|
||||
def self.included(base)
|
||||
def self.included(base) #:nodoc:
|
||||
base.extend(ClassMethods)
|
||||
|
||||
base.default_io_proc = Proc.new do |ch, stream, out|
|
||||
|
@ -17,14 +17,22 @@ module Capistrano
|
|||
attr_accessor :default_io_proc
|
||||
end
|
||||
|
||||
# Invokes the given command. If a +via+ key is given, it will be used
|
||||
# to determine what method to use to invoke the command. It defaults
|
||||
# to :run, but may be :sudo, or any other method that conforms to the
|
||||
# same interface as run and sudo.
|
||||
def invoke(cmd, options={}, &block)
|
||||
options = options.dup
|
||||
via = options.delete(:via) || :run
|
||||
send(via, cmd, options, &block)
|
||||
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}"
|
||||
|
|
|
@ -4,7 +4,7 @@ require 'capistrano/ssh'
|
|||
module Capistrano
|
||||
class Configuration
|
||||
module Connections
|
||||
def self.included(base)
|
||||
def self.included(base) #:nodoc:
|
||||
base.send :alias_method, :initialize_without_connections, :initialize
|
||||
base.send :alias_method, :initialize, :initialize_with_connections
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Capistrano
|
||||
class Configuration
|
||||
module Execution
|
||||
def self.included(base)
|
||||
def self.included(base) #:nodoc:
|
||||
base.send :alias_method, :initialize_without_execution, :initialize
|
||||
base.send :alias_method, :initialize, :initialize_with_execution
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Capistrano
|
||||
class Configuration
|
||||
module Loading
|
||||
def self.included(base)
|
||||
def self.included(base) #:nodoc:
|
||||
base.send :alias_method, :initialize_without_loading, :initialize
|
||||
base.send :alias_method, :initialize, :initialize_with_loading
|
||||
base.extend ClassMethods
|
||||
|
|
|
@ -3,7 +3,7 @@ require 'capistrano/task_definition'
|
|||
module Capistrano
|
||||
class Configuration
|
||||
module Namespaces
|
||||
def self.included(base)
|
||||
def self.included(base) #:nodoc:
|
||||
base.send :alias_method, :initialize_without_namespaces, :initialize
|
||||
base.send :alias_method, :initialize, :initialize_with_namespaces
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ require 'capistrano/server_definition'
|
|||
module Capistrano
|
||||
class Configuration
|
||||
module Roles
|
||||
def self.included(base)
|
||||
def self.included(base) #:nodoc:
|
||||
base.send :alias_method, :initialize_without_roles, :initialize
|
||||
base.send :alias_method, :initialize, :initialize_with_roles
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Capistrano
|
||||
class Configuration
|
||||
module Variables
|
||||
def self.included(base)
|
||||
def self.included(base) #:nodoc:
|
||||
%w(initialize respond_to? method_missing).each do |m|
|
||||
base_name = m[/^\w+/]
|
||||
punct = m[/\W+$/]
|
||||
|
|
8
lib/capistrano/errors.rb
Normal file
8
lib/capistrano/errors.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
module Capistrano
|
||||
class Error < RuntimeError; end
|
||||
|
||||
class CaptureError < Error; end
|
||||
class CommandError < Error; end
|
||||
class ConnectionError < Error; end
|
||||
class UploadError < Error; end
|
||||
end
|
|
@ -1,4 +1,5 @@
|
|||
require 'thread'
|
||||
require 'capistrano/errors'
|
||||
require 'capistrano/ssh'
|
||||
require 'capistrano/server_definition'
|
||||
|
||||
|
@ -19,9 +20,6 @@ module Capistrano
|
|||
# sess1 = gateway.connect_to(Capistrano::ServerDefinition.new('hidden.example.com'))
|
||||
# sess2 = gateway.connect_to(Capistrano::ServerDefinition.new('other.example.com'))
|
||||
class Gateway
|
||||
# An exception class for reporting Gateway-specific errors.
|
||||
class Error < Exception; end
|
||||
|
||||
# The Thread instance driving the gateway connection.
|
||||
attr_reader :thread
|
||||
|
||||
|
@ -94,7 +92,7 @@ module Capistrano
|
|||
end
|
||||
|
||||
thread.join
|
||||
connection or raise Error, "could not establish connection to `#{server.host}'"
|
||||
connection or raise ConnectionError, "could not establish connection to `#{server.host}'"
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require 'net/sftp'
|
||||
require 'capistrano/errors'
|
||||
|
||||
module Capistrano
|
||||
unless ENV['SKIP_VERSION_CHECK']
|
||||
|
@ -22,14 +23,10 @@ module Capistrano
|
|||
# uploader = Capistrano::Upload.new(sessions, "remote-file.txt",
|
||||
# :data => "the contents of the file to upload")
|
||||
# uploader.process!
|
||||
# rescue Capistrano::Upload::Error => e
|
||||
# rescue Capistrano::UploadError => e
|
||||
# warn "Could not upload the file: #{e.message}"
|
||||
# end
|
||||
class Upload
|
||||
# A custom exception that is raised when the uploader is unable to upload
|
||||
# the requested data.
|
||||
class Error < RuntimeError; end
|
||||
|
||||
def self.process(sessions, filename, options)
|
||||
new(sessions, filename, options).process!
|
||||
end
|
||||
|
@ -59,7 +56,7 @@ module Capistrano
|
|||
end
|
||||
|
||||
# Uploads to all specified servers in parallel. If any one of the servers
|
||||
# fails, an exception will be raised (Upload::Error).
|
||||
# fails, an exception will be raised (UploadError).
|
||||
def process!
|
||||
logger.debug "uploading #{filename}" if logger
|
||||
while running?
|
||||
|
@ -70,7 +67,7 @@ module Capistrano
|
|||
end
|
||||
logger.trace "upload finished" if logger
|
||||
|
||||
raise Error, "upload of #{filename} failed on one or more hosts" if failed > 0
|
||||
raise UploadError, "upload of #{filename} failed on one or more hosts" if failed > 0
|
||||
|
||||
self
|
||||
end
|
||||
|
|
|
@ -163,7 +163,7 @@ class CommandTest < Test::Unit::TestCase
|
|||
mock("session", :open_channel => new_channel(true, 0)),
|
||||
mock("session", :open_channel => new_channel(true, 1))]
|
||||
cmd = Capistrano::Command.new("ls", sessions)
|
||||
assert_raises(Capistrano::Command::Error) { cmd.process! }
|
||||
assert_raises(Capistrano::CommandError) { cmd.process! }
|
||||
end
|
||||
|
||||
def test_process_should_loop_until_all_channels_are_closed
|
||||
|
|
62
test/configuration/actions/inspect_test.rb
Normal file
62
test/configuration/actions/inspect_test.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
require "#{File.dirname(__FILE__)}/../../utils"
|
||||
require 'capistrano/configuration/actions/inspect'
|
||||
|
||||
class ConfigurationActionsRunTest < Test::Unit::TestCase
|
||||
class MockConfig
|
||||
include Capistrano::Configuration::Actions::Inspect
|
||||
end
|
||||
|
||||
def setup
|
||||
@config = MockConfig.new
|
||||
@config.stubs(:logger).returns(stub_everything)
|
||||
end
|
||||
|
||||
def test_stream_should_pass_options_through_to_run
|
||||
@config.expects(:invoke).with("tail -f foo.log", :once => true)
|
||||
@config.stream("tail -f foo.log", :once => true)
|
||||
end
|
||||
|
||||
def test_stream_should_emit_stdout_via_puts
|
||||
@config.expects(:invoke).yields(mock("channel"), :out, "something streamed")
|
||||
@config.expects(:puts).with("something streamed")
|
||||
@config.expects(:warn).never
|
||||
@config.stream("tail -f foo.log")
|
||||
end
|
||||
|
||||
def test_stream_should_emit_stderr_via_warn
|
||||
ch = mock("channel")
|
||||
ch.expects(:[]).with(:host).returns("capistrano")
|
||||
@config.expects(:invoke).yields(ch, :err, "something streamed")
|
||||
@config.expects(:puts).never
|
||||
@config.expects(:warn).with("[err :: capistrano] something streamed")
|
||||
@config.stream("tail -f foo.log")
|
||||
end
|
||||
|
||||
def test_capture_should_pass_options_merged_with_once_to_run
|
||||
@config.expects(:invoke).with("hostname", :foo => "bar", :once => true)
|
||||
@config.capture("hostname", :foo => "bar")
|
||||
end
|
||||
|
||||
def test_capture_with_stderr_result_should_raise_capture_error
|
||||
@config.expects(:invoke).yields(mock("channel"), :err, "boom")
|
||||
assert_raises(Capistrano::CaptureError) { @config.capture("hostname") }
|
||||
end
|
||||
|
||||
def test_capture_with_stdout_should_aggregate_and_return_stdout
|
||||
config_expects_invoke_to_loop_with(mock("channel"), "foo", "bar", "baz")
|
||||
assert_equal "foobarbaz", @config.capture("hostname")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def config_expects_invoke_to_loop_with(channel, *output)
|
||||
class <<@config
|
||||
attr_accessor :script, :channel
|
||||
def invoke(*args)
|
||||
script.each { |item| yield channel, :out, item }
|
||||
end
|
||||
end
|
||||
@config.channel = channel
|
||||
@config.script = output
|
||||
end
|
||||
end
|
|
@ -125,6 +125,16 @@ class ConfigurationActionsRunTest < Test::Unit::TestCase
|
|||
callback[a, b, c]
|
||||
end
|
||||
|
||||
def test_invoke_should_default_to_run
|
||||
@config.expects(:run).with("ls", :once => true)
|
||||
@config.invoke("ls", :once => true)
|
||||
end
|
||||
|
||||
def test_invoke_should_delegate_to_method_identified_by_via
|
||||
@config.expects(:foobar).with("ls", :once => true)
|
||||
@config.invoke("ls", :once => true, :via => :foobar)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inspectable_proc
|
||||
|
|
|
@ -68,7 +68,7 @@ class GatewayTest < Test::Unit::TestCase
|
|||
gateway = new_gateway
|
||||
expect_connect_to(:host => "127.0.0.1").raises(RuntimeError)
|
||||
gateway.expects(:warn).times(2)
|
||||
assert_raises(Capistrano::Gateway::Error) { gateway.connect_to(server("app1")) }
|
||||
assert_raises(Capistrano::ConnectionError) { gateway.connect_to(server("app1")) }
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -37,7 +37,7 @@ class UploadTest < Test::Unit::TestCase
|
|||
sftp.expects(:open).with("test.txt", @mode, 0660).yields(mock("status", :code => "bad status", :message => "bad status"), :file_handle)
|
||||
session = mock("session", :sftp => sftp, :host => "capistrano")
|
||||
upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything)
|
||||
assert_raises(Capistrano::Upload::Error) { upload.process! }
|
||||
assert_raises(Capistrano::UploadError) { upload.process! }
|
||||
assert_equal 1, upload.failed
|
||||
assert_equal 1, upload.completed
|
||||
end
|
||||
|
@ -50,7 +50,7 @@ class UploadTest < Test::Unit::TestCase
|
|||
sftp.expects(:write).with(:file_handle, "data").yields(mock("status2", :code => "bad status", :message => "bad status"))
|
||||
session = mock("session", :sftp => sftp, :host => "capistrano")
|
||||
upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything)
|
||||
assert_raises(Capistrano::Upload::Error) { upload.process! }
|
||||
assert_raises(Capistrano::UploadError) { upload.process! }
|
||||
assert_equal 1, upload.failed
|
||||
assert_equal 1, upload.completed
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue