~charles/cdaniels.net

6ba7392104bb665fa6968d949d315d595c6bc869 — Charles Daniels a month ago 201ee20
add siguser draft
1 files changed, 159 insertions(+), 0 deletions(-)

A src/sigusr.md
A src/sigusr.md => src/sigusr.md +159 -0
@@ 0,0 1,159 @@
# SIGUSR1: An Under-Appreciated IPC Mechanism

*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)](https://man.openbsd.org/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)](https://man.openbsd.org/man3/signal.3) on OpenBSD, and
[signal(7)](https://man7.org/linux/man-pages/man7/signal.7.html) 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.

<h2 class=navhdr id="catching-sigusr1">
	Catching SIGUSR1
	<a href=#catching-sigusr1 class=navhandle>¶ </a>
</h2>

We can catch SIGUSR1 like any other signal, by creating an appropriate signal
handler:

```c
void handler(int signo) {
	printf("Caught signal %i at ", signo);
	print_time();
	printf("\n");
}
```

And then registering it using `signal()`:

```c
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:

```c
#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:

```plaintext
$ cc main.c
$ ./a.out
```

And now from another terminal, run:

```plaintext
$ pkill -USR1 a.out
```

Back in the first terminal, we should see something like:

```plaintext
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
```

<h2 class=navhdr id="why">
	Why This is Useful
	<a href=#why class=navhandle>¶ </a>
</h2>

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:

* No need to set up any network service, or indeed even touch the network at
  all.
* Enables synchronous push-based communication, rather than polling.
* Simple to implement in nearly all languages.

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`](https://git.sr.ht/~charles/dotfiles/tree/b8a2505022f932e959c810a5fcff310a4e3d0e27/overlay/bin/dpmsman)
automatically sets my monitor's DPMS settings based on the laptop's dock state
and a config file.
[`displayman`](https://git.sr.ht/~charles/dotfiles/tree/b8a2505022f932e959c810a5fcff310a4e3d0e27/overlay/bin/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.