Project

General

Profile

Bug #20206

Updated by lacostej (Jerome Lacoste) 4 months ago

We use PTY.spawn to call "echo foo", and on Mac it seems to randomly fail, capturing an empty output every now and then. 
 On Linux, the failure doesn't seem to happen. 

 The following code 
 1. contains 2 ways of capturing the output from PTY.spawn. Both seem to show the same issue (`run_command` and `run_command2`). `run_command2`) 
 2. invokes the external `stress` program. This helps to trigger the issue more often. 

 ``` ruby 
 require 'pty' 
 require 'expect' 

 def run_command(command) 
   output = [] 
   PTY.spawn(command) do |command_stdout, command_stdin, pid| 
     begin 
       command_stdout.each do |l| 
         line = l.chomp 
         output << line 
       end 
     rescue Errno::EIO 
       # This is expected on some linux systems, that indicates that the subcommand finished 
       # and we kept trying to read, ignore it 
     ensure 
       command_stdout.close 
       command_stdin.close 
       Process.wait(pid) 
     end 
   end 
   raise "#{$?.exited?} #{$?.stopped?} #{$?.signaled?} - #{$?.stopsig} - #{$?.termsig} -" unless $?.exitstatus == 0 
   [$?.exitstatus, output.join("\n")] 
 end 

 def run_command2(command) 
   output = [] 
   PTY.spawn(command) do |command_stdout, command_stdin, pid| 
     output = "" 
     begin 
       a = command_stdout.expect(/foo.*/, 5) 
       output = a[0] if a 
     ensure 
       command_stdout.close 
       command_stdin.close 
       Process.wait(pid) 
     end 
   end 
   raise "#{$?.exited?} #{$?.stopped?} #{$?.signaled?} - #{$?.stopsig} - #{$?.termsig} -" unless $?.exitstatus == 0 
   [$?.exitstatus, output] 
 end 

 def test_spawn(command) 
   status, output = run_command(command) 
   errors = [] 
   errors << "status was '#{status}'" unless status == 0 
   errors << "output was '#{output}'" unless output == "foo" 
   raise errors.join(" - ") unless errors.empty? 
 end 

 t = nil 
 pid = nil 
 if ENV['STRESS'] 
   t = Thread.new do |t| 
     puts "Spawning stress" 
     pid = spawn("stress -c 16 -t 99", pgroup: true) 
     puts "Waiting #{pid}" 
     Process.wait(pid) 
     puts "#{pid} DONE" 
   end 
 end 

 command = "echo foo" 

 if ARGV.count == 1 
   command = ARGV[0] 
 end 

 puts "Will run command: '#{command}'" 

 errors = 0 
 2000.times do |i| 
   begin 
     test_spawn(command) 
   rescue => e 
     puts "ERROR #{i}: #{e}" 
     errors += 1 
   end 
 end 

 if t 
   begin 
     Process.kill(:SIGKILL, -pid) 
   rescue Errno::ESRCH # already dead, ignore 
   end 
   t.join 
 end 

 raise "Failed #{errors} times" unless errors == 0 
 ``` 

 Here are some ways of reproducing the issue. issue 
 ``` 
 ruby test_pty.rb 
 STRESS=y ruby test_pty.rb 
 ``` 

 Use `stress -c 16 -t 99` in the background to trigger the issue more often. 

 Here's an example of how it fails on circleci. https://app.circleci.com/pipelines/github/lacostej/cienvs/33/workflows/d6d8e604-8a0d-4ede-8c44-d154dde93111 

 Tested on ruby 2.6 to ruby 3.3.0 on Mac.

Back