Sandro Turriate

Coder, cook, explorer

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:

QuietFork

 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.