/* * 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 #include #include #include #include #include #include #include #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef HAVE_IPC #include #include #include # ifdef HAVE_SYS_PARAM_H # include # endif # ifdef HAVE_LIBGEN_H # include # 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 */