Fun with tmux
2 Aug 2021
I finally got around to teaching myself tmux.
I had a bunch of microservices that had to start up in a certain order, and I wanted
each microservice to be in its own tmux window, and I wanted each window to show a tail
of that microservice's logs (using -f).
First, I created some scripts that pretended they were processes that had a long boot-up time.
Here is the script that pretends it sets up a kind
cluster (which generally tends to take a while), named z_init.sh:
#!/bin/bash set -e set -o pipefail set -u echo init kind cluster sleep 5 echo init some other thing sleep 5 echo init db sleep 5 echo done initing
Here's a script that pretends to launch a microservice that can only be launched after
the init step above has happened, named z_events.sh:
#!/bin/bash set -e set -o pipefail set -u echo init events sleep 10 echo done initing
There are two more scripts, z_billing.sh, and z_admin.sh,
not shown, which are pretty much exactly the same as the above script; they pretend
to be other microservices.
Here's a script that pretends to tail the logs of a microservice after it has started up,
named z_heartbeat.sh:
#!/bin/bash
set -e
set -o pipefail
set -u
while true; do
	echo pretend I am a log file for service ${1}
	sleep 2
done
Here is a shell script named z_dev.sh that will:
- create a tmux session named mydevsession
- launch z_init.shin the first window and wait until it is done
- launch z_events.shin a new window named "events" and wait until it is running
- tail (forever) the event service's logs (by using z_heartbeat.sh eventsto pretend log tailing is happening)
- launch z_billing.shin a new window named "billing" and wait until it is running
- tail (forever) the billing service's logs (by using z_heartbeat.sh billingto pretend log tailing is happening)
- launch z_admin.shin a new window named "admin" and wait until it is running
- tail (forever) the admin service's logs (by using z_heartbeat.sh adminto pretend log tailing is happening)
- attach to the tmux session
Here is the script that does that:
#!/bin/bash
set -e
set -o pipefail
set -u
SESSION_NAME=mydevsession
tmux new-session -s ${SESSION_NAME} -d
tmux renamew 'init'
tmux send-keys -t ${SESSION_NAME} 'z_init.sh; tmux wait-for -S init-done' Enter\; wait-for init-done
tmux new-window -t ${SESSION_NAME} -n events -d
tmux select-window -t events
tmux send-keys -t ${SESSION_NAME} 'z_events.sh; tmux wait-for -S events-done' Enter\; wait-for events-done
tmux send-keys -t ${SESSION_NAME} 'z_heartbeat.sh events' Enter
tmux new-window -t ${SESSION_NAME}  -n billing -d
tmux select-window -t billing
tmux send-keys -t ${SESSION_NAME} 'z_billing.sh; tmux wait-for -S billing-done' Enter\; wait-for billing-done
tmux send-keys -t ${SESSION_NAME} 'z_heartbeat.sh billing' Enter
tmux new-window -t ${SESSION_NAME}  -n admin -d
tmux select-window -t admin
tmux send-keys -t ${SESSION_NAME} 'z_admin.sh; tmux wait-for -S admin-done' Enter\; wait-for admin-done
tmux send-keys -t ${SESSION_NAME} 'z_heartbeat.sh admin' Enter
tmux attach -t ${SESSION_NAME}
One fun thing to do is to leave out the last line of the above script, and run it in a different terminal instead, so that you can watch tmux start things up in real-time, and see that tmux really is waiting for each dependency to start before proceeding to the next.
The real secret seems to be this combo:
tmux wait-for -S [my-signal] // wait-for [my-signal]
What we're doing is firing off keystrokes (tmux send-keys) into our running
tmux session. Usually this happens right away, and tmux rushes on to the next thing.
So what we do instead is run a microservice (e.g. z_events.sh) and then follow up that command
with a tmux command that fires a named event (tmux wait-for -S events-done).
Meanwhile, in our shell script, we also have a tmux command, wait-for events-done.
What this does is block our shell script until it receives that signal.
This prevents our shell script from hurring ahead and launching later microservices before the earlier
microservices they depend on are running.