aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile21
-rw-r--r--README.txt79
-rw-r--r--slowbaud.1126
-rw-r--r--slowbaud.c307
-rw-r--r--slowbaud.rst117
5 files changed, 650 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..084bee4
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,21 @@
+PROJ=slowbaud
+
+CC=gcc
+CFLAGS=-Wall -O2 $(EXTRACFLAGS)
+LIBS=-lutil
+RST2MAN=rst2man.py
+
+all: $(PROJ)
+
+$(PROJ): $(PROJ).c $(PROJ).1 README.txt
+ $(CC) $(CFLAGS) -o $(PROJ) $(PROJ).c $(LIBS)
+
+$(PROJ).1: $(PROJ).rst
+ $(RST2MAN) $(PROJ).rst > $(PROJ).1
+
+README.txt: $(PROJ).1
+ man ./$(PROJ).1 | sed 's,_\010,,' > README.txt
+
+# Don't remove the man page or README here, it's in git.
+clean:
+ rm -f $(PROJ) core *.o
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..28132d3
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,79 @@
+SLOWBAUD(1) Urchlay's Useless Stuff SLOWBAUD(1)
+
+NAME
+ slowbaud - simulate a low bitrate serial connection
+
+SYNOPSIS
+ slowbaud <bits-per-sec> [<file> ...]
+
+ slowbaud <bits-per-sec> -c [<command> [<arg> ...]]
+
+DESCRIPTION
+ slowbaud by default acts as a filter, or like the cat(1) command. It reads files or
+ its standard input, and writes the contents unmodified to standard output... but
+ slowly, at the given bits-per-second rate. Output is unbuffered.
+
+ With the -c option, it creates a pseudo-tty and runs the given command in it (or an
+ interactive shell, if no command is given).
+
+ The <bits-per-sec> argument supports a range of 1 to 500000. Timing accuracy depends
+ on your OS, kernel config (HZ and/or NO_HZ on Linux), and system load. No "fancy"
+ techniques like realtime scheduling or hardware event timers are used. At bitrates up
+ to 57600, on a typical unloaded Linux system, the timing should be around 99.7% accu‐
+ rate.
+
+ENVIRONMENT
+ SLOWBAUD_DEBUG
+ Set this (to any value) in the environment to see verbose debug output on
+ stderr, including timing accuracy stats.
+
+ SHELL Standard *nix environment variable, used to determine what shell to run when -c
+ is given with no <command>. If unset, /bin/sh is used.
+
+EXIT STATUS
+ Without -c, 0 for success, non-zero on any error such as nonexistent/unreadable files.
+ slowbaud exits immediately on such errors (this is unlike cat(1)).
+
+ With -c, exit status is that of the child process, or 127 if the child process
+ couldn't be spawned (e.g. command not found). Of course, the child process could also
+ exit with status 127...
+
+NOTES
+ We can't really insert a delay between the bits of a byte, since I/O is done with byte
+ granularity. For calculation purposes, <bits-per-sec> is divided by 10 to get bytes
+ per second. This simulates "8-N-1": one start bit, 8 data bits, no parity, and 1 stop
+ bit (total of 10 bits per byte).
+
+ The timing code works by calculating how long to sleep after each character (in
+ microseconds), but actually sleeping slightly less than that, then busy-waiting until
+ the rest of the interval expires. At slower bitrates, this works well, and the CPU
+ overhead is barely noticeable (at least on reasonably fast modern systems).
+
+ The timing error will almost always result in the bitrate being slightly too slow at
+ lower bitrates and slightly too fast at higher ones.
+
+ Timing is more accurate on Linux than OSX. It's done with getitimer() and sigwait().
+ This works out to be slightly more accurate than using usleep() on both Linux and OSX.
+ It would be possible to use the realtime timer_create() and clock_gettime() API on
+ Linux, for possibly even better accuracy, but OSX doesn't have these (and I want to be
+ portable).
+
+ If this were a truly useful application, it would be worth trying to decrease latency
+ further, with realtime process scheduling. I didn't do this because slowbaud is just a
+ toy, and because the RT stuff tends to be unportable and require elevated privileges
+ (root, or something like setrtlimit or extended filesystem attributes to manage capa‐
+ bilities).
+
+ About the name... I'm aware that "baud" is not synonymous with bps. I just think
+ "slowbaud" sounds better than "slowbps", as a name. Anyway the stty command on Linux
+ misuses the term ("speed 38400 baud"), so I'm in good company.
+
+BUGS
+ With -c, signals aren't handled gracefully. Window size changes (SIGWINCH) don't get
+ propagated to the child process, and pressing ^C doesn't interrupt the process. Yet.
+
+COPYRIGHT
+ slowbaud is copyright 2021, B. Watson <yalhcru@gmail.com>. Released under the WTFPL.
+ See http://www.wtfpl.net/txt/copying/ for details.
+
+0.0.1 2021-07-21 SLOWBAUD(1)
diff --git a/slowbaud.1 b/slowbaud.1
new file mode 100644
index 0000000..7fcec1e
--- /dev/null
+++ b/slowbaud.1
@@ -0,0 +1,126 @@
+.\" Man page generated from reStructuredText.
+.
+.TH SLOWBAUD 1 "2021-07-21" "0.0.1" "Urchlay's Useless Stuff"
+.SH NAME
+slowbaud \- simulate a low bitrate serial connection
+.
+.nr rst2man-indent-level 0
+.
+.de1 rstReportMargin
+\\$1 \\n[an-margin]
+level \\n[rst2man-indent-level]
+level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
+-
+\\n[rst2man-indent0]
+\\n[rst2man-indent1]
+\\n[rst2man-indent2]
+..
+.de1 INDENT
+.\" .rstReportMargin pre:
+. RS \\$1
+. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
+. nr rst2man-indent-level +1
+.\" .rstReportMargin post:
+..
+.de UNINDENT
+. RE
+.\" indent \\n[an-margin]
+.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.nr rst2man-indent-level -1
+.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
+..
+.\" RST source for slowbaud(1) man page. Convert with:
+.
+.\" rst2man.py slowbaud.rst > slowbaud.1
+.
+.\" rst2man.py comes from the SBo development/docutils package.
+.
+.SH SYNOPSIS
+.sp
+\fBslowbaud\fP \fI<bits\-per\-sec>\fP [\fI<file>\fP ...]
+.sp
+\fBslowbaud\fP \fI<bits\-per\-sec>\fP \fB\-c\fP [\fI<command>\fP [\fI<arg>\fP ...]]
+.SH DESCRIPTION
+.sp
+slowbaud by default acts as a filter, or like the \fBcat(1)\fP command. It
+reads files or its standard input, and writes the contents unmodified
+to standard output... but slowly, at the given bits\-per\-second rate.
+Output is unbuffered.
+.sp
+With the \fB\-c\fP option, it creates a pseudo\-tty and runs the given command
+in it (or an interactive shell, if no command is given).
+.sp
+The \fI<bits\-per\-sec>\fP argument supports a range of 1 to 500000. Timing
+accuracy depends on your OS, kernel config (HZ and/or NO_HZ on Linux),
+and system load. No "fancy" techniques like realtime scheduling
+or hardware event timers are used. At bitrates up to 57600, on a
+typical unloaded Linux system, the timing should be around 99.7%
+accurate.
+.SH ENVIRONMENT
+.INDENT 0.0
+.TP
+.B \fBSLOWBAUD_DEBUG\fP
+Set this (to any value) in the environment to see verbose debug
+output on stderr, including timing accuracy stats.
+.TP
+.B \fBSHELL\fP
+Standard *nix environment variable, used to determine what
+shell to run when \fB\-c\fP is given with no \fI<command>\fP\&. If
+unset, \fB/bin/sh\fP is used.
+.UNINDENT
+.SH EXIT STATUS
+.sp
+Without \fB\-c\fP, 0 for success, non\-zero on any error such as
+nonexistent/unreadable files. slowbaud exits immediately on such
+errors (this is unlike \fBcat(1)\fP).
+.sp
+With \fB\-c\fP, exit status is that of the child process, or 127 if
+the child process couldn\(aqt be spawned (e.g. command not found).
+Of course, the child process could also exit with status 127...
+.SH NOTES
+.sp
+We can\(aqt really insert a delay between the bits of a byte, since
+I/O is done with byte granularity. For calculation purposes,
+\fI<bits\-per\-sec>\fP is divided by 10 to get bytes per second. This
+simulates "8\-N\-1": one start bit, 8 data bits, no parity, and 1 stop
+bit (total of 10 bits per byte).
+.sp
+The timing code works by calculating how long to sleep after each
+character (in microseconds), but actually sleeping slightly less
+than that, then busy\-waiting until the rest of the interval expires.
+At slower bitrates, this works well, and the CPU overhead is barely
+noticeable (at least on reasonably fast modern systems).
+.sp
+The timing error will almost always result in the bitrate
+being slightly too slow at lower bitrates and slightly too fast at
+higher ones.
+.sp
+Timing is more accurate on Linux than OSX. It\(aqs done with getitimer()
+and sigwait(). This works out to be slightly more accurate than
+using usleep() on both Linux and OSX. It would be possible to use
+the realtime timer_create() and clock_gettime() API on Linux, for
+possibly even better accuracy, but OSX doesn\(aqt have these (and I want to be
+portable).
+.sp
+If this were a truly useful application, it would be worth trying to
+decrease latency further, with realtime process scheduling. I didn\(aqt
+do this because slowbaud is just a toy, and because the RT stuff tends
+to be unportable and require elevated privileges (root, or something
+like setrtlimit or extended filesystem attributes to manage capabilities).
+.sp
+About the name... I\(aqm aware that "baud" is not synonymous with bps. I
+just think "slowbaud" sounds better than "slowbps", as a name. Anyway
+the stty command on Linux misuses the term ("speed 38400 baud"), so
+I\(aqm in good company.
+.SH BUGS
+.sp
+With \fB\-c\fP, signals aren\(aqt handled gracefully. Window size changes
+(SIGWINCH) don\(aqt get propagated to the child process, and pressing ^C
+doesn\(aqt interrupt the process. Yet.
+.SH COPYRIGHT
+.sp
+slowbaud is copyright 2021, B. Watson <\fI\%yalhcru@gmail.com\fP>. Released
+under the WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details.
+.\" Generated by docutils manpage writer.
+.
diff --git a/slowbaud.c b/slowbaud.c
new file mode 100644
index 0000000..bbf31ac
--- /dev/null
+++ b/slowbaud.c
@@ -0,0 +1,307 @@
+/* Simulate low bitrate serial connection, like a 1980s modem.
+ Author: B. Watson. License: WTFPL. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/select.h>
+#include <sys/wait.h>
+#include <termios.h>
+#include <sys/ioctl.h>
+#include <signal.h>
+#include <time.h>
+#include <string.h>
+
+/* portability stuff only tested on Linux and OSX. Hope it's OK. */
+#if defined(__FreeBSD__) || defined(HAVE_LIBUTIL_H)
+ #include <libutil.h>
+#elif defined (__OpenBSD__) || defined (__NetBSD__) || defined (__APPLE__) || defined(HAVE_UTIL_H)
+ #include <util.h>
+#else
+ #include <pty.h>
+#endif
+
+#define DEFAULT_BPS 1200
+#define MIN_BPS 1
+#define MAX_BPS 500000
+
+const char *self;
+unsigned int bps;
+unsigned long interval, starttime, outbytes = 0;
+
+int debug = 0;
+
+int timer;
+struct timeval tv;
+struct itimerval itv, itv_disarm;
+sigset_t sigmask;
+
+#define NOW_USEC() ( (gettimeofday(&tv, NULL)), tv.tv_sec * 1000000L + tv.tv_usec )
+
+void die(const char *msg) {
+ if(errno) {
+ fprintf(stderr, "%s: %s: %s\n", self, strerror(errno), msg);
+ } else {
+ fprintf(stderr, "%s: %s\n", self, msg);
+ }
+ exit(1);
+}
+
+void setup_timer(void) {
+ itv_disarm.it_interval.tv_sec =
+ itv_disarm.it_interval.tv_usec =
+ itv_disarm.it_value.tv_sec =
+ itv_disarm.it_value.tv_usec = 0;
+
+ itv = itv_disarm;
+ itv.it_interval.tv_usec = interval;
+ itv.it_value.tv_usec = interval;
+
+ sigemptyset(&sigmask);
+ sigaddset(&sigmask, SIGALRM);
+ sigprocmask(SIG_BLOCK, &sigmask, NULL);
+}
+
+void slow_write(int outfd, char c) {
+ int j;
+ unsigned long now, target, overslept;
+
+ setitimer(ITIMER_REAL, &itv, NULL);
+
+ target = NOW_USEC() + interval;
+
+ write(outfd, &c, 1);
+ outbytes++;
+ sigwait(&sigmask, &j);
+ setitimer(ITIMER_REAL, &itv_disarm, NULL);
+
+ now = NOW_USEC();
+ if(now > target) {
+ overslept = now - target;
+ if(overslept < interval) {
+ unsigned long old = itv.it_interval.tv_usec;
+ itv.it_interval.tv_usec -= overslept;
+ itv.it_value.tv_usec = itv.it_interval.tv_usec;
+ if(debug) {
+ fprintf(stderr, "tv_usec was %ldms, overslept %ld, new tv_usec %ldms\n",
+ old, overslept, itv.it_interval.tv_usec);
+ }
+ }
+ } else {
+ while(NOW_USEC() < target)
+ /* NOP */ ;
+ }
+}
+
+void debug_stats(void) {
+ if(outbytes && debug) {
+ unsigned long elapsed_us = NOW_USEC() - starttime;
+ double elapsed_sec = ((double)elapsed_us/1000000.0L);
+ double finterval = (1000000.0L / ((double)bps / 10.0L));
+ double actual = ((double)outbytes * 10.0L) / elapsed_sec;
+ double offby = 100.0L * (((double)bps / actual) - 1.0L);
+ fprintf(stderr,
+ "outbytes %lu, elapsed_us %lu, requested bps %d (%.2fms), "
+ "actual %.2f, accuracy %.2f%%\n",
+ outbytes, elapsed_us, bps, finterval, actual, 100.0 - offby);
+ }
+ outbytes = 0;
+}
+
+void slowcat(int infd) {
+ int c;
+
+ starttime = NOW_USEC();
+ while(read(infd, &c, 1) == 1) {
+ slow_write(1, c);
+ }
+
+ if(debug) debug_stats();
+}
+
+void slowtty(char **args) {
+ char *dflt_args[] = { NULL, NULL };
+ pid_t kidpid;
+ int master, status;
+ struct termios term, oldterm;
+ struct winsize winsz;
+ char c;
+
+ if(!*args) {
+ args = dflt_args;
+ args[0] = getenv("SHELL");
+ if(!*args) args[0] = "/bin/sh";
+ }
+ if(debug) {
+ char **a = args;
+ fprintf(stderr, "executing cmd '%s', argv[]: ", a[0]);
+ while(*a) {
+ fprintf(stderr, "%s ", *a++);
+ }
+ fprintf(stderr, "\n");
+ }
+
+ if( (tcgetattr(0, &term)) ) die("tcgetattr");
+
+ if( (ioctl(0, TIOCGWINSZ, &winsz)) ) die("ioctl(TIOCGWINSZ)");
+
+ kidpid = forkpty(&master, NULL, &term, &winsz);
+ if(kidpid < 0) die("forkpty");
+
+ if(kidpid == 0) {
+ /* we are the child, do childish things */
+ execvp(args[0], args);
+ perror("execvp"); /* don't use die() */
+ exit(127);
+ }
+
+ /* we are the parent, watch the child */
+ oldterm = term; /* so we can restore it later */
+ cfmakeraw(&term);
+ tcsetattr(0, TCSANOW, &term);
+
+ while(1) {
+ ssize_t nbytes;
+ fd_set readfds;
+
+ FD_ZERO(&readfds);
+ FD_SET(0, &readfds);
+ FD_SET(master, &readfds);
+
+ if(select(master + 1, &readfds, NULL, NULL, NULL) < 0) {
+ if(errno == EINTR) continue;
+ break;
+ }
+
+ if(FD_ISSET(0, &readfds)) {
+ nbytes = read(0, &c, 1);
+ if(nbytes < 1) break;
+ write(master, &c, 1);
+ }
+
+ if(FD_ISSET(master, &readfds)) {
+ nbytes = read(master, &c, 1);
+ if(nbytes < 1) break;
+ slow_write(1, c);
+ }
+ }
+
+ close(master);
+ while (waitpid(kidpid, &status, 0) < 0 && errno == EINTR) /* NOP */ ;
+
+ tcsetattr(STDIN_FILENO, TCSANOW, &oldterm);
+ tcsetattr(STDOUT_FILENO, TCSANOW, &oldterm);
+ tcsetattr(STDERR_FILENO, TCSANOW, &oldterm);
+
+ exit(WIFEXITED(status) ? WEXITSTATUS(status) : 127);
+}
+
+void echo_args(char **args) {
+ starttime = NOW_USEC();
+ while(*args) {
+ char *a = *args;
+ while(*a) slow_write(1, *a++);
+ args++;
+ if(*args) slow_write(1, ' ');
+ }
+
+ slow_write(1, '\n');
+
+ if(debug) debug_stats();
+ exit(0);
+}
+
+void usage(int exitstat) {
+ printf("Usage: %s [<bits-per-sec>] [<file> [<file> ...]]\n", self);
+ printf(" With no filenames, reads stdin.\n");
+ printf("or: %s [<bits-per-sec>] -c [<command> [<arg> ...]]\n", self);
+ printf(" With no command, spawns a shell.\n");
+ printf("or: %s [<bits-per-sec>] -e <string> [<string> ...]\n", self);
+ printf("With no <bits-per-sec>, default rate is %d\n", DEFAULT_BPS);
+ exit(exitstat);
+}
+
+int main(int argc, char **argv) {
+ int infd;
+ const char *p, *env_bps;
+
+ if(getenv("SLOWBAUD_DEBUG")) debug = 1;
+
+ self = argv[0];
+ for(p = argv[0]; *p; p++) {
+ if(*p == '/') self = p + 1;
+ }
+ argv++, argc--;
+
+ /* set bps. command line option overrides env, env overrides builtin */
+ bps = DEFAULT_BPS;
+
+ if( (env_bps = getenv("SLOWBAUD_BPS")) )
+ bps = atoi(env_bps);
+
+ if(*argv && (**argv > '0' && **argv <= '9')) {
+ char *e;
+ long b = strtol(*argv, &e, 0);
+ if(!*e) {
+ bps = (int)b;
+ argv++, argc--;
+ }
+ }
+
+ if(bps < MIN_BPS || bps > MAX_BPS) {
+ fprintf(stderr, "%s: invalid bps, range %d - %d\n", self, MIN_BPS, MAX_BPS);
+ exit(1);
+ }
+
+ /* if we used only integer math here, we couldn't support bps not
+ a multiple of 10 (e.g. 75 would be taken as 70). */
+ interval = (unsigned long)(1000000.0L / ((double)bps / 10.0L));
+ if(debug) fprintf(stderr, "interval %ld us\n", interval);
+
+ if(*argv && argv[0][0] == '-' && argv[0][1] == '-' && !argv[0][2]) {
+ if(debug) fprintf(stderr, "skipping -- argument\n");
+ argv++, argc--;
+ }
+
+ setup_timer();
+
+ if(!argc) {
+ if(debug) fprintf(stderr, "no args, reading stdin\n");
+ slowcat(0);
+ } else if(**argv == '-') {
+ switch(argv[0][1]) {
+ case 'c':
+ if(debug) fprintf(stderr, "-c given, creating a pty\n");
+ slowtty(++argv);
+ break;
+ case 'e':
+ if(argc < 2) die("-e requires at least one argument");
+ if(debug) fprintf(stderr, "-e given, echoing args\n");
+ echo_args(++argv);
+ break;
+ case 'h':
+ case '?':
+ usage(0);
+ break;
+ default:
+ die("invalid option, try -h for help");
+ break;
+ }
+ } else {
+ while(*argv) {
+ if(debug) fprintf(stderr, "opening file %s for reading\n", *argv);
+ if( (infd = open(*argv, O_RDONLY)) < 0) {
+ die(*argv);
+ }
+ slowcat(infd);
+ close(infd);
+ argv++;
+ }
+ }
+ exit(0);
+}
diff --git a/slowbaud.rst b/slowbaud.rst
new file mode 100644
index 0000000..13e9e64
--- /dev/null
+++ b/slowbaud.rst
@@ -0,0 +1,117 @@
+.. RST source for slowbaud(1) man page. Convert with:
+.. rst2man.py slowbaud.rst > slowbaud.1
+.. rst2man.py comes from the SBo development/docutils package.
+
+.. |version| replace:: 0.0.1
+.. |date| date::
+
+========
+slowbaud
+========
+
+----------------------------------------
+simulate a low bitrate serial connection
+----------------------------------------
+
+:Manual section: 1
+:Manual group: Urchlay's Useless Stuff
+:Date: |date|
+:Version: |version|
+
+SYNOPSIS
+========
+
+**slowbaud** *<bits-per-sec>* [*<file>* ...]
+
+**slowbaud** *<bits-per-sec>* **-c** [*<command>* [*<arg>* ...]]
+
+DESCRIPTION
+===========
+
+slowbaud by default acts as a filter, or like the **cat(1)** command. It
+reads files or its standard input, and writes the contents unmodified
+to standard output... but slowly, at the given bits-per-second rate.
+Output is unbuffered.
+
+With the **-c** option, it creates a pseudo-tty and runs the given command
+in it (or an interactive shell, if no command is given).
+
+The *<bits-per-sec>* argument supports a range of 1 to 500000. Timing
+accuracy depends on your OS, kernel config (HZ and/or NO_HZ on Linux),
+and system load. No "fancy" techniques like realtime scheduling
+or hardware event timers are used. At bitrates up to 57600, on a
+typical unloaded Linux system, the timing should be around 99.7%
+accurate.
+
+ENVIRONMENT
+===========
+
+**SLOWBAUD_DEBUG**
+ Set this (to any value) in the environment to see verbose debug
+ output on stderr, including timing accuracy stats.
+
+**SHELL**
+ Standard \*nix environment variable, used to determine what
+ shell to run when **-c** is given with no *<command>*. If
+ unset, **/bin/sh** is used.
+
+EXIT STATUS
+===========
+
+Without **-c**, 0 for success, non-zero on any error such as
+nonexistent/unreadable files. slowbaud exits immediately on such
+errors (this is unlike **cat(1)**).
+
+With **-c**, exit status is that of the child process, or 127 if
+the child process couldn't be spawned (e.g. command not found).
+Of course, the child process could also exit with status 127...
+
+NOTES
+=====
+
+We can't really insert a delay between the bits of a byte, since
+I/O is done with byte granularity. For calculation purposes,
+*<bits-per-sec>* is divided by 10 to get bytes per second. This
+simulates "8-N-1": one start bit, 8 data bits, no parity, and 1 stop
+bit (total of 10 bits per byte).
+
+The timing code works by calculating how long to sleep after each
+character (in microseconds), but actually sleeping slightly less
+than that, then busy-waiting until the rest of the interval expires.
+At slower bitrates, this works well, and the CPU overhead is barely
+noticeable (at least on reasonably fast modern systems).
+
+The timing error will almost always result in the bitrate
+being slightly too slow at lower bitrates and slightly too fast at
+higher ones.
+
+Timing is more accurate on Linux than OSX. It's done with getitimer()
+and sigwait(). This works out to be slightly more accurate than
+using usleep() on both Linux and OSX. It would be possible to use
+the realtime timer_create() and clock_gettime() API on Linux, for
+possibly even better accuracy, but OSX doesn't have these (and I want to be
+portable).
+
+If this were a truly useful application, it would be worth trying to
+decrease latency further, with realtime process scheduling. I didn't
+do this because slowbaud is just a toy, and because the RT stuff tends
+to be unportable and require elevated privileges (root, or something
+like setrtlimit or extended filesystem attributes to manage capabilities).
+
+About the name... I'm aware that "baud" is not synonymous with bps. I
+just think "slowbaud" sounds better than "slowbps", as a name. Anyway
+the stty command on Linux misuses the term ("speed 38400 baud"), so
+I'm in good company.
+
+BUGS
+====
+
+With **-c**, signals aren't handled gracefully. Window size changes
+(SIGWINCH) don't get propagated to the child process, and pressing ^C
+doesn't interrupt the process. Yet.
+
+COPYRIGHT
+=========
+
+slowbaud is copyright 2021, B. Watson <yalhcru@gmail.com>. Released
+under the WTFPL. See http://www.wtfpl.net/txt/copying/ for details.