为什么对于STDOUT而言,IO :: WaitReadable的提升方式与STDERR不同?

时间:2011-08-19 21:25:12

标签: ruby io pipe popen

鉴于我希望测试一个长命令的非阻塞读取,我创建了以下脚本,将其保存为long,使其可以chmod 755执行,并将其放在我的路径中(保存为~/bin/long ~/bin在我的路径中。)

我在使用RVM默认值编译的ruby 1.9.2p290 (2011-07-09 revision 32553) [x86_64-darwin11.0.0]的* nix变体上。我不使用Windows,因此如果你这样做,我不确定测试脚本是否适合你。

#!/usr/bin/env ruby

3.times do
  STDOUT.puts 'message on stdout'
  STDERR.puts 'message on stderr'
  sleep 1
end

为什么long_err生成每条STDERR消息,因为它是由“long”

打印的
def long_err( bash_cmd = 'long', maxlen = 4096)
  stdin, stdout, stderr = Open3.popen3(bash_cmd)
  begin
    begin
      puts 'err -> ' + stderr.read_nonblock(maxlen)
    end while true
  rescue IO::WaitReadable
    IO.select([stderr])
    retry
  rescue EOFError
    puts 'EOF'
  end
end

虽然long_out仍然被阻止,直到打印完所有STDOUT消息?

def long_out( bash_cmd = 'long', maxlen = 4096)
  stdin, stdout, stderr = Open3.popen3(bash_cmd)
  begin
    begin
      puts 'out -> ' + stdout.read_nonblock(maxlen)
    end while true
  rescue IO::WaitReadable
    IO.select([stdout])
    retry
  rescue EOFError
    puts 'EOF'
  end
end

我认为在测试任一功能之前你会require 'open3'

为什么IO::WaitReadable对STDOUT的提升方式与STDERR不同?

使用other ways to start subprocesses的变通方法,如果您拥有它们,也会感激不尽。

2 个答案:

答案 0 :(得分:4)

在大多数操作系统中,STDOUT 缓冲,而STDERR则不是。 popen3所做的基本上是在可执行的启动和Ruby之间打开一个管道。

任何处于缓冲模式的输出都不会通过此管道发送,直到:

  1. 填充缓冲区(从而强制冲洗)。
  2. 发送应用程序退出(达到EOF,强制刷新)。
  3. 明确刷新流。
  4. STDERR没有被缓冲的原因是,通常认为错误消息立即出现很重要,而不是通过缓冲来提高效率。

    因此,知道这一点,您可以使用STDOUT模拟STDERR行为,如下所示:

    #!/usr/bin/env ruby
    
    3.times do
      STDOUT.puts 'message on stdout'
      STDOUT.flush 
      STDERR.puts 'message on stderr'
      sleep 1
    end
    

    你会发现差异。

    您可能还想查看“Understanding Ruby and OS I/O buffering”。

答案 1 :(得分:0)

这是我到目前为止开始子进程的最好成绩。我发布了很多网络命令,所以如果它们需要很长时间才能返回,我需要一种方法来计算它们。在您希望保持对执行路径的控制的任何情况下,这都应该很方便。

我从Gist改编了这个,添加代码来测试3个结果的命令的退出状态:

  1. 成功完成(退出状态0)
  2. 错误完成(退出状态为非零) - 引发异常
  3. 命令超时并被杀死 - 引发异常
  4. 还修复了竞争条件,简化参数,添加了一些注释,并添加了调试代码,以帮助我了解出口和信号发生的情况。

    像这样调用函数:

    output = run_with_timeout("command that might time out", 15)
    
    如果命令成功完成,

    输出将包含命令的组合STDOUT和STDERR。如果命令在15秒内没有完成,它将被终止并引发异常。

    这是函数(你需要在顶部定义2个常量):

    DEBUG = false        # change to true for some debugging info
    BUFFER_SIZE = 4096   # in bytes, this should be fine for many applications
    
    def run_with_timeout(command, timeout)
      output = ''
      tick = 1
      begin
        # Start task in another thread, which spawns a process
        stdin, stderrout, thread = Open3.popen2e(command)
        # Get the pid of the spawned process
        pid = thread[:pid]
        start = Time.now
    
        while (Time.now - start) < timeout and thread.alive?
          # Wait up to `tick' seconds for output/error data
          Kernel.select([stderrout], nil, nil, tick)
          # Try to read the data
          begin
            output << stderrout.read_nonblock(BUFFER_SIZE)
            puts "we read some data..." if DEBUG
          rescue IO::WaitReadable
            # No data was ready to be read during the `tick' which is fine
            print "."       # give feedback each tick that we're waiting
          rescue EOFError
            # Command has completed, not really an error...
            puts "got EOF." if DEBUG
            # Wait briefly for the thread to exit...
            # We don't want to kill the process if it's about to exit on its
            # own. We decide success or failure based on whether the process
            # completes successfully.
            sleep 1
            break
          end
        end
    
        if thread.alive?
          # The timeout has been reached and the process is still running so
          # we need to kill the process, because killing the thread leaves
          # the process alive but detached.
          Process.kill("TERM", pid)
        end
    
      ensure
        stdin.close if stdin
        stderrout.close if stderrout
      end
    
      status = thread.value         # returns Process::Status when process ends
    
      if DEBUG
        puts "thread.alive?: #{thread.alive?}"
        puts "status: #{status}"
        puts "status.class: #{status.class}"
        puts "status.exited?: #{status.exited?}"
        puts "status.exitstatus: #{status.exitstatus}"
        puts "status.signaled?: #{status.signaled?}"
        puts "status.termsig: #{status.termsig}"
        puts "status.stopsig: #{status.stopsig}"
        puts "status.stopped?: #{status.stopped?}"
        puts "status.success?: #{status.success?}"
      end
    
      # See how process ended: .success? => true, false or nil if exited? !true
      if status.success? == true       # process exited (0)
        return output
      elsif status.success? == false   # process exited (non-zero)
        raise "command `#{command}' returned non-zero exit status (#{status.exitstatus}), see below output\n#{output}"
      elsif status.signaled?           # we killed the process (timeout reached)
        raise "shell command `#{command}' timed out and was killed (timeout = #{timeout}s): #{status}"
      else
        raise "process didn't exit and wasn't signaled. We shouldn't get to here."
      end
    end
    

    希望这很有用。