1
0
Fork 0
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:
Jamis Buck 2007-03-02 21:33:27 +00:00
parent 472431004c
commit b5daf9f58b
19 changed files with 160 additions and 71 deletions

View file

@ -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.
#

View file

@ -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

View file

@ -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

View 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

View file

@ -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}"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
View 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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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