about summary refs log tree commit diff
path: root/lib/terrapin
diff options
context:
space:
mode:
authorClaire <claire.github-309c@sitedethib.com>2021-05-06 14:22:54 +0200
committerGitHub <noreply@github.com>2021-05-06 14:22:54 +0200
commit566fc909134586d1746ad60ee455832dec6bc61a (patch)
tree26c8f77002555a8e7277d6ab9b2f4241b3fdbc38 /lib/terrapin
parent0a3fa034fc66246dbf9dfb4627a983e0903042d4 (diff)
Add Ruby 3.0 support (#16046)
* Fix issues with POSIX::Spawn, Terrapin and Ruby 3.0

Also improve the Terrapin monkey-patch for the stderr/stdout issue.

* Fix keyword argument handling throughout the codebase

* Monkey-patch Paperclip to fix keyword arguments handling in validators

* Change validation_extensions to please CodeClimate

* Bump microformats from 4.2.1 to 4.3.1

* Allow Ruby 3.0

* Add Ruby 3.0 test target to CircleCI

* Add test for admin dashboard warnings

* Fix admin dashboard warnings on Ruby 3.0
Diffstat (limited to 'lib/terrapin')
-rw-r--r--lib/terrapin/multi_pipe_extensions.rb87
1 files changed, 45 insertions, 42 deletions
diff --git a/lib/terrapin/multi_pipe_extensions.rb b/lib/terrapin/multi_pipe_extensions.rb
index 51d7de37c..209f4ad6c 100644
--- a/lib/terrapin/multi_pipe_extensions.rb
+++ b/lib/terrapin/multi_pipe_extensions.rb
@@ -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