Skip to content

Instantly share code, notes, and snippets.

@tkareine
Created May 31, 2011 22:35
Show Gist options
  • Save tkareine/1001431 to your computer and use it in GitHub Desktop.
Save tkareine/1001431 to your computer and use it in GitHub Desktop.
Capture input and output and get the exit status of arbitrary Ruby code, variant of Open4::popen4
# coding: utf-8
module PFork
extend self
# Call function +fun+ in a child process, giving access to the child process
# id and capturing STDIN, STDOUT, and STDERR of the function call.
#
# Adapted from stdlib's Open3::popen3 and
# {Open4::popen4}[https://github.com/ahoward/open4] by Ara T. Howard, but
# without using double forking and fork-exec'ing.
def pfork(fun, &block)
pw, pr, pe, ps = IO.pipe, IO.pipe, IO.pipe, IO.pipe
verbose = $VERBOSE
begin
$VERBOSE = nil
cid = fork do
pw.last.close
STDIN.reopen pw.first
pw.first.close
pr.first.close
STDOUT.reopen pr.last
pr.last.close
pe.first.close
STDERR.reopen pe.last
pe.last.close
ps.first.close
STDOUT.sync = STDERR.sync = true
begin
fun.call
exit_status = 0
rescue SystemExit => e
# Make it seem to the caller that calling Kernel#exit in +fun+ kills
# the child process normally. Kernel#exit! bypasses this rescue
# block.
exit_status = e.status
rescue Exception => e
Marshal.dump e, ps.last
ps.last.flush
exit_status = 42 # won't be seen by the caller
ensure
ps.last.close unless ps.last.closed?
exit! exit_status
end
end
ensure
$VERBOSE = verbose
end
[ pw.first, pr.last, pe.last, ps.last ].each { |fd| fd.close }
pw.last.sync = true
pi = [ pw.last, pr.first, pe.first ]
begin
block.call(cid, *pi) if block_given?
ensure
pi.each { |fd| fd.close unless fd.closed? }
end
begin
e = Marshal.load ps.first
raise Exception === e ? e : "unknown failure!"
rescue EOFError
# Calling +fun+ did not raise exception.
ensure
ps.first.close
exit_status = Process.waitpid2(cid).last
end
exit_status
end
end
if $0 == __FILE__
require 'minitest/autorun'
class PForkTest < MiniTest::Unit::TestCase
include PFork
def test_fun_successful_return
fun = lambda { 'lucky me' }
status = pfork fun
assert_equal 0, status.exitstatus
end
def test_fun_raise_exception
msg = 'oh noes'
fun = lambda { raise msg }
err = assert_raises(RuntimeError) { pfork fun }
assert_equal msg, err.to_s
end
def test_fun_force_exit
exit_code = 43
fun = lambda { exit! exit_code }
status = pfork fun
assert_equal exit_code, status.exitstatus
end
def test_fun_normal_exit
exit_code = 43
fun = lambda { exit exit_code }
status = pfork fun
assert_equal exit_code, status.exitstatus
end
def test_io
via_msg = 'foo'
err_msg = 'bar'
fun = lambda do
$stdout.write $stdin.read
$stderr.write err_msg
end
out_actual, err_actual = nil, nil
status = pfork fun do |_, stdin, stdout, stderr|
stdin.write via_msg
stdin.close
out_actual = stdout.read
err_actual = stderr.read
end
assert_equal via_msg, out_actual
assert_equal err_msg, err_actual
assert_equal 0, status.exitstatus
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment