require 'thread' require 'capistrano/processable' module Capistrano # The Capistrano::Shell class is the guts of the "shell" task. It implements # an interactive REPL interface that users can employ to execute tasks and # commands. It makes for a GREAT way to monitor systems, and perform quick # maintenance on one or more machines. class Shell include Processable # A Readline replacement for platforms where readline is either # unavailable, or has not been installed. class ReadlineFallback #:nodoc: HISTORY = [] def self.readline(prompt) STDOUT.print(prompt) STDOUT.flush STDIN.gets end end # The configuration instance employed by this shell attr_reader :configuration # Instantiate a new shell and begin executing it immediately. def self.run(config) new(config).run! end # Instantiate a new shell def initialize(config) @configuration = config end # Start the shell running. This method will block until the shell # terminates. def run! setup puts <<-INTRO ==================================================================== Welcome to the interactive Capistrano shell! This is an experimental feature, and is liable to change in future releases. Type 'help' for a summary of how to use the shell. -------------------------------------------------------------------- INTRO loop do break if !read_and_execute end @bgthread.kill end def read_and_execute command = read_line case command when "?", "help" then help when "quit", "exit" then puts "exiting" return false when /^set -(\w)\s*(\S+)/ set_option($1, $2) when /^set :(.*)\s+(.*)/ configuration.set($1.to_sym, $2) puts "updated :#{$1} to #{$2}" when /^(?:(with|on)\s*(\S+))?\s*(\S.*)?/i process_command($1, $2, $3) else raise "eh?" end return true end private # Present the prompt and read a single line from the console. It also # detects ^D and returns "exit" in that case. Adds the input to the # history, unless the input is empty. Loops repeatedly until a non-empty # line is input. def read_line loop do command = reader.readline("cap> ") if command.nil? command = "exit" puts(command) else command.strip! end unless command.empty? reader::HISTORY << command return command end end end # Display a verbose help message. def help puts <<-HELP --- HELP! --------------------------------------------------- "Get me out of this thing. I just want to quit." -> Easy enough. Just type "exit", or "quit". Or press ctrl-D. "I want to execute a command on all servers." -> Just type the command, and press enter. It will be passed, verbatim, to all defined servers. "What if I only want it to execute on a subset of them?" -> No problem, just specify the list of servers, separated by commas, before the command, with the `on' keyword: cap> on app1.foo.com,app2.foo.com echo ping "Nice, but can I specify the servers by role?" -> You sure can. Just use the `with' keyword, followed by the comma-delimited list of role names: cap> with app,db echo ping "Can I execute a Capistrano task from within this shell?" -> Yup. Just prefix the task with an exclamation mark: cap> !deploy HELP end # Determine which servers the given task requires a connection to, and # establish connections to them if necessary. Return the list of # servers (names). def connect(task) servers = configuration.find_servers_for_task(task) needing_connections = servers - configuration.sessions.keys unless needing_connections.empty? puts "[establishing connection(s) to #{needing_connections.join(', ')}]" configuration.establish_connections_to(needing_connections) end servers end # Execute the given command. If the command is prefixed by an exclamation # mark, it is assumed to refer to another capistrano task, which will # be invoked. Otherwise, it is executed as a command on all associated # servers. def exec(command) @mutex.synchronize do if command[0] == ?! exec_tasks(command[1..-1].split) else servers = connect(configuration.current_task) exec_command(command, servers) end end ensure STDOUT.flush end # Given an array of task names, invoke them in sequence. def exec_tasks(list) list.each do |task_name| task = configuration.find_task(task_name) raise Capistrano::NoSuchTaskError, "no such task `#{task_name}'" unless task connect(task) configuration.execute_task(task) end rescue Capistrano::NoMatchingServersError, Capistrano::NoSuchTaskError => error warn "error: #{error.message}" end # Execute a command on the given list of servers. def exec_command(command, servers) command = command.gsub(/^(\s*)sudo\b|([|;&])\s*sudo\b/, "\\0 -p '#{configuration.sudo_prompt}'") processor = configuration.sudo_behavior_callback(Configuration.default_io_proc) sessions = servers.map { |server| configuration.sessions[server] } options = configuration.add_default_command_options({}) cmd = Command.new(command, sessions, options.merge(:logger => configuration.logger), &processor) previous = trap("INT") { cmd.stop! } cmd.process! rescue Capistrano::Error => error warn "error: #{error.message}" ensure trap("INT", previous) end # Return the object that will be used to query input from the console. # The returned object will quack (more or less) like Readline. def reader @reader ||= begin require 'readline' Readline rescue LoadError ReadlineFallback end end # Prepare every little thing for the shell. Starts the background # thread and generally gets things ready for the REPL. def setup configuration.logger.level = Capistrano::Logger::INFO wait_for = 0.1 @mutex = Mutex.new @bgthread = Thread.new do loop do ret = @mutex.synchronize { process_iteration(wait_for) } sleep wait_for if !ret end end end # Set the given option to +value+. def set_option(opt, value) case opt when "v" then puts "setting log verbosity to #{value.to_i}" configuration.logger.level = value.to_i when "o" then case value when "vi" then puts "using vi edit mode" reader.vi_editing_mode when "emacs" then puts "using emacs edit mode" reader.emacs_editing_mode else puts "unknown -o option #{value.inspect}" end else puts "unknown setting #{opt.inspect}" end end # Process a command. Interprets the scope_type (must be nil, "with", or # "on") and the command. If no command is given, then the scope is made # effective for all subsequent commands. If the scope value is "all", # then the scope is unrestricted. def process_command(scope_type, scope_value, command) env_var = case scope_type when "with" then "ROLES" when "on" then "HOSTS" end old_var, ENV[env_var] = ENV[env_var], (scope_value == "all" ? nil : scope_value) if env_var if command begin exec(command) ensure ENV[env_var] = old_var if env_var end else puts "scoping #{scope_type} #{scope_value}" end end # All open sessions, needed to satisfy the Command::Processable include def sessions configuration.sessions.values end end end