From dcb6c7355effae2f8fd0b65a7b705fbdabf408b9 Mon Sep 17 00:00:00 2001 From: Mike Mangino Date: Tue, 6 Feb 2018 20:41:16 +0000 Subject: [PATCH] Don't hang on stderr This fixes an issue from back when the gem was called Cocaine, by using nonblocking IO and select on file descriptors. The contents of this commit was previously merged into the unreleased `master` branch after we'd already moved to working off `main`. https://github.com/thoughtbot/cocaine/issues/96 https://github.com/thoughtbot/cocaine/pull/97 https://github.com/thoughtbot/terrapin/pull/5 --- lib/terrapin/command_line/multi_pipe.rb | 42 ++++++++++++++++++++++--- spec/support/nonblocking_examples.rb | 13 ++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/lib/terrapin/command_line/multi_pipe.rb b/lib/terrapin/command_line/multi_pipe.rb index 0a0d4b7..4bc904b 100644 --- a/lib/terrapin/command_line/multi_pipe.rb +++ b/lib/terrapin/command_line/multi_pipe.rb @@ -29,19 +29,51 @@ def close_write end def read - @stdout_output = read_stream(@stdout_in) - @stderr_output = read_stream(@stderr_in) + read_streams(@stdout_in, @stderr_in) end def close_read - @stdout_in.close + begin + @stdout_in.close + rescue IOError + # do nothing + end + + begin @stderr_in.close + rescue IOError + # do nothing + end + end + + def read_streams(output, error) + @stdout_output = "" + @stderr_output = "" + read_fds = [output, error] + while !read_fds.empty? + to_read, = IO.select(read_fds) + if to_read.include?(output) + @stdout_output << read_stream(output) + read_fds.delete(output) if output.closed? + end + + if to_read.include?(error) + @stderr_output << read_stream(error) + read_fds.delete(error) if error.closed? + end + end end def read_stream(io) result = String.new - while partial_result = io.read(8192) - result << partial_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 end result end diff --git a/spec/support/nonblocking_examples.rb b/spec/support/nonblocking_examples.rb index 59aa2de..4d1996b 100644 --- a/spec/support/nonblocking_examples.rb +++ b/spec/support/nonblocking_examples.rb @@ -1,4 +1,17 @@ shared_examples_for "a command that does not block" do + it "does not block if the command output a lot on stderr" do + cmd = Terrapin::CommandLine.new( + "ruby", + "-e '$stdout.puts %{hello}; $stderr.puts %{goodbye}*10_000'", + :swallow_stderr => false + ) + Timeout.timeout(5) do + cmd.run + end + expect(cmd.command_output).to eq "hello\n" + expect(cmd.command_error_output).to eq "#{"goodbye" * 10_000}\n" + end + it 'does not block if the command outputs a lot of data' do garbage_file = Tempfile.new("garbage") 10.times{ garbage_file.write("A" * 1024 * 1024) }