diff --git a/lib/subprocess.rb b/lib/subprocess.rb index fbf274c..aca7f05 100644 --- a/lib/subprocess.rb +++ b/lib/subprocess.rb @@ -436,13 +436,15 @@ def communicate(input=nil, timeout_s=nil) # the input depending on how many bytes were written input = input.dup.force_encoding('BINARY') unless input.nil? - @stdin.close if (input.nil? || input.empty?) && !@stdin.nil? + # Close stdin immediately if input is nil or empty + @stdin.close if @stdin && (input.nil? || input.empty?) timeout_at = Time.now + timeout_s if timeout_s self.class.catching_sigchld(pid) do |global_read, self_read| wait_r = [@stdout, @stderr, self_read, global_read].compact - wait_w = [input && @stdin].compact + wait_w = @stdin&.closed? ? [] : [input && @stdin].compact + done = false while !done # If the process has exited, we want to drain any remaining output before returning diff --git a/test/test_empty_stdin.rb b/test/test_empty_stdin.rb new file mode 100644 index 0000000..b555c8d --- /dev/null +++ b/test/test_empty_stdin.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true +require 'rubygems' +gem 'minitest' +require 'minitest/autorun' +require 'subprocess' + +describe Subprocess do + describe "communicate with empty string input" do + # Bug report: subprocess.communicate("") doesn't properly handle stdin, + # causing it to close incorrectly and result in a broken pipe. + it "should not raise IOError when passing empty string" do + # Before the fix, this would raise: IOError: closed stream + Subprocess.check_call(['cat'], + stdin: Subprocess::PIPE, + stdout: Subprocess::PIPE) do |p| + stdout, stderr = p.communicate("") + assert_equal("", stdout, "Empty input should produce empty output") + assert_equal("", stderr, "No errors expected") + end + end + + it "should work correctly with non-empty string input" do + test_input = "hello world" + Subprocess.check_call(['cat'], + stdin: Subprocess::PIPE, + stdout: Subprocess::PIPE) do |p| + stdout, stderr = p.communicate(test_input) + assert_equal(test_input, stdout, "Input should be echoed back") + assert_equal("", stderr, "No errors expected") + end + end + + it "should work correctly with nil input" do + Subprocess.check_call(['cat'], + stdin: Subprocess::PIPE, + stdout: Subprocess::PIPE) do |p| + stdout, stderr = p.communicate(nil) + assert_equal("", stdout, "Nil input should produce empty output") + assert_equal("", stderr, "No errors expected") + end + end + + it "should handle already closed stdin gracefully" do + # Edge case: what if stdin is already closed? + p = Subprocess.popen(['cat'], stdin: Subprocess::PIPE, stdout: Subprocess::PIPE) + p.stdin.close + stdout, stderr = p.communicate("") + assert_equal("", stdout) + assert_equal("", stderr) + p.wait + end + end +end