public
Description: Remote multi-server automation tool. This repository is no longer being actively maintained. Please ask on the mailing list to find someone who has a well-maintained fork. Thanks!
Homepage: http://www.capify.org
Clone URL: git://github.com/jamis/capistrano.git
capistrano / lib / capistrano / shell.rb
100644 261 lines (224 sloc) 8.107 kb
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
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 /^(?:(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(/\bsudo\b/, "sudo -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
 
        @mutex = Mutex.new
        @bgthread = Thread.new do
          loop do
            @mutex.synchronize { process_iteration(0.1) }
          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
    end
 
    # All open sessions, needed to satisfy the Command::Processable include
    def sessions
      configuration.sessions.values
    end
end