Silencing a Forked Process
How to provide the concurrency of a forked process without spamming the parent process‘ stdout.
I'm adding a feature to specjour which starts a manager in a subprocess when Bonjour fails to find any listening managers. This will enable the dream of specjour running the full suite with just one command instead of two. The first problem I ran into while working on this feature was that all of the debugging information that a manager prints, like the current test it's running, was being printed in-between the progress dots. The forked process was sharing the parent process' stdout. While this makes sense, it's undesirable.
A forked process sharing stdout
1 2 3 4 5 6 7 8 9 10 11 12
$stdout.sync = true fork do 1.upto(3) do puts "running test" sleep 1 end end 1.upto(10) do print "." sleep rand end puts
A forked manager is essentially the same as running a manager in a terminal that gets minimized or backgrounded. All the user cares about are those little green dots so when a manager is forked, all of its output must be suppressed.
My first attempt was to use IO.pipe to hand off a nice clean IO object to the forked process. This worked some of the time but larger test suites would just hang. I finally learned that that a pipe's buffer is not infinite and over time, if you don't read it, your write will just sit there blocking, waiting to be read before continuing with to write.
Overloading the buffer of a pipe
1 2 3 4 5 6 7 8
r,w = IO.pipe fork do $stdout = w puts 200 ** 28_000 # change this number to 29_000 to see it hang end Process.waitall w.close_write puts r.read
IO.pipe was the right idea but a little off. The underlying principle was to point $stdout to another IO object. Unfortunately, our pipe was space constrained. What we really need is a super sized, dumb IO object.
Thanks to Shay Arnett for reminding me of StringIO! Let's try it out.
Replacing stdout with a StringIO
1 2 3 4 5 6 7
require 'stringio' fork do $stdout = StringIO.new # comment this out to see the subprocess use the parent's stdout puts 200 ** 29_000 end Process.waitall
Excellent, it didn't hang! Now we have a silent forked process. Let's wrap it up:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
require 'stringio' module QuietFork extend self attr_reader :pid def self.fork(&block) @pid = Kernel.fork do $stdout = StringIO.new block.call end end end QuietFork.fork do puts 200 ** 29_000 end Process.waitall
Check out specjour to see how I actually use this thing.