Created
May 31, 2011 22:35
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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