2013-06-21 103 views
12

这是最简单的代码来解释:在popen中超时工作,但在超时内popen不?

require 'timeout' 

puts "this block will properly kill the sleep after a second" 

IO.popen("sleep 60") do |io| 
    begin 
    Timeout.timeout(1) do 
     while (line=io.gets) do 
     output += line 
     end 
    end 
    rescue Timeout::Error => ex 
    Process.kill 9, io.pid 
    puts "timed out: this block worked correctly" 
    end 
end 

puts "but this one blocks for >1 minute" 

begin 
    pid = 0 
    Timeout.timeout(1) do 
    IO.popen("sleep 60") do |io| 
     pid = io.pid 
     while (line=io.gets) do 
     output += line 
     end 
    end 
    end 
rescue Timeout::Error => ex 
    puts "timed out: the exception gets thrown, but much too late" 
end 

我的两个块的心智模式是相同的:

flow chart

所以,我缺少什么?

编辑:drmaciver在twitter上建议,在第一种情况下,由于某种原因,管道套接字进入非阻塞模式,但在第二种情况下不会。我想不出为什么会发生这种情况,我也不知道如何获得描述符的标志,但至少是一个合理的答案?在这个可能性上工作。

+0

你跑哪个红宝石? –

+0

至少在1.8.7和1.9.3上发生此行为。 jruby块在这两个块上都是60,这是我先前猜测的行为。 – llimllib

+0

请注意,您为我的两个块之间的'puts(“但是这个......”)'等待第一个'sleep'完成,因为第一个IO#popen块正在对'waitpid() '。如果你不想这样做,那么你的救援逻辑就需要杀死子进程。 – pilcrow

回答

13

啊哈,含蓄。

在第二种情况下,存在隐藏的阻塞ensure子句,该子句位于IO#popen块的末尾。超时::错误提出的适时提出的,但您不能rescue它直到从隐含的ensure子句执行返回。

Under the hoodIO.popen(cmd) { |io| ... }做这样的事情:

def my_illustrative_io_popen(cmd, &block) 
    begin 
    pio = IO.popen(cmd) 
    block.call(pio)  # This *is* interrupted... 
    ensure 
    pio.close   # ...but then control goes here, which blocks on cmd's termination 
    end 

和IO#千钧一发真的是更多或更少pclose(3),它阻止你waitpid(2)直到熟睡的孩子退出。

您可以验证这一点,像这样:

#!/usr/bin/env ruby 

require 'timeout' 

BEGIN { $BASETIME = Time.now.to_i } 

def xputs(msg) 
    puts "%4.2f: %s" % [(Time.now.to_f - $BASETIME), msg] 
end 

begin 
    Timeout.timeout(3) do 
    begin 
     xputs "popen(sleep 10)" 
     pio = IO.popen("sleep 10") 
     sleep 100      # or loop over pio.gets or whatever 
    ensure 
     xputs "Entering ensure block" 
     #Process.kill 9, pio.pid  # <--- This would solve your problem! 
     pio.close 
     xputs "Leaving ensure block" 
    end 
    end 
rescue Timeout::Error => ex 
    xputs "rescuing: #{ex}" 
end 

那么,你能做些什么?

您必须以明确的方式来做,因为解释器不会公开重写IO#popen ensure逻辑的方法。例如,您可以使用上面的代码作为起始模板,并取消对kill()行的注释。

+0

我在io.c上盯着这么久,看上去只有几行*保证,没有看到保证或考虑它。很好的回答,非常感谢。 – llimllib

+0

可以通过此解决方案获取退出状态吗? – tiktak

0

在第一个块中,超时在子级中引发,将其杀死并将控制权返回给父级。在第二个块中,超时在父级中引发。孩子从来没有得到信号。

io.chttps://github.com/ruby/ruby/blob/trunk/io.c#L6021timeout.rbhttps://github.com/ruby/ruby/blob/trunk/lib/timeout.rb#L51

+0

我知道这不是一个详细的答案比尔,但这是我阅读块的方式。 –

+1

传递给IO#popen的块在父进程的上下文中执行。当你说孩子的过程可能或不能“获得”信号时,我不确定你的意思。 – pilcrow

+0

@JonathanJulian我已经在分屏中打开了这两个文件,试图找出它。据我所知,超时是从两个例子中的主线程产生的。这里是popen运行它传递的块的地方:https://github.com/ruby/ruby/blob/trunk/io.c#L6075 – llimllib