summaryrefslogtreecommitdiff
path: root/src/pv/cursor.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/pv/cursor.c')
-rw-r--r--src/pv/cursor.c515
1 files changed, 515 insertions, 0 deletions
diff --git a/src/pv/cursor.c b/src/pv/cursor.c
new file mode 100644
index 0000000..0c6fc5b
--- /dev/null
+++ b/src/pv/cursor.c
@@ -0,0 +1,515 @@
+/*
+ * Cursor positioning functions.
+ *
+ * If IPC is available, then a shared memory segment is used to co-ordinate
+ * cursor positioning across multiple instances of `pv'. The shared memory
+ * segment contains an integer which is the original "y" co-ordinate of the
+ * first `pv' process.
+ *
+ * However, some OSes (FreeBSD and MacOS X so far) don't allow locking of a
+ * terminal, so we try to use a lockfile if terminal locking doesn't work,
+ * and finally abort if even that is unavailable.
+ *
+ * Copyright 2012 Andrew Wood, distributed under the Artistic License 2.0.
+ */
+
+#include "options.h"
+#include "pv.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <termios.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#ifdef HAVE_IPC
+#include <sys/types.h>
+#include <sys/ipc.h>
+#include <sys/shm.h>
+# ifdef HAVE_SYS_PARAM_H
+# include <sys/param.h>
+# endif
+# ifdef HAVE_LIBGEN_H
+# include <libgen.h>
+# endif
+#endif /* HAVE_IPC */
+
+
+#ifdef HAVE_IPC
+static int pv_crs__shmid = -1; /* ID of our shared memory segment */
+static int pv_crs__pvcount = 1; /* number of `pv' processes in total */
+static int pv_crs__pvmax = 0; /* highest number of `pv's seen */
+static int *pv_crs__y_top = 0; /* pointer to Y coord of topmost `pv' */
+static int pv_crs__y_lastread = 0; /* last value of __y_top seen */
+static int pv_crs__y_offset = 0; /* our Y offset from this top position */
+static int pv_crs__needreinit = 0; /* set if we need to reinit cursor pos */
+static int pv_crs__noipc = 0; /* set if we can't use IPC */
+#endif /* HAVE_IPC */
+static int pv_crs__uselockfile = 0; /* set if we used a lockfile */
+static int pv_crs__lock_fd = -1; /* fd of lockfile, -1 if none open */
+static int pv_crs__y_start = 0; /* our initial Y coordinate */
+
+
+/*
+ * Lock the terminal on the given file descriptor by creating and locking a
+ * per-euid, per-tty, lockfile in ${TMPDIR:-${TMP:-/tmp}}.
+ */
+static void pv_crs__lock_lockfile(int fd)
+{
+#ifdef O_EXLOCK
+ char *ttydev;
+ char *tmpdir;
+#ifndef MAXPATHLEN
+#define MAXPATHLEN 4096
+#endif
+ char lockfile[MAXPATHLEN + 1]; /* RATS: ignore */
+
+ pv_crs__uselockfile = 1;
+
+ ttydev = ttyname(fd); /* RATS: ignore */
+ if (!ttydev) {
+#ifdef HAVE_IPC
+ pv_crs__noipc = 1;
+#endif
+ return;
+ }
+
+ tmpdir = (char *) getenv("TMPDIR"); /* RATS: ignore */
+ if (!tmpdir)
+ tmpdir = (char *) getenv("TMP"); /* RATS: ignore */
+ if (!tmpdir)
+ tmpdir = "/tmp";
+
+#ifdef HAVE_SNPRINTF
+ snprintf(lockfile, MAXPATHLEN, "%s/pv-%s-%i.lock",
+ tmpdir, basename(ttydev), geteuid());
+#else
+ sprintf(lockfile, /* RATS: ignore */
+ "%.*s/pv-%8s-%i.lock",
+ MAXPATHLEN - 64, tmpdir, basename(ttydev), geteuid());
+#endif
+
+ pv_crs__lock_fd =
+ open(lockfile, O_RDWR | O_EXLOCK | O_CREAT | O_NOFOLLOW, 0600);
+#ifdef HAVE_IPC
+ if (pv_crs__lock_fd < 0)
+ pv_crs__noipc = 1;
+#endif
+
+#else /* !O_EXLOCK */
+
+ pv_crs__uselockfile = 1;
+#ifdef HAVE_IPC
+ pv_crs__noipc = 1;
+#endif
+
+#endif /* O_EXLOCK */
+}
+
+
+/*
+ * Lock the terminal on the given file descriptor, falling back to using a
+ * lockfile if the terminal itself cannot be locked.
+ */
+static void pv_crs__lock(int fd)
+{
+ struct flock lock;
+
+ lock.l_type = F_WRLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 1;
+ while (fcntl(fd, F_SETLKW, &lock) < 0) {
+ if (errno != EINTR) {
+ pv_crs__lock_lockfile(fd);
+ return;
+ }
+ }
+}
+
+
+/*
+ * Unlock the terminal on the given file descriptor. If pv_crs__lock used
+ * lockfile locking, unlock the lockfile.
+ */
+static void pv_crs__unlock(int fd)
+{
+ struct flock lock;
+
+ if (pv_crs__uselockfile) {
+ if (pv_crs__lock_fd >= 0)
+ close(pv_crs__lock_fd);
+ pv_crs__lock_fd = -1;
+ } else {
+ lock.l_type = F_UNLCK;
+ lock.l_whence = SEEK_SET;
+ lock.l_start = 0;
+ lock.l_len = 1;
+ fcntl(fd, F_SETLK, &lock);
+ }
+}
+
+
+#ifdef HAVE_IPC
+/*
+ * Get the current number of processes attached to our shared memory
+ * segment, i.e. find out how many `pv' processes in total are running in
+ * cursor mode (including us), and store it in pv_crs__pvcount. If this is
+ * larger than pv_crs__pvmax, update pv_crs__pvmax.
+ */
+static void pv_crs__ipccount(void)
+{
+ struct shmid_ds buf;
+
+ buf.shm_nattch = 0;
+
+ shmctl(pv_crs__shmid, IPC_STAT, &buf);
+ pv_crs__pvcount = buf.shm_nattch;
+
+ if (pv_crs__pvcount > pv_crs__pvmax)
+ pv_crs__pvmax = pv_crs__pvcount;
+}
+#endif /* HAVE_IPC */
+
+
+/*
+ * Get the current cursor Y co-ordinate by sending the ECMA-48 CPR code to
+ * the terminal connected to the given file descriptor.
+ */
+static int pv_crs__get_ypos(int terminalfd)
+{
+ struct termios tty;
+ struct termios old_tty;
+ char cpr[32]; /* RATS: ignore (checked) */
+ int ypos;
+
+ tcgetattr(terminalfd, &tty);
+ tcgetattr(terminalfd, &old_tty);
+ tty.c_lflag &= ~(ICANON | ECHO);
+ tcsetattr(terminalfd, TCSANOW | TCSAFLUSH, &tty);
+ write(terminalfd, "\033[6n", 4);
+ memset(cpr, 0, sizeof(cpr));
+ read(terminalfd, cpr, 6); /* RATS: ignore (OK) */
+ ypos = pv_getnum_i(cpr + 2);
+ tcsetattr(terminalfd, TCSANOW | TCSAFLUSH, &old_tty);
+
+ return ypos;
+}
+
+
+#ifdef HAVE_IPC
+/*
+ * Initialise the IPC data, returning nonzero on error.
+ *
+ * To do this, we attach to the shared memory segment (creating it if it
+ * does not exist). If we are the only process attached to it, then we
+ * initialise it with the current cursor position.
+ *
+ * There is a race condition here: another process could attach before we've
+ * had a chance to check, such that no process ends up getting an "attach
+ * count" of one, and so no initialisation occurs. So, we lock the terminal
+ * with pv_crs__lock() while we are attaching and checking.
+ */
+static int pv_crs__ipcinit(opts_t opts, char *ttyfile, int terminalfd)
+{
+ key_t key;
+
+ /*
+ * Base the key for the shared memory segment on our current tty, so
+ * we don't end up interfering in any way with instances of `pv'
+ * running on another terminal.
+ */
+ key = ftok(ttyfile, 'p');
+ if (key == -1) {
+ fprintf(stderr, "%s: %s: %s\n",
+ opts->program_name,
+ _("failed to open terminal"), strerror(errno));
+ return 1;
+ }
+
+ pv_crs__lock(terminalfd);
+ if (pv_crs__noipc) {
+ fprintf(stderr, "%s: %s: %s\n",
+ opts->program_name,
+ _("failed to lock terminal"), strerror(errno));
+ return 1;
+ }
+
+ pv_crs__shmid = shmget(key, sizeof(int), 0600 | IPC_CREAT);
+ if (pv_crs__shmid < 0) {
+ fprintf(stderr, "%s: %s: %s\n",
+ opts->program_name,
+ _("failed to open terminal"), strerror(errno));
+ pv_crs__unlock(terminalfd);
+ return 1;
+ }
+
+ pv_crs__y_top = shmat(pv_crs__shmid, 0, 0);
+
+ pv_crs__ipccount();
+
+ /*
+ * If nobody else is attached to the shared memory segment, we're
+ * the first, so we need to initialise the shared memory with our
+ * current Y cursor co-ordinate.
+ */
+ if (pv_crs__pvcount < 2) {
+ pv_crs__y_start = pv_crs__get_ypos(terminalfd);
+ *pv_crs__y_top = pv_crs__y_start;
+ pv_crs__y_lastread = pv_crs__y_start;
+ }
+
+ pv_crs__y_offset = pv_crs__pvcount - 1;
+ if (pv_crs__y_offset < 0)
+ pv_crs__y_offset = 0;
+
+ /*
+ * If anyone else had attached to the shared memory segment, we need
+ * to read the top Y co-ordinate from it.
+ */
+ if (pv_crs__pvcount > 1) {
+ pv_crs__y_start = *pv_crs__y_top;
+ pv_crs__y_lastread = pv_crs__y_start;
+ }
+
+ pv_crs__unlock(terminalfd);
+
+ return 0;
+}
+#endif /* HAVE_IPC */
+
+
+/*
+ * Initialise the terminal for cursor positioning.
+ */
+void pv_crs_init(opts_t opts)
+{
+ char *ttyfile;
+ int fd;
+
+ if (!opts->cursor)
+ return;
+
+ ttyfile = ttyname(STDERR_FILENO); /* RATS: ignore (unimportant) */
+ if (!ttyfile) {
+ opts->cursor = 0;
+ return;
+ }
+
+ fd = open(ttyfile, O_RDWR); /* RATS: ignore (no race) */
+ if (fd < 0) {
+ fprintf(stderr, "%s: %s: %s\n",
+ opts->program_name,
+ _("failed to open terminal"), strerror(errno));
+ opts->cursor = 0;
+ return;
+ }
+#ifdef HAVE_IPC
+ if (pv_crs__ipcinit(opts, ttyfile, fd)) {
+ opts->cursor = 0;
+ close(fd);
+ return;
+ }
+
+ /*
+ * If we are not using IPC, then we need to get the current Y
+ * co-ordinate. If we are using IPC, then the pv_crs__ipcinit()
+ * function takes care of this in a more multi-process-friendly way.
+ */
+ if (pv_crs__noipc) {
+#else /* ! HAVE_IPC */
+ if (1) {
+#endif /* HAVE_IPC */
+ /*
+ * Get current cursor position + 1.
+ */
+ pv_crs__lock(fd);
+ pv_crs__y_start = pv_crs__get_ypos(fd);
+ pv_crs__unlock(fd);
+
+ if (pv_crs__y_start < 1)
+ opts->cursor = 0;
+ }
+
+ close(fd);
+}
+
+
+#ifdef HAVE_IPC
+/*
+ * Set the "we need to reinitialise cursor positioning" flag.
+ */
+void pv_crs_needreinit(void)
+{
+ pv_crs__needreinit += 2;
+ if (pv_crs__needreinit > 3)
+ pv_crs__needreinit = 3;
+}
+#endif
+
+
+#ifdef HAVE_IPC
+/*
+ * Reinitialise the cursor positioning code (called if we are backgrounded
+ * then foregrounded again).
+ */
+void pv_crs_reinit(void)
+{
+ pv_crs__lock(STDERR_FILENO);
+
+ pv_crs__needreinit--;
+ if (pv_crs__y_offset < 1)
+ pv_crs__needreinit = 0;
+
+ if (pv_crs__needreinit > 0) {
+ pv_crs__unlock(STDERR_FILENO);
+ return;
+ }
+
+ pv_crs__y_start = pv_crs__get_ypos(STDERR_FILENO);
+
+ if (pv_crs__y_offset < 1)
+ *pv_crs__y_top = pv_crs__y_start;
+ pv_crs__y_lastread = pv_crs__y_start;
+
+ pv_crs__unlock(STDERR_FILENO);
+}
+#endif
+
+
+/*
+ * Output a single-line update, moving the cursor to the correct position to
+ * do so.
+ */
+void pv_crs_update(opts_t opts, char *str)
+{
+ char pos[32]; /* RATS: ignore (checked OK) */
+ int y;
+
+#ifdef HAVE_IPC
+ if (!pv_crs__noipc) {
+ if (pv_crs__needreinit)
+ pv_crs_reinit();
+
+ pv_crs__ipccount();
+ if (pv_crs__y_lastread != *pv_crs__y_top) {
+ pv_crs__y_start = *pv_crs__y_top;
+ pv_crs__y_lastread = pv_crs__y_start;
+ }
+
+ if (pv_crs__needreinit > 0)
+ return;
+ }
+#endif /* HAVE_IPC */
+
+ y = pv_crs__y_start;
+
+#ifdef HAVE_IPC
+ /*
+ * If the screen has scrolled, or is about to scroll, due to
+ * multiple `pv' instances taking us near the bottom of the screen,
+ * scroll the screen (only if we're the first `pv'), and then move
+ * our initial Y co-ordinate up.
+ */
+ if (((pv_crs__y_start + pv_crs__pvmax) > opts->height)
+ && (!pv_crs__noipc)
+ ) {
+ int offs;
+
+ offs = ((pv_crs__y_start + pv_crs__pvmax) - opts->height);
+
+ pv_crs__y_start -= offs;
+ if (pv_crs__y_start < 1)
+ pv_crs__y_start = 1;
+
+ /*
+ * Scroll the screen if we're the first `pv'.
+ */
+ if (pv_crs__y_offset == 0) {
+ pv_crs__lock(STDERR_FILENO);
+
+ sprintf(pos, "\033[%d;1H", opts->height);
+ write(STDERR_FILENO, pos, strlen(pos));
+ for (; offs > 0; offs--) {
+ write(STDERR_FILENO, "\n", 1);
+ }
+
+ pv_crs__unlock(STDERR_FILENO);
+ }
+ }
+
+ if (!pv_crs__noipc)
+ y = pv_crs__y_start + pv_crs__y_offset;
+#endif /* HAVE_IPC */
+
+ /*
+ * Keep the Y co-ordinate within sensible bounds, so we can never
+ * overflow the "pos" buffer.
+ */
+ if ((y < 1) || (y > 999999))
+ y = 1;
+ sprintf(pos, "\033[%d;1H", y);
+
+ pv_crs__lock(STDERR_FILENO);
+
+ write(STDERR_FILENO, pos, strlen(pos)); /* RATS: ignore */
+ write(STDERR_FILENO, str, strlen(str)); /* RATS: ignore */
+
+ pv_crs__unlock(STDERR_FILENO);
+}
+
+
+/*
+ * Reposition the cursor to a final position.
+ */
+void pv_crs_fini(opts_t opts)
+{
+ char pos[32]; /* RATS: ignore (checked OK) */
+ int y;
+
+ y = pv_crs__y_start;
+
+#ifdef HAVE_IPC
+ if ((pv_crs__pvmax > 0) && (!pv_crs__noipc))
+ y += pv_crs__pvmax - 1;
+#endif /* HAVE_IPC */
+
+ if (y > opts->height)
+ y = opts->height;
+
+ /*
+ * Absolute bounds check.
+ */
+ if ((y < 1) || (y > 999999))
+ y = 1;
+
+ sprintf(pos, "\033[%d;1H\n", y); /* RATS: ignore */
+
+ pv_crs__lock(STDERR_FILENO);
+
+ write(STDERR_FILENO, pos, strlen(pos)); /* RATS: ignore */
+
+#ifdef HAVE_IPC
+ pv_crs__ipccount();
+ shmdt((void *) pv_crs__y_top);
+
+ /*
+ * If we are the last instance detaching from the shared memory,
+ * delete it so it's not left lying around.
+ */
+ if (pv_crs__pvcount < 2)
+ shmctl(pv_crs__shmid, IPC_RMID, 0);
+
+#endif /* HAVE_IPC */
+
+ pv_crs__unlock(STDERR_FILENO);
+}
+
+/* EOF */