class ProcessExecuter::MonitoredPipe

Acts as a pipe that writes the data written to it to one or more destinations

{ProcessExecuter::MonitoredPipe} was created to expand the output redirection options for [Process.spawn](docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn) and methods derived from it within the ‘ProcessExecuter` module.

This class’s initializer accepts any redirection destination supported by [Process.spawn](docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn) (this is the ‘value` part of the file redirection option described in [the File Redirection section of `Process.spawn`](docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-File+Redirection+-28File+Descriptor-29).

In addition to the standard redirection destinations, {ProcessExecuter::MonitoredPipe} also supports these additional types of destinations:

When a new MonitoredPipe is created, a pipe is created (via IO.pipe) and a thread is created to read data written to the pipe. As data is read from the pipe, it is written to the destination provided in the MonitoredPipe initializer.

If the destination raises an exception (of any class, not just ‘StandardError`), the monitoring thread will exit, the pipe will be closed, and the exception will be saved in `#exception`.

> **⚠️ WARNING** > > ‘#close` must be called to ensure that (1) the pipe is closed, (2) all data is

read from the pipe and written to the destination, and (3) the monitoring thread is
killed.

## File descriptor usage

Each MonitoredPipe holds four file descriptors: two for the data pipe the subprocess writes to and two for an internal wake pipe that interrupts the monitoring thread when the pipe is closed. All four are held concurrently from construction until the pipe is closed, so a ‘ProcessExecuter.run` that captures stdout and stderr holds 8 pipe file descriptors for the duration of the subprocess.

File descriptor limits are per-process. An application spawning many commands concurrently can hit the soft ‘RLIMIT_NOFILE` limit – macOS defaults to 256 and Linux commonly to 1024. The workaround is to raise the limit in the spawning process, for example with `Process.setrlimit(:NOFILE, 10_240)` or `ulimit -n`.

@example Collect pipe data into a StringIO object

pipe_data = StringIO.new
begin
  pipe = ProcessExecuter::MonitoredPipe.new(pipe_data)
  pipe.write("Hello World")
ensure
  pipe.close
end
pipe_data.string #=> "Hello World"

@example Collect pipe data into a string AND a file

pipe_data_string = StringIO.new
pipe_data_file = File.open("pipe_data.txt", "w")
begin
  pipe = ProcessExecuter::MonitoredPipe.new([:tee, pipe_data_string, pipe_data_file])
  pipe.write("Hello World")
ensure
  pipe.close
end
pipe_data_string.string #=> "Hello World"
# It is your responsibility to close the file you opened
pipe_data_file.close
File.read("pipe_data.txt") #=> "Hello World"

@example Using a MonitoredPipe with Process.spawn

stdout_buffer = StringIO.new
begin
  stdout_pipe = ProcessExecuter::MonitoredPipe.new(stdout_buffer)
  pid = Process.spawn('echo Hello World', out: stdout_pipe)
  _waited_pid, status = Process.wait2(pid)
ensure
  stdout_pipe.close
end
stdout_buffer.string #=> "Hello World\n"

@api public