Quality of coding life with a terminal multiplexing startup script
Saving minutes every time I (re-)boot or remote login to a machine, using terminator, screen and ssh.
For my various jobs (academic and business) I have spend a lot of time behind a computer doing code-related things. I have been in the situation many times (actually, I should say all the time, before I started doing what I will describe below) where I had many windows open; specifically a bunch of terminals, editor windows (and browser of course). Like, way too many. Different terminals had different purposes, and were grouped and named so I could find them back easily; I would be working on multiple projects simultaneously in different workspaces; a couple of terminals would be remotely logged in to other machines. So every time I had to reboot (or lost connection) I would have to go back and open all the windows, and start all the processes again, and rename all the terminals, and put them all back in the same place (because I had gotten used to everything being in a certain place).
This could take a lot of time. My guess is up to 15 minutes every time, although I never timed it. It did take waaaay too long for me before I finally automated this whole thing, but I did in the end. Perhaps some of my findings can be useful to other people who have a mild anxiety attack every time they have to reboot.
I use terminator for my terminal multiplexing nowadays. Just
apt-get install terminator
and the important thing is that you can configure various layouts, and make it so that each terminal window executes a predefined command when they start.
For the layout, start terminator, and begin creating and resizing the windows to your fancy. It’s very easy. Then save this layout by right-clicking any terminator window > Preferences > Layouts > Add, then give it a name and hit save.
Open the config file in an editor (should be in ~/.config/terminator/config) and then then manually set all of the following for each terminal
directory = /home/me/projects/someproject
# which directory to start in, obviously
profile = default
# the visual profile: colors, fonts, etc. You can create these from the terminator settings.
title = jupyter notebook # the name of this terminal window
# here is the important bit
command = '''bash --rcfile <(cat ${HOME}/.bashrc; echo 'export PROMPT_COMMAND="source activate my_env; jupyter notebook; unset PROMPT_COMMAND"') -i'''
This layout has a name (you set it before in the preferences window), but you can edit the layout name in the config file as well. If you edit the layout named ‘default’, this will be the layout that is loaded by default when you start terminator without any options (what’s in a name, huh?). Let’s call it mylayout for now. So after (re)booting my system, I just do:
terminator --layout=mylayout
and then, for example, automatically I will get my jupyter notebook server in the correct virtualenv, pycharm, a window running htop, a continuous rsync process for pushing code to a remote, and whatever else have you. My terminator after start-up
I’ll explain a little about the command, because it is a bit fiddly. First of all, terminator can only execute a single command, so we have to hack everything we want to happen into one command: bash
. Props to this stackoverflow answer. The explanation below is mostly a copy-paste from that SO answer, but I’ll add some notes of my own. Here is the command again:
command = '''bash --rcfile <(cat ${HOME}/.bashrc; echo 'export PROMPT_COMMAND="source activate my_env; jupyter notebook; unset PROMPT_COMMAND"') -i'''
- We execute bash in interactive (
-i
) mode. So the bash session stays open at the end. - We execute commands from a custom command file (
--rcfile
) instead of .bashrc. - This file is created with the contents of .bashrc plus one more command.
- This extra command exports
PROMPT_COMMAND
with a value of “whatever we want to execute”. - The
PROMPT_COMMAND
is a bash built-in, and gets executed at every new prompt line. - Therefore, the
PROMPT_COMMAND
must be unset just after it is executed the first time to avoid multiple executions after each interaction with the shell. - Within the
PROMPT_COMMAND
I can mostly chain together all the commands that I want. So in this example, I activate my virtual environment, and start my jupyter notebook server inside it.
Another example; starting some application but having it run in the background, so we can use the terminal windows for something else afterwards:
command = '''bash --rcfile <(cat ${HOME}/.bashrc; echo 'export PROMPT_COMMAND="unset PROMPT_COMMAND; pycharm &"') -i'''
To run in the background, you usually add the &
at the end of the command, but apparently this doesn’t work if you want to chain another command after it using a semicolon. Luckily, we can switch the commands around so the &
is at the end, and the unsetting of the PROMPT_COMMAND
doesn’t interfere with the execution of itself.
Another very related issue is doing things on remote machines over ssh. We have the same problems as above, having to re-setup the ssh connections every time we reboot our own system. But there are two more liabilities when working over ssh (seasoned ssh users will know the solutions to this already, of course): if you lose connection to a process on a remote machine, the process will exit, and if the remote machine reboots, you obviously also have to restart all the stuff that you were running there. My most convoluted terminator startup command solves all three of these at the same time.
For connection loss / client machine reboot, we are going to use good old screen. The essential primer is this: type screen, hit enter, and you just started a new terminal session, but it is sort of like a background process. You are currently connected to it, and it is practically indistinguishable from when you normally work in a terminal. However, if you close the window, or press CTRL-A CTRL-D, this sessions get detached. This means it is still running, it’s just no longer attached to a user I/O. To reconnect simply do screen -r
. Screen can do a LOT more that I won’t go into right now.
For convenience, instead of writing out the entire command into the PROMPT_COMMAND
, I make an alias out of it. So
# in my terminator config file
command = '''bash --rcfile <(cat ${HOME}/.bashrc; echo 'export PROMPT_COMMAND="unset PROMPT_COMMAND; remote_tensorboard &"') -i'''
# and in my ~/.bash_aliases:
remote_tensorboard () {
SCRIPT='bash --rcfile <(cat ${HOME}/.bashrc; echo "source activate tf_env; cd project/output; tensorboard --logdir=runs")'
ssh -t myremote "screen -RR -q -S tensorboard bash -c '${SCRIPT}'"
}
This command is going to:
- ssh into my remote machine (
myremote
is the ssh alias defined in my~/.ssh/config
, defining username, ssh-key location, port forwarding, etc) - execute a command on this remote machine (the
-t
option) - this screen will get a name (
-S
tensorboard in this example) and will not show the standard screen welcome screen (using-q
) - if there is already a screen session with the same name, we will attach to that screen session instead without running the bash command (
-RR
) - run a ‘single’ bash command inside this screen session using
bash -c
(the-c
tells bash to execute a command from a string) - if inside this bash command we want something complex, we use the same
--rcfile
trick as before. In this case, we start tensorboard from a virtual environment and from a specific directory.
Honest disclaimer: I always have to muck about with the quotation marks and string escapes and such in bash, so there might be a better way to compose all this, but at least it works!
Having set all this stuff up has made me inappropriately happy, and has probably saved me between 5 and 15 minutes (depending on the project) every time when I reboot/reconnect, not to mention the annoyances!
Of course I’m on a linux system here. I expect macOS probably has something very similar. I’m using terminator and screen, but tmux I think has combined functionality, and byobu as well I believe. Haven’t tried them yet, but they look nice and I might try them in the future.