require 'r10k/util/subprocess/runner' require 'r10k/util/subprocess/runner/pump' require 'fcntl' # Implement a POSIX command runner by using fork/exec. # # This implementation is optimized to run commands in the background, and has # a few noteworthy implementation details. # # First off, when the child process is forked, it calls setsid() to detach from # the controlling TTY. This has two main ramifications: sending signals will # never be send to the forked process, and the forked process does not have # access to stdin. # # @api private class R10K::Util::Subprocess::Runner::POSIX < R10K::Util::Subprocess::Runner def initialize(argv) @argv = argv mkpipes end def run # Create a pipe so that the parent can verify that the child process # successfully executed. The pipe will be closed on a successful exec(), # and will contain an error message on failure. exec_r, exec_w = pipe @stdout_pump = R10K::Util::Subprocess::Runner::Pump.new(@stdout_r) @stderr_pump = R10K::Util::Subprocess::Runner::Pump.new(@stderr_r) pid = fork do exec_r.close execute_child(exec_w) end exec_w.close @stdout_pump.start @stderr_pump.start execute_parent(exec_r, pid) @result end private def execute_child(exec_w) if @cwd Dir.chdir @cwd end # Create a new session for the forked child. This prevents children from # ever being the foreground process on a TTY, which is almost always what # we want in r10k. Process.setsid # Reopen file descriptors STDOUT.reopen(@stdout_w) STDERR.reopen(@stderr_w) executable = @argv.shift exec([executable, executable], *@argv) rescue SystemCallError => e exec_w.write("#{e.class}: #{e.message}") exit(254) end def execute_parent(exec_r, pid) @stdout_w.close @stderr_w.close stdout = '' stderr = '' if !exec_r.eof? stderr = exec_r.read || "exec() failed" _, @status = Process.waitpid2(pid) else _, @status = Process.waitpid2(pid) @stdout_pump.wait @stderr_pump.wait stdout = @stdout_pump.string stderr = @stderr_pump.string end exec_r.close @stdout_r.close @stderr_r.close @result = R10K::Util::Subprocess::Result.new(@argv, stdout, stderr, @status.exitstatus) end def mkpipes @stdout_r, @stdout_w = pipe @stderr_r, @stderr_w = pipe end def pipe ::IO.pipe.tap do |pair| pair.each { |p| p.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) } end end end