| 1 |
require 'thread' |
|---|
| 2 |
|
|---|
| 3 |
module Capistrano |
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
class Shell |
|---|
| 9 |
|
|---|
| 10 |
|
|---|
| 11 |
class ReadlineFallback |
|---|
| 12 |
HISTORY = [] |
|---|
| 13 |
|
|---|
| 14 |
def self.readline(prompt) |
|---|
| 15 |
STDOUT.print(prompt) |
|---|
| 16 |
STDOUT.flush |
|---|
| 17 |
STDIN.gets |
|---|
| 18 |
end |
|---|
| 19 |
end |
|---|
| 20 |
|
|---|
| 21 |
|
|---|
| 22 |
attr_reader :configuration |
|---|
| 23 |
|
|---|
| 24 |
|
|---|
| 25 |
def self.run(config) |
|---|
| 26 |
new(config).run! |
|---|
| 27 |
end |
|---|
| 28 |
|
|---|
| 29 |
|
|---|
| 30 |
def initialize(config) |
|---|
| 31 |
@configuration = config |
|---|
| 32 |
end |
|---|
| 33 |
|
|---|
| 34 |
|
|---|
| 35 |
|
|---|
| 36 |
def run! |
|---|
| 37 |
setup |
|---|
| 38 |
|
|---|
| 39 |
puts <<-INTRO |
|---|
| 40 |
==================================================================== |
|---|
| 41 |
Welcome to the interactive Capistrano shell! This is an experimental |
|---|
| 42 |
feature, and is liable to change in future releases. Type 'help' for |
|---|
| 43 |
a summary of how to use the shell. |
|---|
| 44 |
-------------------------------------------------------------------- |
|---|
| 45 |
INTRO |
|---|
| 46 |
|
|---|
| 47 |
loop do |
|---|
| 48 |
break if !read_and_execute |
|---|
| 49 |
end |
|---|
| 50 |
|
|---|
| 51 |
@bgthread.kill |
|---|
| 52 |
end |
|---|
| 53 |
|
|---|
| 54 |
def read_and_execute |
|---|
| 55 |
command = read_line |
|---|
| 56 |
|
|---|
| 57 |
case command |
|---|
| 58 |
when "?", "help" then help |
|---|
| 59 |
when "quit", "exit" then |
|---|
| 60 |
puts "exiting" |
|---|
| 61 |
return false |
|---|
| 62 |
when /^set -(\w)\s*(\S+)/ |
|---|
| 63 |
set_option($1, $2) |
|---|
| 64 |
when /^(?:(with|on)\s*(\S+))?\s*(\S.*)?/i |
|---|
| 65 |
process_command($1, $2, $3) |
|---|
| 66 |
else |
|---|
| 67 |
raise "eh?" |
|---|
| 68 |
end |
|---|
| 69 |
|
|---|
| 70 |
return true |
|---|
| 71 |
end |
|---|
| 72 |
|
|---|
| 73 |
private |
|---|
| 74 |
|
|---|
| 75 |
|
|---|
| 76 |
|
|---|
| 77 |
|
|---|
| 78 |
|
|---|
| 79 |
def read_line |
|---|
| 80 |
loop do |
|---|
| 81 |
command = reader.readline("cap> ") |
|---|
| 82 |
|
|---|
| 83 |
if command.nil? |
|---|
| 84 |
command = "exit" |
|---|
| 85 |
puts(command) |
|---|
| 86 |
else |
|---|
| 87 |
command.strip! |
|---|
| 88 |
end |
|---|
| 89 |
|
|---|
| 90 |
unless command.empty? |
|---|
| 91 |
reader::HISTORY << command |
|---|
| 92 |
return command |
|---|
| 93 |
end |
|---|
| 94 |
end |
|---|
| 95 |
end |
|---|
| 96 |
|
|---|
| 97 |
|
|---|
| 98 |
def help |
|---|
| 99 |
puts <<-HELP |
|---|
| 100 |
--- HELP! --------------------------------------------------- |
|---|
| 101 |
"Get me out of this thing. I just want to quit." |
|---|
| 102 |
-> Easy enough. Just type "exit", or "quit". Or press ctrl-D. |
|---|
| 103 |
|
|---|
| 104 |
"I want to execute a command on all servers." |
|---|
| 105 |
-> Just type the command, and press enter. It will be passed, |
|---|
| 106 |
verbatim, to all defined servers. |
|---|
| 107 |
|
|---|
| 108 |
"What if I only want it to execute on a subset of them?" |
|---|
| 109 |
-> No problem, just specify the list of servers, separated by |
|---|
| 110 |
commas, before the command, with the `on' keyword: |
|---|
| 111 |
|
|---|
| 112 |
cap> on app1.foo.com,app2.foo.com echo ping |
|---|
| 113 |
|
|---|
| 114 |
"Nice, but can I specify the servers by role?" |
|---|
| 115 |
-> You sure can. Just use the `with' keyword, followed by the |
|---|
| 116 |
comma-delimited list of role names: |
|---|
| 117 |
|
|---|
| 118 |
cap> with app,db echo ping |
|---|
| 119 |
|
|---|
| 120 |
"Can I execute a Capistrano task from within this shell?" |
|---|
| 121 |
-> Yup. Just prefix the task with an exclamation mark: |
|---|
| 122 |
|
|---|
| 123 |
cap> !deploy |
|---|
| 124 |
HELP |
|---|
| 125 |
end |
|---|
| 126 |
|
|---|
| 127 |
|
|---|
| 128 |
|
|---|
| 129 |
|
|---|
| 130 |
def connect(task) |
|---|
| 131 |
servers = configuration.find_servers_for_task(task) |
|---|
| 132 |
needing_connections = servers - configuration.sessions.keys |
|---|
| 133 |
unless needing_connections.empty? |
|---|
| 134 |
puts "[establishing connection(s) to #{needing_connections.join(', ')}]" |
|---|
| 135 |
configuration.establish_connections_to(needing_connections) |
|---|
| 136 |
end |
|---|
| 137 |
servers |
|---|
| 138 |
end |
|---|
| 139 |
|
|---|
| 140 |
|
|---|
| 141 |
|
|---|
| 142 |
|
|---|
| 143 |
|
|---|
| 144 |
def exec(command) |
|---|
| 145 |
if command[0] == ?! |
|---|
| 146 |
exec_tasks(command[1..-1].split) |
|---|
| 147 |
else |
|---|
| 148 |
servers = connect(configuration.current_task) |
|---|
| 149 |
exec_command(command, servers) |
|---|
| 150 |
end |
|---|
| 151 |
ensure |
|---|
| 152 |
STDOUT.flush |
|---|
| 153 |
end |
|---|
| 154 |
|
|---|
| 155 |
|
|---|
| 156 |
def exec_tasks(list) |
|---|
| 157 |
list.each do |task_name| |
|---|
| 158 |
task = configuration.find_task(task_name) |
|---|
| 159 |
raise Capistrano::NoSuchTaskError, "no such task `#{task_name}'" unless task |
|---|
| 160 |
connect(task) |
|---|
| 161 |
configuration.execute_task(task) |
|---|
| 162 |
end |
|---|
| 163 |
rescue Capistrano::NoMatchingServersError, Capistrano::NoSuchTaskError => error |
|---|
| 164 |
warn "error: #{error.message}" |
|---|
| 165 |
end |
|---|
| 166 |
|
|---|
| 167 |
|
|---|
| 168 |
def exec_command(command, servers) |
|---|
| 169 |
command = command.gsub(/\bsudo\b/, "sudo -p '#{configuration.sudo_prompt}'") |
|---|
| 170 |
processor = configuration.sudo_behavior_callback(Configuration.default_io_proc) |
|---|
| 171 |
sessions = servers.map { |server| configuration.sessions[server] } |
|---|
| 172 |
options = configuration.add_default_command_options({}) |
|---|
| 173 |
cmd = Command.new(command, sessions, options.merge(:logger => configuration.logger), &processor) |
|---|
| 174 |
previous = trap("INT") { cmd.stop! } |
|---|
| 175 |
cmd.process! |
|---|
| 176 |
rescue Capistrano::Error => error |
|---|
| 177 |
warn "error: #{error.message}" |
|---|
| 178 |
ensure |
|---|
| 179 |
trap("INT", previous) |
|---|
| 180 |
end |
|---|
| 181 |
|
|---|
| 182 |
|
|---|
| 183 |
|
|---|
| 184 |
def reader |
|---|
| 185 |
@reader ||= begin |
|---|
| 186 |
require 'readline' |
|---|
| 187 |
Readline |
|---|
| 188 |
rescue LoadError |
|---|
| 189 |
ReadlineFallback |
|---|
| 190 |
end |
|---|
| 191 |
end |
|---|
| 192 |
|
|---|
| 193 |
|
|---|
| 194 |
|
|---|
| 195 |
def setup |
|---|
| 196 |
configuration.logger.level = Capistrano::Logger::INFO |
|---|
| 197 |
|
|---|
| 198 |
@mutex = Mutex.new |
|---|
| 199 |
@bgthread = Thread.new do |
|---|
| 200 |
loop do |
|---|
| 201 |
ready = configuration.sessions.values.select { |sess| sess.connection.reader_ready? } |
|---|
| 202 |
if ready.empty? |
|---|
| 203 |
sleep 0.1 |
|---|
| 204 |
else |
|---|
| 205 |
@mutex.synchronize do |
|---|
| 206 |
ready.each { |session| session.connection.process(true) } |
|---|
| 207 |
end |
|---|
| 208 |
end |
|---|
| 209 |
end |
|---|
| 210 |
end |
|---|
| 211 |
end |
|---|
| 212 |
|
|---|
| 213 |
|
|---|
| 214 |
def set_option(opt, value) |
|---|
| 215 |
case opt |
|---|
| 216 |
when "v" then |
|---|
| 217 |
puts "setting log verbosity to #{value.to_i}" |
|---|
| 218 |
configuration.logger.level = value.to_i |
|---|
| 219 |
when "o" then |
|---|
| 220 |
case value |
|---|
| 221 |
when "vi" then |
|---|
| 222 |
puts "using vi edit mode" |
|---|
| 223 |
reader.vi_editing_mode |
|---|
| 224 |
when "emacs" then |
|---|
| 225 |
puts "using emacs edit mode" |
|---|
| 226 |
reader.emacs_editing_mode |
|---|
| 227 |
else |
|---|
| 228 |
puts "unknown -o option #{value.inspect}" |
|---|
| 229 |
end |
|---|
| 230 |
else |
|---|
| 231 |
puts "unknown setting #{opt.inspect}" |
|---|
| 232 |
end |
|---|
| 233 |
end |
|---|
| 234 |
|
|---|
| 235 |
|
|---|
| 236 |
|
|---|
| 237 |
|
|---|
| 238 |
|
|---|
| 239 |
def process_command(scope_type, scope_value, command) |
|---|
| 240 |
env_var = case scope_type |
|---|
| 241 |
when "with" then "ROLES" |
|---|
| 242 |
when "on" then "HOSTS" |
|---|
| 243 |
end |
|---|
| 244 |
|
|---|
| 245 |
old_var, ENV[env_var] = ENV[env_var], (scope_value == "all" ? nil : scope_value) if env_var |
|---|
| 246 |
if command |
|---|
| 247 |
begin |
|---|
| 248 |
@mutex.synchronize { exec(command) } |
|---|
| 249 |
ensure |
|---|
| 250 |
ENV[env_var] = old_var if env_var |
|---|
| 251 |
end |
|---|
| 252 |
else |
|---|
| 253 |
puts "scoping #{scope_type} #{scope_value}" |
|---|
| 254 |
end |
|---|
| 255 |
end |
|---|
| 256 |
end |
|---|
| 257 |
end |
|---|