Class: Vagrant::Util::Subprocess
- Inherits:
-
Object
- Object
- Vagrant::Util::Subprocess
- Defined in:
- lib/vagrant/util/subprocess.rb
Overview
Execute a command in a subprocess, gathering the results and exit status.
This class also allows you to read the data as it is outputted from the subprocess in real time, by simply passing a block to the execute method.
Defined Under Namespace
Classes: LaunchError, Result, TimeoutExceeded
Class Method Summary collapse
-
.execute(*command, &block) ⇒ Object
Convenience method for executing a method.
Instance Method Summary collapse
-
#execute ⇒ Result
Start the process.
-
#initialize(*command) ⇒ Subprocess
constructor
A new instance of Subprocess.
-
#running? ⇒ TrueClass, FalseClass
Subprocess is currently running.
-
#stop ⇒ TrueClass
Stop the subprocess if running.
Constructor Details
#initialize(*command) ⇒ Subprocess
Returns a new instance of Subprocess.
28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/vagrant/util/subprocess.rb', line 28 def initialize(*command) @options = command.last.is_a?(Hash) ? command.pop : {} @command = command.dup @command[0] = Which.which(@command[0]) if !File.file?(@command[0]) if !@command[0] raise Errors::CommandUnavailableWindows, file: command[0] if Platform.windows? raise Errors::CommandUnavailable, file: command[0] end @logger = Log4r::Logger.new("vagrant::util::subprocess") end |
Class Method Details
.execute(*command, &block) ⇒ Object
Convenience method for executing a method.
24 25 26 |
# File 'lib/vagrant/util/subprocess.rb', line 24 def self.execute(*command, &block) new(*command).execute(&block) end |
Instance Method Details
#execute ⇒ Result
Start the process
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 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
# File 'lib/vagrant/util/subprocess.rb', line 60 def execute # Get the timeout, if we have one timeout = @options[:timeout] # Get the working directory workdir = @options[:workdir] || Dir.pwd # Get what we're interested in being notified about notify = @options[:notify] || [] notify = [notify] if !notify.is_a?(Array) if notify.empty? && block_given? # If a block is given, subscribers must be given, otherwise the # block is never called. This is usually NOT what you want, so this # is an error. = "A list of notify subscriptions must be given if a block is given" raise ArgumentError, end # Let's get some more useful booleans that we access a lot so # we're not constantly calling an `include` check notify_table = {} notify_table[:stderr] = notify.include?(:stderr) notify_table[:stdout] = notify.include?(:stdout) notify_stdin = notify.include?(:stdin) # Build the ChildProcess @logger.info("Starting process: #{@command.inspect}") @process = process = ChildProcess.build(*@command) # Create the pipes so we can read the output in real time as # we execute the command. stdout, stdout_writer = ::IO.pipe stderr, stderr_writer = ::IO.pipe process.io.stdout = stdout_writer process.io.stderr = stderr_writer process.duplex = true process.leader = true if @options[:detach] process.detach = true if @options[:detach] # Special installer-related things if Vagrant.in_installer? installer_dir = Vagrant..to_s.downcase # If we're in an installer on Mac and we're executing a command # in the installer context, then force DYLD_LIBRARY_PATH to look # at our libs first. if Platform.darwin? if @command[0].downcase.include?(installer_dir) @logger.info("Command in the installer. Specifying DYLD_LIBRARY_PATH...") process.environment["DYLD_LIBRARY_PATH"] = "#{installer_dir}/lib:#{ENV["DYLD_LIBRARY_PATH"]}" else @logger.debug("Command not in installer, not touching env vars.") end if File.setuid?(@command[0]) || File.setgid?(@command[0]) @logger.info("Command is setuid/setgid, clearing DYLD_LIBRARY_PATH") process.environment["DYLD_LIBRARY_PATH"] = "" end end # If the command that is being run is not inside the installer, reset # the original environment - this is required for shelling out to # other subprocesses that depend on environment variables (like Ruby # and $GEM_PATH for example) internal = [installer_dir, Vagrant.user_data_path.to_s.downcase]. any? { |path| @command[0].downcase.include?(path) } if !internal @logger.info("Command not in installer, restoring original environment...") jailbreak(process.environment) end # If running within an AppImage and calling external executable. When # executable is external set the LD_LIBRARY_PATH to host values. if ENV["VAGRANT_APPIMAGE"] = Pathname.new(Vagrant.)..to_s exec_path = Pathname.new(@command[0])..to_s if !exec_path.start_with?() && ENV["VAGRANT_APPIMAGE_HOST_LD_LIBRARY_PATH"] @logger.info("Detected AppImage environment and request to external binary. Updating library path.") @logger.debug("Setting LD_LIBRARY_PATH to #{ENV["VAGRANT_APPIMAGE_HOST_LD_LIBRARY_PATH"]}") process.environment["LD_LIBRARY_PATH"] = ENV["VAGRANT_APPIMAGE_HOST_LD_LIBRARY_PATH"].to_s end end else @logger.info("Vagrant not running in installer, restoring original environment...") jailbreak(process.environment) end # Set the environment on the process if we must if @options[:env] @options[:env].each do |k, v| process.environment[k] = v end end # Start the process begin SafeChdir.safe_chdir(workdir) do process.start end rescue ChildProcess::LaunchError => ex # Raise our own version of the error so that users of the class # don't need to be aware of ChildProcess raise LaunchError.new(ex.) end # If running with the detach option, no need to capture IO or # ensure program exists. if @options[:detach] return end # Make sure the stdin does not buffer process.io.stdin.sync = true if RUBY_PLATFORM != "java" # On Java, we have to close after. See down the method... # Otherwise, we close the writers right here, since we're # not on the writing side. stdout_writer.close stderr_writer.close end # Create a dictionary to store all the output we see. io_data = { stdout: "", stderr: "" } # Record the start time for timeout purposes start_time = Time.now.to_i open_readers = [stdout, stderr] open_writers = notify_stdin ? [process.io.stdin] : [] @logger.debug("Selecting on IO") while true results = ::IO.select(open_readers, open_writers, nil, 0.1) results ||= [] readers = results[0] writers = results[1] # Check if we have exceeded our timeout raise TimeoutExceeded, process.pid if timeout && (Time.now.to_i - start_time) > timeout # Check the readers to see if they're ready if readers && !readers.empty? readers.each do |r| # Read from the IO object data = IO.read_until_block(r) # We don't need to do anything if the data is empty next if data.empty? io_name = r == stdout ? :stdout : :stderr @logger.trace("#{io_name}: #{data.chomp}") io_data[io_name] += data yield io_name, data if block_given? && notify_table[io_name] end end # Break out if the process exited. We have to do this before # attempting to write to stdin otherwise we'll get a broken pipe # error. break if process.exited? # Check the writers to see if they're ready, and notify any listeners if writers && !writers.empty? && block_given? yield :stdin, process.io.stdin # if the callback closed stdin, we should remove it, because # IO.select() will throw if called with a closed io. if process.io.stdin.closed? open_writers = [] end end end # Wait for the process to end. begin remaining = (timeout || 32000) - (Time.now.to_i - start_time) remaining = 0 if remaining < 0 @logger.debug("Waiting for process to exit. Remaining to timeout: #{remaining}") process.poll_for_exit(remaining) rescue ChildProcess::TimeoutError raise TimeoutExceeded, process.pid end @logger.debug("Exit status: #{process.exit_code}") # Read the final output data, since it is possible we missed a small # amount of text between the time we last read data and when the # process exited. [stdout, stderr].each do |io| # Read the extra data, ignoring if there isn't any extra_data = IO.read_until_block(io) next if extra_data == "" # Log it out and accumulate io_name = io == stdout ? :stdout : :stderr io_data[io_name] += extra_data @logger.trace("#{io_name}: #{extra_data.chomp}") # Yield to any listeners any remaining data yield io_name, extra_data if block_given? && notify_table[io_name] end if RUBY_PLATFORM == "java" # On JRuby, we need to close the writers after the process, # for some reason. See GH-711. stdout_writer.close stderr_writer.close end # Return an exit status container return Result.new(process.exit_code, io_data[:stdout], io_data[:stderr]) ensure if process && process.alive? && !@options[:detach] # Make sure no matter what happens, the process exits process.stop(2) end end |
#running? ⇒ TrueClass, FalseClass
Returns subprocess is currently running.
41 42 43 |
# File 'lib/vagrant/util/subprocess.rb', line 41 def running? !!(@process && @process.alive?) end |
#stop ⇒ TrueClass
Stop the subprocess if running
48 49 50 51 52 53 54 55 |
# File 'lib/vagrant/util/subprocess.rb', line 48 def stop if @process && @process.alive? @process.stop true else false end end |