Daily Code Reading #17 – Capistrano control flow

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
  • restart

Based on what I’ve read today, I would separate Capistrano into two components:

  1. Configuration and option parsing component
  2. 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.