about summary refs log tree commit diff
path: root/lib/terrapin/multi_pipe_extensions.rb
blob: 209f4ad6ced5f6856f527f232bbee85147785cb6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
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
# 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)