lib/child_labor.rb



module ChildLabor
  def self.subprocess(cmd)
    t = Task.new(cmd)
    t.launch

    if block_given?
      begin
        yield t
      ensure
        t.wait
        t.close
      end
    end

    return t
  end

  class Task
    attr_reader :cmd, :stdout, :stderr, :stdin

    def initialize(cmd)
      @cmd = cmd
      @pid = nil
      @exit_status = nil
      @terminated  = false
    end

    def launch
      create_pipes

      @pid = Process.fork

      # child
      unless @pid
        @stdout.close
        STDOUT.reopen @stdout_child

        @stdin.close
        STDIN.reopen @stdin_child

        @stderr.close
        STDERR.reopen @stderr_child

        Process.exec cmd
      end

      @stdout_child.close
      @stdin_child.close
      @stderr_child.close

      true
    end

    def launched?
      @pid
    end

    def running?
      poll_status(Process::WNOHANG)
    end

    def terminated?
      launched? && !running?
    end

    def exit_status
      poll_status
      @exit_status
    end

    def wait
      exit_status
    end

    def suspend
      signal('STOP')
    end

    def resume
      signal('CONT')
    end

    def terminate(signal = 'TERM')
      signal(signal)
    end

    def read(length = nil, buffer = nil)
      @stdout.read(length, buffer)
    end

    def readline
      @stdout.readline
    end

    def read_stderr(length = nil, buffer = nil)
      @stderr.read(length, buffer)
    end

    def write(str)
      @stdin.write(str)
    end

    def signal(signal)
      Process.kill(signal, @pid)
    end

    def close_read
      @stdout.close if @stdout
      @stdout = nil
    end

    def close_write
      @stdin.close if @stdin
      @stdin = nil
    end

    def close_stderr
      @stderr.close if @stderr
      @stderr = nil
    end

    def close
      close_read
      close_write
      close_stderr
    end

  private

    # Handles the state of the Task in a single call to
    # Process.wait2.
    # Returns true if the process is currently running
    def poll_status(flags = 0)
      return false unless @pid
      return false if @terminated

      pid, status = Process.wait2(@pid, flags)

      return true unless pid

      @terminated  = true
      @exit_status = status
      false
    rescue Errno::ECHILD
      false
    end

    def create_pipes
      @stdout, @stdout_child = IO.pipe
      @stdin_child, @stdin   = IO.pipe
      @stderr, @stderr_child = IO.pipe
    end
  end
end