Published DRAFT
Polling sucks, but inter-process communication (IPC) is hard. Sometimes you have two programs that need to coordinate, and you just want a simple way for one to tell the other "hey, new information is ready, go look at it". Here's a trick I picked up from xidle(1)... I'm sure other programs have used it, but this is where I got the idea.
You may be familiar with UNIX signals (see
signal(3) on OpenBSD, and
signal(7) on Linux),
which allow one program to interrupt another. Most often, this is in the form
of a SIGKILL
or SIGSTOP
to halt a misbehaving program. What we're
interested in today is SIGUSR1
, which is one of two signals set aside for
user-defined use.
We can catch SIGUSR1 like any other signal, by creating an appropriate signal handler:
void handler(int signo) {
printf("Caught signal %i at ", signo);
print_time();
printf("\n");
}
And then registering it using signal()
:
if (signal(SIGUSR1, handler) == SIG_ERR) {
err(1, "Failed to register signal handler");
}
In this trivial example, we just print out a timestamp, but in a real program,
it might cause you to go read a file, FIFO, socket, or some other medium by
which you can gather more information. Or in the case of xidle(1)
, if your
need for IPC is simple enough, you may not need any further information. Lets
look at a fully working example:
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <err.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
/**
* @brief Displays the current local time on standard out.
*/
void print_time(void) {
/* get the current time and convert to local time */
time_t t;
struct tm* l;
time (&t);
l = localtime(&t);
/* generate a timestamp string, and remove the trailing newline */
char* ts;
asprintf(&ts, "%s", asctime(l));
ts[strlen(ts)-1] = '\0';
/* display the timestamp */
printf("%s", ts);
free(ts);
}
/**
* @brief Handler for SIGUSR1
*
* @param signo
*/
void handler(int signo) {
printf("Caught signal %i at ", signo);
print_time();
printf("\n");
}
int main(void) {
/* register the signal handler */
if (signal(SIGUSR1, handler) == SIG_ERR) {
err(1, "Failed to register signal handler");
}
/* wait forever, to give us time to send the signal */
printf("Beginning wait at ");
print_time();
printf("\n");
while(1) {
sleep(10);
printf("Waiting... (");
print_time();
printf(")\n");
}
}
We can go ahead and compile and run this program:
$ cc main.c
$ ./a.out
And now from another terminal, run:
$ pkill -USR1 a.out
Back in the first terminal, we should see something like:
Beginning wait at Mon Jun 22 20:15:17 2020
Waiting... (Mon Jun 22 20:15:27 2020)
Caught signal 10 atMon Jun 22 20:15:35 2020
This trick is super easy to implement (just look up how to write a signal handler in your language of choice), and has a few benefits:
The real sell though is that this method can be used to easily trigger code
running as a normal user from the root user, without any of the environment
variable or X11-related shenanigans of sudo
. This is why it's so handy in
xidle
-- you can run xidle
as your own user account, and your ACPI handler
can, as root, run pkill -SIGUSR1 xidle
and cause it to lock the screen, the
xidle
instance having been launched from your ~/.xsession
.
I use this method from a pair of shell scripts to handle my monitor settings.
dpmsman
automatically sets my monitor's DPMS settings based on the laptop's dock state
and a config file.
displayman
updates my xrandr
settings as needed based on what monitors are connected.
Both scripts are triggered from scripts that run as root in response to udev
and ACPI events, and would otherwise not have a convenient or elegant way to
run code as me that needs to interact with the running X session.