Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/tools/capistrano/lib/capistrano/shell.rb

Revision 9025, 8.0 kB (checked in by minam, 1 year ago)

Ensure that the default run options are mixed into the command options when executing a command from the cap shell (closes #11348)

Line 
1 require 'thread'
2
3 module Capistrano
4   # The Capistrano::Shell class is the guts of the "shell" task. It implements
5   # an interactive REPL interface that users can employ to execute tasks and
6   # commands. It makes for a GREAT way to monitor systems, and perform quick
7   # maintenance on one or more machines.
8   class Shell
9     # A Readline replacement for platforms where readline is either
10     # unavailable, or has not been installed.
11     class ReadlineFallback #:nodoc:
12       HISTORY = []
13
14       def self.readline(prompt)
15         STDOUT.print(prompt)
16         STDOUT.flush
17         STDIN.gets
18       end
19     end
20
21     # The configuration instance employed by this shell
22     attr_reader :configuration
23
24     # Instantiate a new shell and begin executing it immediately.
25     def self.run(config)
26       new(config).run!
27     end
28
29     # Instantiate a new shell
30     def initialize(config)
31       @configuration = config
32     end
33
34     # Start the shell running. This method will block until the shell
35     # terminates.
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       # Present the prompt and read a single line from the console. It also
76       # detects ^D and returns "exit" in that case. Adds the input to the
77       # history, unless the input is empty. Loops repeatedly until a non-empty
78       # line is input.
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       # Display a verbose help message.
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       # Determine which servers the given task requires a connection to, and
128       # establish connections to them if necessary. Return the list of
129       # servers (names).
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       # Execute the given command. If the command is prefixed by an exclamation
141       # mark, it is assumed to refer to another capistrano task, which will
142       # be invoked. Otherwise, it is executed as a command on all associated
143       # servers.
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       # Given an array of task names, invoke them in sequence.
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       # Execute a command on the given list of servers.
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       # Return the object that will be used to query input from the console.
183       # The returned object will quack (more or less) like Readline.
184       def reader
185         @reader ||= begin
186           require 'readline'
187           Readline
188         rescue LoadError
189           ReadlineFallback
190         end
191       end
192
193       # Prepare every little thing for the shell. Starts the background
194       # thread and generally gets things ready for the REPL.
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       # Set the given option to +value+.
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       # Process a command. Interprets the scope_type (must be nil, "with", or
236       # "on") and the command. If no command is given, then the scope is made
237       # effective for all subsequent commands. If the scope value is "all",
238       # then the scope is unrestricted.
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
Note: See TracBrowser for help on using the browser.