Class: Exeggutor::ProcessHandle

Inherits:
Object
  • Object
show all
Defined in:
lib/exeggutor.rb

Overview

A handle to a process, with IO handles to communicate with it and a ProcessResult object when it’s done. It’s largely similar to the array of 4 values return by Open3.popen3. However, it doesn’t suffer from that library’s dead-locking issue. For example, even if lots of data has been written to stdout that hasn’t been read, the subprocess can still write to stdout and stderr without blocking

Instance Method Summary collapse

Constructor Details

#initialize(args, env: nil, chdir: nil) ⇒ ProcessHandle

Returns a new instance of ProcessHandle.



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/exeggutor.rb', line 13

def initialize(args, env: nil, chdir: nil)
  @stdin_io, @stdout_io, @stderr_io, @wait_thread = Exeggutor::run_popen3(args, env, chdir)

  # Make the streams as synchronous as possible, to minimize the possibility of a surprising lack
  # of output
  @stdout_io.sync = true
  @stderr_io.sync = true

  @stdout_queue = Queue.new
  @stderr_queue = Queue.new

  @stdout_pipe_reader, @stdout_pipe_writer = IO.pipe
  @stderr_pipe_reader, @stderr_pipe_writer = IO.pipe

  @stdout_write_thread = Thread.new do
    loop do
      data = @stdout_queue.pop
      break if !data # Queue is closed
      @stdout_pipe_writer.write(data)
    end
    @stdout_pipe_writer.close
  end

  @stderr_write_thread = Thread.new do
    loop do
      data = @stderr_queue.pop
      break if !data # Queue is closed
      @stderr_pipe_writer.write(data)
    end
    @stderr_pipe_writer.close
  end

  # popen3 can deadlock if one stream is written to too much without being read,
  # so it's important to continuously read from both streams. This is why
  # we can't just let the user call .gets on the streams themselves
  @read_thread = Thread.new do
    remaining_ios = [@stdout_io, @stderr_io]
    while remaining_ios.size > 0
      readable_ios, = IO.select(remaining_ios)
      for readable_io in readable_ios
        begin
          data = readable_io.read_nonblock(100_000)
          if readable_io == @stdout_io
            @stdout_queue.push(data)
          else
            @stderr_queue.push(data)
          end
        rescue IO::WaitReadable
          # Shouldn't usually happen because IO.select indicated data is ready, but maybe due to EINTR or something
          next
        rescue EOFError
          if readable_io == @stdout_io
            @stdout_queue.close
          else
            @stderr_queue.close
          end
          remaining_ios.delete(readable_io)
        end
      end
    end
  end
end

Instance Method Details

#stderrObject

An IO object for stderr that can be written to



93
94
95
# File 'lib/exeggutor.rb', line 93

def stderr
  @stderr_pipe_reader
end

#stdinObject

An IO object for stdin that can be written to



83
84
85
# File 'lib/exeggutor.rb', line 83

def stdin
  @stdin_io
end

#stdoutObject

An IO object for stdout that can be written to



88
89
90
# File 'lib/exeggutor.rb', line 88

def stdout
  @stdout_pipe_reader
end

#wait_thrObject

An object containing process metadata and which can be waited on to wait until the subprocess ends. Identical to popen3’s wait_thr



78
79
80
# File 'lib/exeggutor.rb', line 78

def wait_thr
  @wait_thread
end