multi_pipe_extensions.rb 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. # frozen_string_literal: false
  2. require 'fcntl'
  3. module Terrapin
  4. module MultiPipeExtensions
  5. def initialize
  6. @stdout_in, @stdout_out = IO.pipe
  7. @stderr_in, @stderr_out = IO.pipe
  8. clear_nonblocking_flags!
  9. end
  10. def pipe_options
  11. # Add some flags to explicitly close the other end of the pipes
  12. { out: @stdout_out, err: @stderr_out, @stdout_in => :close, @stderr_in => :close }
  13. end
  14. def read
  15. # While we are patching Terrapin, fix child process potentially getting stuck on writing
  16. # to stderr.
  17. @stdout_output = +''
  18. @stderr_output = +''
  19. fds_to_read = [@stdout_in, @stderr_in]
  20. until fds_to_read.empty?
  21. rs, = IO.select(fds_to_read)
  22. read_nonblocking!(@stdout_in, @stdout_output, fds_to_read) if rs.include?(@stdout_in)
  23. read_nonblocking!(@stderr_in, @stderr_output, fds_to_read) if rs.include?(@stderr_in)
  24. end
  25. end
  26. private
  27. # @param [IO] io IO Stream to read until there is nothing to read
  28. # @param [String] result Mutable string to which read values will be appended to
  29. # @param [Array<IO>] fds_to_read Mutable array from which `io` should be removed on EOF
  30. def read_nonblocking!(io, result, fds_to_read)
  31. while (partial_result = io.read_nonblock(8192))
  32. result << partial_result
  33. end
  34. rescue IO::WaitReadable
  35. # Do nothing
  36. rescue EOFError
  37. fds_to_read.delete(io)
  38. end
  39. def clear_nonblocking_flags!
  40. # Ruby 3.0 sets pipes to non-blocking mode, and resets the flags as
  41. # needed when calling fork/exec-related syscalls, but posix-spawn does
  42. # not currently do that, so we need to do it manually for the time being
  43. # so that the child process do not error out when the buffers are full.
  44. stdout_flags = @stdout_out.fcntl(Fcntl::F_GETFL)
  45. @stdout_out.fcntl(Fcntl::F_SETFL, stdout_flags & ~Fcntl::O_NONBLOCK) if stdout_flags & Fcntl::O_NONBLOCK
  46. stderr_flags = @stderr_out.fcntl(Fcntl::F_GETFL)
  47. @stderr_out.fcntl(Fcntl::F_SETFL, stderr_flags & ~Fcntl::O_NONBLOCK) if stderr_flags & Fcntl::O_NONBLOCK
  48. rescue NameError, NotImplementedError, Errno::EINVAL
  49. # Probably on windows, where pipes are blocking by default
  50. end
  51. end
  52. end
  53. Terrapin::CommandLine::MultiPipe.prepend(Terrapin::MultiPipeExtensions)