I’m reading through Capistrano‘s code this week and decided to start with something different. Instead of jumping right into different methods, I’m going to review the overall flow to get an understanding of how one part works. For Capistrano, I’m going to figure out how cap deploy
works.
1 2 3 4 |
#!/usr/bin/env ruby require 'capistrano/cli' Capistrano::CLI.execute |
Capistrano includes a cap
script which just loads a Capistrano::CLI
class.
1 2 3 4 5 6 7 8 |
module Capistrano class CLI # ... # Mix-in the actual behavior include Execute, Options, UI include Help # needs to be included last, because it overrides some methods end end |
Capistrano::CLI
doesn’t define an #execute
method but it does include one as a helper so I’ll have to look there for the behavior.
1 2 3 4 5 6 7 8 9 10 11 12 |
module Capistrano class CLI module Execute module ClassMethods def execute parse(ARGV).execute! end end # ... end end end |
Capistrano::CLI::Execute#execute
still isn’t doing much, it’s just chaining #parse
and #execute!
. It looks like Capistrano::CLI::Options
defines the #parse
method so I’ll jump over to there now.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
module Capistrano class CLI module Options module ClassMethods # Return a new CLI instance with the given arguments pre-parsed and # ready for execution. def parse(args) cli = new(args) cli.parse_options! cli end end # ... end end end |
Now I’m starting to get somewhere. #parse
will create a new instance of Capistrano::CLI
and parse it’s options. Since Capistrano::CLI::Options
was mixed into Capistrano::CLI
, the #new
method is called on Capistrano::CLI
and not Capistrano::CLI::Options
.
1 2 3 4 5 6 7 8 |
module Capistrano class CLI def initialize(args) @args = args.dup $stdout.sync = true # so that Net::SSH prompts show up end end end |
Back in Capistrano::CLI
, the only significant thing that #initialize
does is to store the command line arguments into @args
. So the control flow returns back in Capistrano::CLI::Options
and calls #parse_options!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
module Capistrano class CLI module Options def parse_options! #:nodoc: @options = { :recipes => [], :actions => [], :vars => {}, :pre_vars => {}, :sysconf => default_sysconf, :dotfile => default_dotfile } if args.empty? warn "Please specify at least one action to execute." warn option_parser exit end option_parser.parse!(args) coerce_variable_types! # if no verbosity has been specified, be verbose options[:verbose] = 3 if !options.has_key?(:verbose) look_for_default_recipe_file! if options[:default_config] || options[:recipes].empty? extract_environment_variables! options[:actions].concat(args) password = options.has_key?(:password) options[:password] = Proc.new { self.class.password_prompt } options[:password] = options[:password].call if password end end end end |
There is a lot of code in here but the only things it’s doing is to set and check different configuration options. So the control flow returns back to Capistrano::CLI::Options#parse
which returns the configured Capistrano::CLI
object back to Capistrano::CLI::Execute#execute
which calls Capistrano::CLI##execute!
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
module Capistrano class CLI module Execute # Using the options build when the command-line was parsed, instantiate # a new Capistrano configuration, initialize it, and execute the # requested actions. # # Returns the Configuration instance used, if successful. def execute! config = instantiate_configuration(options) config.debug = options[:debug] config.dry_run = options[:dry_run] config.preserve_roles = options[:preserve_roles] config.logger.level = options[:verbose] set_pre_vars(config) load_recipes(config) config.trigger(:load) execute_requested_actions(config) config.trigger(:exit) config rescue Exception => error handle_error(error) end end end end |
Now I’m getting into Capistrano’s processing. Execute is doing a few things to run the action:
- creating a local version of the configuration (
config
object) - loading the recipes (
load_recipes(config)
) - running load callback (
config.trigger(:load)
) -
executing the action (
execute_requested_actions(config)
) - running the exit callback (
config.trigger(:exit)
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
module Capistrano class CLI module Execute # ... def execute_requested_actions(config) Array(options[:vars]).each { |name, value| config.set(name, value) } Array(options[:actions]).each do |action| config.find_and_execute_task(action, :before => :start, :after => :finish) end end end end end |
Following the request into #execute_requested_actions
I see that the processing of the action is getting delegated to Capistrano::Configuration#find_and_execute_task
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
module Capistrano class Configuration # The logger instance defined for this configuration. attr_accessor :debug, :logger, :dry_run, :preserve_roles def initialize(options={}) #:nodoc: @debug = false @dry_run = false @preserve_roles = false @logger = Logger.new(options) end # make the DSL easier to read when using lazy evaluation via lambdas alias defer lambda # The includes must come at the bottom, since they may redefine methods # defined in the base class. include Connections, Execution, Loading, Namespaces, Roles, Servers, Variables # Mix in the actions include Actions::FileTransfer, Actions::Inspect, Actions::Invocation # Must mix last, because it hooks into previously defined methods include Callbacks end end |
This class doesn’t define the #find_and_execute_task
so once again, I have to go hunting for it inside the modules.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
module Capistrano class Configuration module Execution # Attempts to locate the task at the given fully-qualified path, and # execute it. If no such task exists, a Capistrano::NoSuchTaskError will # be raised. def find_and_execute_task(path, hooks={}) task = find_task(path) or raise NoSuchTaskError, "the task `#{path}' does not exist" trigger(hooks[:before], task) if hooks[:before] result = execute_task(task) trigger(hooks[:after], task) if hooks[:after] result end end end end |
I found the method inside Capistrano::Configuration::Execution
but it looks like the #execute_task
method is what runs the command.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
module Capistrano class Configuration module Execution # Executes the task with the given name, without invoking any associated # callbacks. def execute_task(task) logger.debug "executing `#{task.fully_qualified_name}'" push_task_call_frame(task) invoke_task_directly(task) ensure pop_task_call_frame end end end end |
Ignoring the #push_tasks_call_frame
and #pop_task_call_frame
for now, the #invoke_task_directly
method is called.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
module Capistrano class Configuration module Execution protected # Invokes the task's body directly, without setting up the call frame. def invoke_task_directly(task) task.namespace.instance_eval(&task.body) end end end end |
You know you are getting into the good stuff when a program’s execution starts using the protected methods. These are the dirty little methods where all the work gets done.
From this, it looks like Capistrano just runs instance_eval
on the task’s body (a block). So I’ve traced the call from the commandline (cap deploy
) all the way into Capistrano where the code calls a specific task/recipe. If I jump ahead and look at a few of the recipe definitions for deploy
, the rest of the call stack starts to make sense:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
namespace :deploy do task :default do update restart end task :update do transaction do update_code symlink end end task :update_code, :except => { :no_release => true } do on_rollback { run "rm -rf #{release_path}; true" } strategy.deploy! finalize_update end task :restart, :roles => :app, :except => { :no_release => true } do warn "[DEPRECATED] `deploy:restart` is going to be changed to Passenger mod_rails' method after 2.5.9 - see http://is.gd/2BPeA" try_runner "#{current_path}/script/process/reaper" end end |
deploy
‘s block runs instance_eval
which makes sense:
- update
- update_code
- deploys via the deploy strategy
- finalizes the update
- symlink
- update_code
- restart
Based on what I’ve read today, I would separate Capistrano into two components:
- Configuration and option parsing component
- Recipes
Since I’ve done a lot of system administration in the past, I’m going to focus on reading through the code for the recipes this week. The configuration component is interesting but as you can see from this example, there is a lot of redirection and delegation going on in there that doesn’t interest me at all. Tomorrow’s code reading will start to take a look at the different recipes that make up cap deploy
.