Foreman, Guard and Tmux Combo

If you ever wanted to use forman with guard but don’t want to go without interactive mode, this little helper might be useful for you.

I recently added react_on_rails to one of my rails projects. Since there’s now a second process you need to run in parallel in the background, webpack, it initially sets up a to be used with foreman.

Foreman is a nice little tool helping to start up a development environment for complex projects – with complex starting at 2 processes like here. You can read more about it here.

The initial setup adds the following

web: rails s -p 3000
client: sh -c 'rm app/assets/webpack/* || true && cd client && yarn run build:development'

However, my standard development environment invokes guard in a tmux pane, starting the rails server with the help of the guard-rails gem. Simply replacing rails s -p 3000 with guard won’t do the trick since guard has an interactive shell built in and to be honest, I don’t want to integrate the output of guard to the foreman result either, because space is limited already and adding something like 21:35:34 guard.1 | at the beginning of each line makes guard not more readable.

So I decided to start guard into another tmux pane - using foreman.

First trick is to not start guard itself but send a command to a tmux pane. You can achieve this with

$ tmux send-keys [options] 'guard' Enter

Unfortunately the tmux send-keys command will return immediately letting foreman also stop immediately, so we have to somehow wait for the guard command to terminate. We achieve this by letting tmux wait for some event and send it as soon as guard terminates.

The first working version looks like this:

guard: tmux send-keys -t 2 'guard; tmux wait-for -S tmux-guard' Enter \; wait-for tmux-guard
client: sh -c 'rm app/assets/webpack/* || true && cd client && yarn run build:development'

This will do the trick, assuming you want to run guard in tmux pane 2.

However there’s a drawback: whenever you kill foreman, guard continues to run, because foreman sends SIGTERM and SIGINT signals to the processes it started by itself, i.e. to the tmux send-keys command. So this won’t work. We have to exit guard on our own.

For that I wrote a simple little script that does exactly that and takes over the tmux part:

#!/usr/bin/env ruby
$exit_sent = false

def send_exit
  unless $exit_sent
    system("tmux send-keys #{ARGV.join(' ')} 'e' Enter");
    $exit_sent = true

Signal.trap("TERM") { send_exit }
Signal.trap("INT") { send_exit }

system("tmux send-keys #{ARGV.join(' ')} 'guard; tmux wait-for -S tmux-guard' Enter \\; wait-for tmux-guard")
puts("guard stopped")

Using this script in the helps me to start guard in a different tmux pane and terminating it gracefully whenever foreman is stopping for whatever reason.

guard: /path/to/tmux_guard_runner.rb -t 2
client: sh -c 'rm app/assets/webpack/* || true && cd client && yarn run build:development'

No Comments