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

root/tools/capistrano/lib/capistrano/command.rb

Revision 7388, 5.1 kB (checked in by minam, 2 years ago)

Set :shell to false to run a command without wrapping it in "sh -c" (closes #9290)

Line 
1 require 'capistrano/errors'
2
3 module Capistrano
4
5   # This class encapsulates a single command to be executed on a set of remote
6   # machines, in parallel.
7   class Command
8     attr_reader :command, :sessions, :options
9
10     def self.process(command, sessions, options={}, &block)
11       new(command, sessions, options, &block).process!
12     end
13
14     # Instantiates a new command object. The +command+ must be a string
15     # containing the command to execute. +sessions+ is an array of Net::SSH
16     # session instances, and +options+ must be a hash containing any of the
17     # following keys:
18     #
19     # * +logger+: (optional), a Capistrano::Logger instance
20     # * +data+: (optional), a string to be sent to the command via it's stdin
21     # * +env+: (optional), a string or hash to be interpreted as environment
22     #   variables that should be defined for this command invocation.
23     def initialize(command, sessions, options={}, &block)
24       @command = command.strip.gsub(/\r?\n/, "\\\n")
25       @sessions = sessions
26       @options = options
27       @callback = block
28       @channels = open_channels
29     end
30
31     # Processes the command in parallel on all specified hosts. If the command
32     # fails (non-zero return code) on any of the hosts, this will raise a
33     # Capistrano::CommandError.
34     def process!
35       since = Time.now
36       loop do
37         active = 0
38         @channels.each do |ch|
39           next if ch[:closed]
40           active += 1
41           ch.connection.process(true)
42         end
43
44         break if active == 0
45         if Time.now - since >= 1
46           since = Time.now
47           @channels.each { |ch| ch.connection.ping! }
48         end
49         sleep 0.01 # a brief respite, to keep the CPU from going crazy
50       end
51
52       logger.trace "command finished" if logger
53
54       if (failed = @channels.select { |ch| ch[:status] != 0 }).any?
55         hosts = failed.map { |ch| ch[:server] }
56         error = CommandError.new("command #{command.inspect} failed on #{hosts.join(',')}")
57         error.hosts = hosts
58         raise error
59       end
60
61       self
62     end
63
64     # Force the command to stop processing, by closing all open channels
65     # associated with this command.
66     def stop!
67       @channels.each do |ch|
68         ch.close unless ch[:closed]
69       end
70     end
71
72     private
73
74       def logger
75         options[:logger]
76       end
77
78       def open_channels
79         sessions.map do |session|
80           session.open_channel do |channel|
81             server = session.xserver
82
83             channel[:server] = server
84             channel[:host] = server.host
85             channel[:options] = options
86
87             execute_command = Proc.new do |ch|
88               logger.trace "executing command", ch[:server] if logger
89               cmd = replace_placeholders(command, ch)
90
91               if options[:shell] == false
92                 shell = nil
93               else
94                 shell = "#{options[:shell] || "sh"} -c"
95                 cmd = cmd.gsub(/[$\\`"]/) { |m| "\\#{m}" }
96                 cmd = "\"#{cmd}\""
97               end
98
99               command_line = [environment, shell, cmd].compact.join(" ")
100
101               ch.exec(command_line)
102               ch.send_data(options[:data]) if options[:data]
103             end
104
105             if options[:pty]
106               channel.request_pty(:want_reply => true)
107               channel.on_success(&execute_command)
108               channel.on_failure do |ch|
109                 # just log it, don't actually raise an exception, since the
110                 # process method will see that the status is not zero and will
111                 # raise an exception then.
112                 logger.important "could not open channel", ch[:server] if logger
113                 ch.close
114               end
115             else
116               execute_command.call(channel)
117             end
118              
119             channel.on_data do |ch, data|
120               @callback[ch, :out, data] if @callback
121             end
122
123             channel.on_extended_data do |ch, type, data|
124               @callback[ch, :err, data] if @callback
125             end
126
127             channel.on_request do |ch, request, reply, data|
128               ch[:status] = data.read_long if request == "exit-status"
129             end
130
131             channel.on_close do |ch|
132               ch[:closed] = true
133             end
134           end
135         end
136       end
137
138       def replace_placeholders(command, channel)
139         command.gsub(/\$CAPISTRANO:HOST\$/, channel[:host])
140       end
141
142       # prepare a space-separated sequence of variables assignments
143       # intended to be prepended to a command, so the shell sets
144       # the environment before running the command.
145       # i.e.: options[:env] = {'PATH' => '/opt/ruby/bin:$PATH',
146       #                        'TEST' => '( "quoted" )'}
147       # environment returns:
148       # "env TEST=(\ \"quoted\"\ ) PATH=/opt/ruby/bin:$PATH"
149       def environment
150         return if options[:env].nil? || options[:env].empty?
151         @environment ||= if String === options[:env]
152             "env #{options[:env]}"
153           else
154             options[:env].inject("env") do |string, (name, value)|
155               value = value.to_s.gsub(/[ "]/) { |m| "\\#{m}" }
156               string << " #{name}=#{value}"
157             end
158           end
159       end
160   end
161 end
Note: See TracBrowser for help on using the browser.