Class: VCAP::Subprocess
- Inherits:
-
Object
- Object
- VCAP::Subprocess
- Defined in:
- lib/vcap/subprocess.rb
Overview
Utility class providing:
- Ability to capture stdout/stderr of a command
- Exceptions when commands fail (useful for running a chain of commands)
- Easier integration with unit tests.
Constant Summary collapse
- READ_SIZE =
4096
Class Method Summary collapse
Instance Method Summary collapse
-
#run(command, expected_exit_status = 0, timeout = nil, options = {}, env = {}) ⇒ Object
Runs the supplied command in a subshell.
Class Method Details
.run(*args) ⇒ Object
61 62 63 |
# File 'lib/vcap/subprocess.rb', line 61 def self.run(*args) VCAP::Subprocess.new.run(*args) end |
Instance Method Details
#run(command, expected_exit_status = 0, timeout = nil, options = {}, env = {}) ⇒ Object
Runs the supplied command in a subshell.
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 |
# File 'lib/vcap/subprocess.rb', line 85 def run(command, expected_exit_status=0, timeout=nil, ={}, env={}) # We use a pipe to ourself to time out long running commands (if desired) as follows: # 1. Set up a pipe to ourselves # 2. Install a signal handler that writes to one end of our pipe on SIGCHLD # 3. Select on the read end of our pipe and check if our process exited sigchld_r, sigchld_w = IO.pipe prev_sigchld_handler = install_sigchld_handler(sigchld_w) start = Time.now.to_i child_pid, stdin, stdout, stderr = POSIX::Spawn.popen4(env, command, ) stdin.close # Used to look up the name of an io object when an errors occurs while # reading from it, as well as to look up the corresponding buffer to # append to. io_map = { stderr => { :name => 'stderr', :buf => '' }, stdout => { :name => 'stdout', :buf => '' }, sigchld_r => { :name => 'sigchld_r', :buf => '' }, sigchld_w => { :name => 'sigchld_w', :buf => '' }, } status = nil time_left = timeout read_cands = [stdout, stderr, sigchld_r] error_cands = read_cands.dup begin while read_cands.length > 0 active_ios = IO.select(read_cands, nil, error_cands, time_left) # Check if timeout was hit if timeout time_left = timeout - (Time.now.to_i - start) unless active_ios && (time_left > 0) raise VCAP::SubprocessTimeoutError.new(timeout, command, io_map[stdout][:buf], io_map[stderr][:buf]) end end # Read as much as we can from the readable ios before blocking for io in active_ios[0] begin io_map[io][:buf] << io.read_nonblock(READ_SIZE) rescue IO::WaitReadable # Reading would block, so put ourselves back on the loop rescue EOFError # Pipe has no more data, remove it from the readable/error set # NB: We cannot break from the loop here, as the other pipes may have data to be read read_cands.delete(io) error_cands.delete(io) end # Our signal handler notified us that >= 1 children have exited; # check if our child has exited. if (io == sigchld_r) && Process.waitpid(child_pid, Process::WNOHANG) status = $? read_cands.delete(sigchld_r) error_cands.delete(sigchld_r) end end # Error reading from one or more pipes. unless active_ios[2].empty? io_names = active_ios[2].map {|io| io_map[io][:name] } raise SubprocessReadError.new(io_names.join(', '), command, io_map[stdout][:buf], io_map[stderr][:buf]) end end rescue # A timeout or an error occurred while reading from one or more pipes. # Kill the process if we haven't reaped its exit status already. kill_pid(child_pid) unless status raise ensure # Make sure we reap the child's exit status, close our fds, and restore # the previous SIGCHLD handler unless status Process.waitpid(child_pid) status = $? end io_map.each_key {|io| io.close unless io.closed? } trap('CLD') { prev_sigchld_handler.call } if prev_sigchld_handler end unless status.exitstatus == expected_exit_status raise SubprocessStatusError.new(command, io_map[stdout][:buf], io_map[stderr][:buf], status) end [io_map[stdout][:buf], io_map[stderr][:buf], status] end |