【问题标题】:Why is IO::WaitReadable being raised differently for STDOUT than STDERR?为什么 STDOUT 的 IO::WaitReadable 与 STDERR 不同?
【发布时间】:2011-10-30 22:53:03
【问题描述】:

鉴于我希望测试长命令的非阻塞读取,我创建了以下脚本,将其保存为long,使其可以使用chmod 755 执行,并将其放置在我的路径中(另存为@987654324 @ 其中~/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 会生成每条由“long”打印的 STDERR 消息

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 的变通方法,也很感激。

【问题讨论】:

    标签: ruby io pipe popen


    【解决方案1】:

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

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

    1. 缓冲区已满(从而强制刷新)。
    2. 发送应用程序退出(到达 EOF,强制刷新)。
    3. 流被显式刷新。

    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”。

    【讨论】:

      【解决方案2】:

      这是迄今为止我最好的启动子流程。我启动了很多网络命令,所以如果它们需要很长时间才能返回,我需要一种方法来让它们超时。这在您希望控制执行路径的任何情况下都应该很方便。

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

      1. 成功完成(退出状态 0)
      2. 错误完成(退出状态非零) - 引发异常
      3. 命令超时并被终止 - 引发异常

      还修复了竞争条件,简化了参数,添加了更多 cmets,并添加了调试代码以帮助我了解退出和信号发生的情况。

      这样调用函数:

      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
      

      希望这是有用的。

      【讨论】:

        猜你喜欢
        • 2017-04-23
        • 2020-01-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-11-29
        • 2016-09-30
        • 2011-08-02
        相关资源
        最近更新 更多