How Foreman Works?
27 Aug, 2022
Foreman, introduced in 2011 is a simple Procfile-based process manager written in Ruby.
With a Procfile like this:
# Procfile
web_a: bundle exec ruby app_a.rb
web_b: bundle exec ruby app_b.rb
service_a: go run service_a.go
And a single command: foreman start, you can run multiple background processes (3 in this case) with a foreground monitoring process (the one attached to the terminal) that multiplexes the outputs of those processes (with color-coded) into the STDOUT and monitoring for signals:

And when we finished, we can send SIGINT, SIGTERM, or SIGHUP to the foreground process, and it will send SIGTERM to terminate all processes gracefully (provided that those processes also trap SIGTERM and terminate themselves gracefully):

Wonderful.
Alas, there is little documentation of how it actually works. Are you curious of how a simple program with core logic with <300 LOC is capable of handling multiple background processes for you?
So, here we go.
A SIDE NOTE
Nowadays, new folks don't hear much about Process Management, mainly because I think containers are already a norm. Nevertheless, I still think this is a fun exploration.
-
TL;DR
If you are not insterested on how the code works internally, just skip to the end where I present a diagram.
The Core Logic
After running foreman start, Foreman will attempt to read Procfile and an optional .env file (for setting environment variables in all processes), parse processes in Procfile, assign them into @processes variable, and then start the core logic.
The core logic is simple, because Foreman is written eloquently, we can easily follow its main logic in engine.rb:
# Start the processes registered to this +Engine+
#
def start
register_signal_handlers
startup
spawn_processes
watch_for_output
sleep 0.1
wait_for_shutdown_or_child_termination
shutdown
exit(@exitstatus) if @exitstatus
end
register_signal_handlerstraps signals:TERM, INT, HUPfor later all processes termination and trapsUSR1, USR2for forwarding controlling signals to all background processes. All other signals are discarded.startupassigns names and colors for outputs of the background processesspawn_processesspawns the processes, for how it does we can just look at the code:
def spawn_processes
@processes.each do |process|
1.upto(formation[@names[process]]) do |n|
reader, writer = IO.pipe
begin
pid = process.run(:output => writer, :env => {
"PORT" => port_for(process, n).to_s,
"PS" => name_for_index(process, n)
})
writer.puts "started with pid #{pid}"
rescue Errno::ENOENT
writer.puts "unknown command: #{process.command}"
end
@running[pid] = [process, n]
@readers[pid] = reader
end
end
end
(For formation, it is just a way to tell how many instances for each defined process type to run).
Basically it uses Foreman::Process#run(), which in turns use the standard lib's ::Process::spawn, which in its most basic usage, given the environment variables and a command, it will start a background child process that has its output reassigned to the writer end of a pipe which connects to its reader end, its process group set to the main Foreman foreground process (which is also its parent process), and run that given command under specified environment variables.
Any output that the spawned background child process normally outputs to the STDOUT will go to its writer instead. And can be read by reading through the reader pipe.
Continue to the rest of the core logic:
watch_for_outputsForeman will start a thread that continuously reads@readerspipes for outputs and a self-pipe for signals received on the main thread (more on this later). If any of@readersor the self-pipe has a value, it will output that value intoSTDOUTwith its assigned process name, color, and also will timestamp the output, or will handle signals, respectively.wait_for_shutdown_or_child_termination, here is where its child processes monitoring loop happens:
def wait_for_shutdown_or_child_termination
loop do
# Stop if it is time to shut down (asked via a signal)
break if @shutdown
# Stop if any of the children died
break if check_for_termination
# Sleep for a moment and do not blow up if any signals are coming our way
begin
sleep(1)
rescue Exception
# noop
end
end
# Ok, we have exited from the main loop, time to shut down gracefully
terminate_gracefully
end
Any of INT, TERM, HUP signals received, will make @shutdown = true and thus will break the loop. Or if any child process has terminated (checked via check_for_termination -- which calls ::Process::wait2), it will also break the loop.
Breaking the loop will make the main thread calls terminate_gracefully, which sends TERM signal to all child processes. Then if all the background child processes haven't terminated yet and a timeout (default is 5 seconds) happened, it will just send KILL signal to kill all children immediately.
Then Foreman will shutdown.
shutdowncurrently does nothing.exitexits with exit status, possibly set to those of its children. The end.
Signal handling and the Self-Pipe Trick
Handling signals is a pain (both in UNIX and Windows). Any signal can be trapped and handled with our own custom logic, but while that signal is being handled, a new signal can be sent and the signal handling will be interrupted, possibly again and again.
The UNIX self-pipe trick is a hack that can be roughly explained like so:
- The main thread creates a pipe, a unidirectional data channel that usually is created for interprocess communication, but in this case will be used for its own internal process communication, thus called a self-pipe, which has
readerandwriterends - The main thread spawns a monitoring thread that monitors the
readerend of the self-pipe - The main thread creates a single signal queue. All interested signals will be trapped and put into this single queue, and notify the monitoring thread by writing a byte to the
writerend of the self-pipe - The monitoring thread will get notified via the
readerend (perhaps with select(2)), and will process signals in the queue with the custom logic, then returns to its normal job, notify main thread to do something (maybe by setting a specific variable), or shutdown
If this still sounds confused, perhaps this article may elaborate better.
Diagram of How Foreman Works
Here we conclude how Foreman works with a diagram.

If you find any mistakes, feel free to notify me.
Till next time!