Тайм-аут в папке работает, но всплывать внутри таймаута нет?

Проще всего объяснить в коде:

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

Итак, что мне не хватает?

edit: drmaciver предложил в twitter, что в первом случае, по какой-то причине, сокет трубки переходит в неблокирующий режим, но во втором - нет. Я не могу придумать никаких причин, почему это произойдет, и я не могу понять, как получить флаги дескриптора, но это, по крайней мере, правдоподобный ответ? Работая над этой возможностью.

Ответ 1

Ага, тонкая.

Существует скрытое, блокирующее предложение ensure в конце всплывающего блока ввода-вывода во втором случае. Ошибка Timeout:: Error поднимается своевременно, но вы не можете rescue до тех пор, пока выполнение не вернется из этого неявного предложения ensure.

Под капотом, IO.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 termination
  end

а вызов IO # close действительно больше или меньше a 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(), например.