Timeout within a popen works, but popen inside a timeout doesn't?
Aha, subtle.
There is a hidden, blocking ensure
clause at the end of the IO#popen block in the second case. The Timeout::Error is raised raised timely, but you cannot rescue
it until execution returns from that implicit ensure
clause.
Under the hood, IO.popen(cmd) { |io| ... }
does something like this:
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
and the IO#close call is really more-or-less a pclose(3)
, which is blocking you in waitpid(2)
until the sleeping child exits.
You can verify this like so:
#!/usr/bin/env rubyrequire 'timeout'BEGIN { $BASETIME = Time.now.to_i }def xputs(msg) puts "%4.2f: %s" % [(Time.now.to_f - $BASETIME), msg]endbegin 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 endrescue Timeout::Error => ex xputs "rescuing: #{ex}"end
So, what can you do?
You'll have to do it the explicit way, since the interpreter doesn't expose a way to override the IO#popen ensure
logic. You can use the above code as a starting template and uncomment the kill()
line, for example.
In the first block, the timeout is raised in the child, killing it and returning control to the parent. In the second block, the timeout is raised in the parent. The child never gets the signal.
See io.c
https://github.com/ruby/ruby/blob/trunk/io.c#L6021and timeout.rb
https://github.com/ruby/ruby/blob/trunk/lib/timeout.rb#L51