about summary refs log tree commit diff
path: root/lib/terrapin/multi_pipe_extensions.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/terrapin/multi_pipe_extensions.rb')
-rw-r--r--lib/terrapin/multi_pipe_extensions.rb66
1 files changed, 66 insertions, 0 deletions
diff --git a/lib/terrapin/multi_pipe_extensions.rb b/lib/terrapin/multi_pipe_extensions.rb
new file mode 100644
index 000000000..209f4ad6c
--- /dev/null
+++ b/lib/terrapin/multi_pipe_extensions.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: false
+
+require 'fcntl'
+
+module Terrapin
+  module MultiPipeExtensions
+    def initialize
+      @stdout_in, @stdout_out = IO.pipe
+      @stderr_in, @stderr_out = IO.pipe
+
+      clear_nonblocking_flags!
+    end
+
+    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
+
+    def read
+      # While we are patching Terrapin, fix child process potentially getting stuck on writing
+      # to stderr.
+
+      @stdout_output = +''
+      @stderr_output = +''
+
+      fds_to_read = [@stdout_in, @stderr_in]
+      until fds_to_read.empty?
+        rs, = IO.select(fds_to_read)
+
+        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
+
+    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
+
+      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
+
+Terrapin::CommandLine::MultiPipe.prepend(Terrapin::MultiPipeExtensions)