@ -1,61 +1,64 @@
# frozen_string_literal: false
# Fix adapted from https://github.com/thoughtbot/terrapin/pull/5
require 'fcntl'
module Terrapin
module MultiPipeExtensions
def read
read_streams ( @stdout_in , @stderr_in )
end
def initialize
@stdout_in , @stdout_out = IO . pipe
@stderr_in , @stderr_out = IO . pipe
def close_read
begin
@stdout_in . close
rescue IOError
# Do nothing
end
begin
@stderr_in . close
rescue IOError
# Do nothing
end
clear_nonblocking_flags!
end
def read_streams ( output , error )
@stdout_output = ''
@stderr_output = ''
def pipe_options
# Add some flags to explicitly close the other end of the pipes
{ out : @stdout_out , err : @stderr_out , @stdout_in = > :close , @stderr_in = > :close }
end
read_fds = [ output , error ]
def read
# While we are patching Terrapin, fix child process potentially getting stuck on writing
# to stderr.
until read_fds . empty?
to_read , = IO . select ( read_fds )
@stdout_output = + ''
@stderr_output = + ''
if to_read . include? ( output )
@stdout_output << read_stream ( output )
read_fds . delete ( output ) if output . closed?
end
fds_to_read = [ @stdout_in , @stderr_in ]
until fds_to_read . empty?
rs , = IO . select ( fds_to_read )
if to_read . include? ( error )
@stderr_output << read_stream ( error )
read_fds . delete ( error ) if error . closed?
end
read_nonblocking! ( @stdout_in , @stdout_output , fds_to_read ) if rs . include? ( @stdout_in )
read_nonblocking! ( @stderr_in , @stderr_output , fds_to_read ) if rs . include? ( @stderr_in )
end
end
def read_stream ( io )
result = ''
begin
while ( partial_result = io . read_nonblock ( 8192 ) )
result << partial_result
end
rescue EOFError , Errno :: EPIPE
io . close
rescue Errno :: EINTR , Errno :: EWOULDBLOCK , Errno :: EAGAIN
# Do nothing
private
# @param [IO] io IO Stream to read until there is nothing to read
# @param [String] result Mutable string to which read values will be appended to
# @param [Array<IO>] fds_to_read Mutable array from which `io` should be removed on EOF
def read_nonblocking! ( io , result , fds_to_read )
while ( partial_result = io . read_nonblock ( 8192 ) )
result << partial_result
end
rescue IO :: WaitReadable
# Do nothing
rescue EOFError
fds_to_read . delete ( io )
end
def clear_nonblocking_flags!
# Ruby 3.0 sets pipes to non-blocking mode, and resets the flags as
# needed when calling fork/exec-related syscalls, but posix-spawn does
# not currently do that, so we need to do it manually for the time being
# so that the child process do not error out when the buffers are full.
stdout_flags = @stdout_out . fcntl ( Fcntl :: F_GETFL )
@stdout_out . fcntl ( Fcntl :: F_SETFL , stdout_flags & ~ Fcntl :: O_NONBLOCK ) if stdout_flags & Fcntl :: O_NONBLOCK
result
stderr_flags = @stderr_out . fcntl ( Fcntl :: F_GETFL )
@stderr_out . fcntl ( Fcntl :: F_SETFL , stderr_flags & ~ Fcntl :: O_NONBLOCK ) if stderr_flags & Fcntl :: O_NONBLOCK
rescue NameError , NotImplementedError , Errno :: EINVAL
# Probably on windows, where pipes are blocking by default
end
end
end