Class: VCAP::Subprocess

Inherits:
Object
  • Object
show all
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

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.

Parameters:

  • command

    String The command to be run

  • expected_exit_status (defaults to: 0)

    Integer The expected exit status of the command in [0, 255]

  • timeout (defaults to: nil)

    Integer How long the command should be allowed to run for nil indicates no timeout

  • options (defaults to: {})

    Hash Options to be passed to Posix::Spawn See github.com/rtomayko/posix-spawn

  • env (defaults to: {})

    Hash Environment to be passed to Posix::Spawn See github.com/rtomayko/posix-spawn

Returns:

  • Array An array of [stdout, stderr, status]. Note that status is an instance of Process::Status.

Raises:

  • VCAP::SubprocessStatusError Thrown if the exit status does not match the expected exit status.

  • VCAP::SubprocessTimeoutError Thrown if a timeout occurs.

  • VCAP::SubprocessReadError Thrown if there is an error reading from any of the pipes to the child.



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, options={}, 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, options)
  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