Maybe FIFO
This post concerns using a FIFO on unix for notifications from a command line to to a service bot. The maybe part is that we have a reader (the bot) that will mostly be doing other things, and a writer that will make a best effort but will not block. The default for FIFOs is to block, a lot, which does not work here. If a message fails we do want alerts to stderr and possibly also to syslog, so that afterwards there are indications of what failed and when.
File permissions (and those of any parent directories) should be restricted by default, or custom groups used to limit who can talk to what, the group advice coming with the caveat that certain daemons have a habit of dropping secondary groups by default. Restricted permissions will help prevent malicious or annoying users from playing around with the FIFO.
Reader Implementation
This part reads from the FIFO, non-blocking. There is not an event loop as the actual code is part of a libircclient bot, so the read happens now and then in a thread that also carries out other checks. If you do have libevent or some other fancy I/O library, then the FIFO should ideally be integrated into that.
// reader1 - non-blocking FIFO reader example
#include "yadda.h"
int
main(int argc, char *argv[])
{
char buf[MSG_MAX + 1], *fname;
int fd;
ssize_t amount;
if (argc != 2 || !argv[1] || !argv[1][0]) {
fputs("Usage: reader1 fifo-name\n", stderr);
exit(1);
}
fname = argv[1];
#ifdef __OpenBSD__
if (pledge("dpath stdio rpath unveil", NULL) == -1) err(1, "pledge");
if (unveil(fname, "crw") == -1) err(1, "unveil");
if (unveil(NULL, NULL) == -1) err(1, "unveil");
#endif
if (mkfifo(fname, 0600) == -1 && errno != EEXIST)
err(1, "mkfifo '%s'", fname);
fd = open(fname, O_RDONLY | O_NDELAY);
if (fd == -1) err(1, "open '%s'", fname);
while (1) {
if ((amount = read(fd, buf, MSG_MAX)) == -1) {
if (errno != EAGAIN) warn("read");
} else if (amount > 0) {
buf[amount] = '\0';
fprintf(stderr, "%ld %s\n", amount, buf);
continue;
}
// KLUGE pretend to be doing other things
sleep(10);
}
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MSG_MAX 127
A bummer is that the FIFO is busy-checked. Not often, but too much given the desire for quick enough notifications versus the fact that the notifications happen somewhere between zero to only a few times per day. This portion of the code should block and only run when there are bytes to read. However, the entire process cannot be blocked as the process must respond to IRC PING messages, among other tasks. One option would be to integrate the libircclient code into an event system, but that would require learning more about the low-level details of libircclient instead of leaning on the event loop that libircclient provides. Yet another would be to replace libircclient with enough string handling for the IRC protocol to work in an event system. A third option would be to add a third thread (one is already running the libircclient loop, and another is performing periodic monitoring checks) that does block until the FIFO has bytes to read. A concern is whether the libircclient routines used are thread safe, or whether the various threads will need to use locks to avoid conflicting with one another. And that blocking I/O can be put into a random thread without problems, like, with signals?
Writer Implementation
This is a tool that writes a message to the reader. Longer messages would run into IRC length limits, and anyways notifications (unlike certain blog posts) should be short and to the point. A postcard is not an appropriate medium for a dissertation. No attempt at locking is made as the notifications will be both infrequent and short enough that the writes should send a complete message in one go. Hopefully. Also the messages are saved "somewhere" after the writer exits; this somewhere is doubtless limited and may result in messages being lost, or new messages failing to post if the reader does not drain that somewhere soon enough. Or you could run the kernel out of memory and the system may crash. Hopefully you don't have one of those kernels.
// writer1 - send messages to a FIFO
#include "yadda.h"
int
main(int argc, char *argv[])
{
char *fname, *message;
int fd, exit_status = 0;
size_t len;
ssize_t amount;
if (argc != 3) {
fputs("Usage: writer1 fifo-name message\n", stderr);
exit(1);
}
fname = argv[1];
message = argv[2];
if ((len = strlen(message)) > MSG_MAX) {
warnx("message is too long, truncating");
len = MSG_MAX;
exit_status = 2;
}
#ifdef __OpenBSD__
if (pledge("dpath stdio rpath wpath unveil", NULL) == -1)
err(1, "pledge");
if (unveil(fname, "cw") == -1) err(1, "unveil");
if (unveil(NULL, NULL) == -1) err(1, "unveil");
#endif
if (mkfifo(fname, 0600) == -1 && errno != EEXIST)
err(1, "mkfifo '%s'", fname);
fd = open(fname, O_WRONLY | O_NDELAY);
// whoops, there is no reader
if (fd == -1) err(1, "open '%s'", fname);
if ((amount = write(fd, message, len)) == -1) err(1, "write");
if ((size_t) amount != len) {
warnx("incomplete write (%ld,%lu)", amount, len);
exit_status = 2;
}
exit(exit_status);
}
A URL shortener may be necessary if a tool generates notification URL that are too long, and you cannot fix that tool to send something shorter. This involves reading the too-long input, obtaining a shorter link that redirects to the original, then sending the shorter link through the message system. There are various problems with this, like not knowing where links go (did someone submit a phishing link?) and all the extra complexity involved. I mention this because overly long URL do exist and you may need to deal with them. Overly long URL for the old web wrapped in a blob of JavaScript that generates a button to click on? Burn it with fire.
$ make reader1 writer1
cc -O2 -pipe -o reader1 reader1.c
cc -O2 -pipe -o writer1 writer1.c
$ ./reader1 io &
[1] 31568
$ ./writer1 foo
Usage: writer1 fifo-name message
$ ./writer1 io foo
$ 3 foo
$ pkill reader1
[1] + Terminated ./reader1 io
$ rm io
A known FIFO file location should be compiled into the notification tool so that it does not need to be specified, just the message to write. Or, an alias could be written that includes the right FIFO, but then you need to remember to have and use that correct alias.
Some Random Testing
What happens when the reader blocks for too long, and too much data is stuffed into the FIFO by a writer? For this the sleep(3) delay in reader1.c might be increased, and a custom client used to write a lot of bytes.
$ cp reader1.c reader2.c
$ perl -i -ple 's/10/9999/' reader2.c
$ make reader2
cc -O2 -pipe -o reader2 reader2.c
$ ./reader2 io &
[1] 47706
For a test client we want something easy to fiddle around with, but also low-level enough so that specific return values from specific system calls can be called out, rather than as abstracted through a higher level I/O system that may well hide such details. On embiggened systems the defaults may need to be embiggened to account for embiggened buffers on such systems, or you could use a binary search to look around for the limits automatically, but that's more work.
#!/usr/bin/perl
use Fcntl;
my $fname = shift // "io";
my $len = shift || 99999;
my $lottabytes = "a" x $len;
sysopen my $fh, "io", O_WRONLY | O_NONBLOCK or die "sysopen: $!\n";
warn "pid $$\n";
while (1) {
my $ret = syswrite $fh, $lottabytes;
if ( !defined $ret ) {
die "syswrite: $!\n";
} elsif ( $ret != $len ) {
die "syswrite: wrote $ret != $len\n";
}
}
First up, the 99999 has run into the default buffer size and failed. This will vary depending on how embiggened the buffers are on your system. With multiple clients writing such large messages there would definitely be a risk of corrupted input, as client A could write 39299 of some larger message, then client B might write something, and, lo, mixed messages seen by the reader.
$ perl busywriter.pl
pid 39299
syswrite: wrote 32768 != 99999
$ perl busywriter.pl io 32768
pid 64010
syswrite: Resource temporarily unavailable (35)
$ errno 35
EAGAIN 35 Resource temporarily unavailable
Some tweaks later (I'm making this up as I go along) and the script is now:
#!/usr/bin/perl
use Errno;
use Fcntl;
my $fname = shift // "io";
my $len = shift || 99999;
my $lottabytes = "a" x $len;
sysopen my $fh, "io", O_WRONLY | O_NONBLOCK or die "sysopen: $!\n";
warn "pid $$\n";
while (1) {
my $ret = syswrite $fh, $lottabytes;
if ( !defined $ret ) {
if ($!{EAGAIN}) {
warn "Eagain, son of Erranon\n";
sleep 1;
} else {
my $e = 0 + $!;
die "syswrite: $! ($e)\n";
}
} elsif ( $ret != $len ) {
die "syswrite: wrote $ret != $len\n";
}
}
Which results in the non-blocking I/O bouncing off of the sleeping reader:
$ perl busywriter.pl io 32768
pid 33411
Eagain, son of Erranon
Eagain, son of Erranon
Eagain, son of Erranon
Eagain, son of Erranon
^C
It may also be instructive to see what happens when the writer uses blocking I/O.