class Derelict::Executer

Executes an external (shell) command “safely”

The safety involved is mainly ensuring that the command is gracefully terminated if this process is about to terminate.

Attributes

pid[R]
stderr[R]
stdout[R]

Public Class Methods

execute(command, options = {}, &block) click to toggle source

Executes command and returns after execution

* command: A string containing the command to run
* options: A hash of options, with the following (symbol) keys:
   * :mode:      Controls how the process' output is given to
                 the block, one of :chars (pass each character
                 one by one, retrieved with getc), or :lines
                 (pass only whole lines, retrieved with gets).
                 (optional, defaults to :lines)
   * :no_buffer: If true, the process' stdout and stderr won't
                 be collected in the stdout and stderr
                 properties, and will only be passed to the
                 block (optional, defaults to false)
* block:   Gets passed stdout and stderr every time the process
           outputs to each stream (first parameter is stdout,
           second parameter is stderr; only one will contain
           data, the other will be nil)
# File lib/derelict/executer.rb, line 29
def self.execute(command, options = {}, &block)
  self.new(options).execute(command, &block)
end
new(options = {}) click to toggle source

Initializes an Executer instance with particular options

* options: A hash of options, with the following (symbol) keys:
   * :mode:      Controls how the process' output is given to
                 the block, one of :chars (pass each character
                 one by one, retrieved with getc), or :lines
                 (pass only whole lines, retrieved with gets).
                 (optional, defaults to :lines)
   * :no_buffer: If true, the process' stdout and stderr won't
                 be collected in the stdout and stderr
                 properties, and will only be passed to the
                 block (optional, defaults to false)
# File lib/derelict/executer.rb, line 46
def initialize(options = {})
  @options = {:mode => :lines, :no_buffer => false}.merge(options)

  logger.info "Initializing with options: #{@options.inspect}"

  if @options[:mode] == :chars
    @reader = proc {|s| s.getc }
  else
    @reader = proc {|s| s.gets }
  end

  @mutex = Mutex.new
  reset
end

Public Instance Methods

execute(command, &block) click to toggle source

Executes command and returns after execution

* command: A string containing the command to run
* block:   Gets passed stdout and stderr every time the process
           outputs to each stream (first parameter is stdout,
           second parameter is stderr; only one will contain
           data, the other will be nil)
# File lib/derelict/executer.rb, line 68
def execute(command, &block)
  logger.info "Executing command '#{command}'"
  reset
  pid, stdin, stdout_stream, stderr_stream = Open4::popen4(command)
  @pid = pid
  stdin.close rescue nil

  save_exit_status(pid)
  forward_signals_to(pid) do
    handle_streams stdout_stream, stderr_stream, &block
  end

  self
ensure
  logger.debug "Closing stdout and stderr streams for process"
  stdout.close rescue nil
  stderr.close rescue nil
end
success?() click to toggle source

Determines whether the last command was successful or not

If the command's exit status was zero, this will return true. If the command's exit status is anything else, this will return false. If a command is currently running, this will return nil.

# File lib/derelict/executer.rb, line 92
def success?
  @success
end

Private Instance Methods

buffer(data, stream_name) click to toggle source

Buffers data for a stream into this object to retrieve it later

* data:        The data that should be added to the buffer
* stream_name: Which stream the data came from (:stdout or
               :stderr)
# File lib/derelict/executer.rb, line 220
def buffer(data, stream_name)
  if stream_name == :stdout
    @stdout += data
  else
    @stderr += data
  end
end
forward_signals_to(pid, signals = %w[INT]) { || ... } click to toggle source

Forward signals to a process while running the given block

* pid:     The process ID to forward signals to
* signals: The names of the signals to handle (optional,
           defaults to SIGINT only)
# File lib/derelict/executer.rb, line 129
def forward_signals_to(pid, signals = %w[INT])
  # Set up signal handlers
  logger.debug "Setting up signal handlers for #{signals.inspect}"
  signal_count = 0
  signals.each do |signal|
    Signal.trap(signal) do
      Process.kill signal, pid rescue nil

      # If this is the second time we've received and forwarded
      # on the signal, make sure next time we just give up.
      reset_handlers_for signals if ++signal_count >= 2
    end
  end

  # Run the block now that the signals are being forwarded
  yield
ensure
  # Always good to make sure we clean up after ourselves
  reset_handlers_for signals
end
handle_streams(stdout_stream, stderr_stream, &block) click to toggle source

Manages reading from the stdout and stderr streams

* stdout_stream: The process' stdout stream
* stderr_stream: The process' stderr stream
* block:         The block to pass output to (optional)
# File lib/derelict/executer.rb, line 163
def handle_streams(stdout_stream, stderr_stream, &block)
  logger.debug "Monitoring stdout/stderr streams for output"
  streams = [stdout_stream, stderr_stream]
  until streams.empty?
    # Find which streams are ready for reading, timeout 0.1s
    selected, = select(streams, nil, nil, 0.1)

    # Try again if none were ready
    next if selected.nil? or selected.empty?

    selected.each do |stream|
      if stream.eof?
        logger.debug "Stream reached end-of-file"
        if @success.nil?
          logger.debug "Process hasn't finished, keeping stream"
        else
          logger.debug "Removing stream"
          streams.delete(stream)
        end
        next
      end

      while data = @reader.call(stream)
        data = ((@options[:mode] == :chars) ? data.chr : data)
        stream_name = (stream == stdout_stream) ? :stdout : :stderr
        output data, stream_name, &block unless block.nil?
        buffer data, stream_name unless @options[:no_buffer]
      end
    end
  end
end
output(data, stream_name = :stdout) { |data| ... } click to toggle source

Outputs data to the block

* data:        The data that needs to be passed to the block
* stream_name: The stream data came from (:stdout or :stderr)
* block:       The block to pass the data to
# File lib/derelict/executer.rb, line 200
def output(data, stream_name = :stdout, &block)
  # Pass the output to the block
  if block.arity == 2
    args = [nil, nil]
    if stream_name == :stdout
      args[0] = data
    else
      args[1] = data
    end
    block.call(*args)
  else
    yield data if stream_name == :stdout
  end
end
reset() click to toggle source

Clears the variables relating to a particular command execution

This is done when first initialising, and just before a command is run, to get rid of the previous command's data.

# File lib/derelict/executer.rb, line 101
def reset
  logger.debug "Resetting executer state"
  @stdout = ''
  @stderr = ''
  @success = nil
  @pid = nil
end
reset_handlers_for(signals) click to toggle source

Resets the handlers for particular signals to the default

* signals: An array of signal names to reset the handlers for
# File lib/derelict/executer.rb, line 153
def reset_handlers_for(signals)
  logger.debug "Resetting signal handlers for #{signals.inspect}"
  signals.each {|signal| Signal.trap signal, "DEFAULT" }
end
save_exit_status(pid) click to toggle source

Waits for the exit status of a process (in a thread) saving it

This will set the @status instance variable to true if the exit status was 0, or false if the exit status was anything else.

# File lib/derelict/executer.rb, line 113
def save_exit_status(pid)
  logger.debug "Spawning thread to monitor process ID #{pid}"
  @success = nil
  Thread.start do
    logger.debug "Thread started, waiting for PID #{pid}"
    status = Process.waitpid2(pid).last.exitstatus
    logger.debug "Process exited with status #{status}"
    @success = (status == 0)
  end
end