ENOSUCHBLOG

Programming, philosophy, pedaling.


Abusing Signals for Shell Synchronization

Jul 14, 2015     Tags: programming, workflow    

This post is at least a year old.

As I mentioned in my very first post, I do a lot of configuration. In particular, I have quite a few bash functions, aliases, and environment settings that I update regularly to reflect my needs.

To synchronize my configurations across all of my computers, I use a git repository and a long and not very pretty bash script that essentially maintains clones of the dotfiles repo and copies files and directories from it depending on the needs of the host system.

As a result, updating a given host’s configuration only requires a single command:

getconfigs

Unfortunately, this only performs a file-level synchronization of configurations. This is perfectly fine for programs that are run frequently (like git and feh) or that poll for their configuration files while running, but it presents a problem for bash itself, which normally only reads ~/.bashrc (and/or ~/.bash_profile) once per session.

Until now, my solution was a shell alias I named bashreload:

1
alias bashreload='unalias -a ; source ~/.bashrc'

This works, but only for a single shell at a time. In other words, reloading my bash configuration across my (very numerous) terminal sessions would require me to go to each and manually type bashreload, wasting time and effort to do something that should be instantaneous and uninvolved.

a few bash processes

just a few bash processes.

To solve this, I began to experiment with good old Unix signals, which are actually very easy to manage with bash’s trap and kill builtins:

1
2
3
4
5
6
7
8
function handler()
{
   echo "received signal"
}

trap handler SIGUSR1

kill -USR1 $$ # -> "received signal"

Knowing this behavior, I whipped up an initial solution in my ~/.bashrc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
alias getconfigs='dotfiles ; allreload' # dotfiles is the syncing script

trap bashreload SIGUSR1

# allreload - send SIGUSR1 to every bash process, which is trapped to bashreload
function allreload()
{
	pids=$(pidof bash)

	[[ -n "${pids}" ]] && kill -SIGUSR1 ${pids}
}

# bashreload - wipe aliases and re-source from ~/.bashrc
function bashreload()
{
	unalias -a
	source ~/.bashrc
}

However, as I soon learned, this has a very bad side effect. Because the allreload function collects the PIDs of all bash processes, including non-interactive ones, it ends up sending SIGUSR1 to non-interactive bash instances. What is bash’s default SIGUSR1 behavior, I hear you ask?

When untrapped, a bash process that receives SIGUSR1 is immediately killed.

As a result I ended up killing several non-interactive scripts, one of which was apparently vital to Compiz and whose death caused all windows on my left monitor to stop responding.

This presents a major problem: How can interactive and non-interactive bash processes be distinguished externally, with no access to the normal $PS1 or $- variables?

Ultimately, as far as I can tell, the above is simply not possible. We could produce a similar effect with an IPC system other than signals, but the results would almost certainly be ugly, difficult to maintain, and error prone.

I was about to give up and revert to my original system of running bashreload on demand when IRC saved me once again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<yossarian>	looks like this idea is going down the toilet
<yossarian>	unless i do IPC without signals
<ham-peas>	well
<ham-peas>	i suppose you could have all your login shells start listening to a file or fifo somewhere at init time, and reinit themselves when they see something written to it
<ham-peas>	but that would be insane and disgusting
<yossarian>	yeah, not doing that
<yossarian>	if only the default behavior wasn't kill
<yossarian>	i could override another signal maybe
...
<ham-peas>	the only one i see in signal(7) that a) has default action ignore, and b) isn't already used by bash, is SIGURG
<yossarian>	hmm
<ham-peas>	which i've never actually heard of before, for whatever that's worth
<ham-peas>	and 'kill -URG $$' doesn't seem to do my shell any harm
<ham-peas>	so there's that
<yossarian>	hooray
<yossarian>	let's see if it works

SIGURG (which is defined in POSIX.1-2001 and is normally used to signal out-of-band data on a TCP/IP socket), has a default action of “Ignore” according to signal(7), an action that bash appears to respect. This is confirmed by its absence from terminating_signals[] in the bash source tree.

Therefore, a quick modification of three lines:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
alias getconfigs='dotfiles ; allreload' # dotfiles is the syncing script

trap bashreload SIGURG

# allreload - send SIGURG to every bash process, which is trapped to bashreload
function allreload()
{
	pids=$(pidof bash)

	[[ -n "${pids}" ]] && kill -SIGURG ${pids}
}

# bashreload - wipe aliases and re-source from ~/.bashrc
function bashreload()
{
	unalias -a
	source ~/.bashrc
}

…and it works!

In action:

Click to play on YouTube.

The results

After a little bit of tinkering, I can now:

Of course, this still isn’t great. Unix signals are slow, and using a signal (SIGURG) to do something completely orthogonal to its original purpose probably isn’t a good idea (especially if bash suddenly adds it to its terminating_signals[] array). Then again, this is bash we’re talking about - performance isn’t going to be great in the first place, and it works!

¯\_(ツ)_/¯

- William