Files
pv/src/pv/cursor.c
T

692 lines
18 KiB
C

/*
* 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 2002-2008, 2010, 2012-2015, 2017, 2021, 2023-2025 Andrew Wood
*
* License GPLv3+: GNU GPL version 3 or later; see `docs/COPYING'.
*/
#include "config.h"
#include "pv.h"
#include "pv-internal.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>
#include <signal.h>
#ifdef HAVE_IPC
#include <sys/ipc.h>
#include <sys/shm.h>
#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif
#endif /* HAVE_IPC */
/* for basename() */
#ifdef HAVE_LIBGEN_H
#include <libgen.h>
#endif
/*
* Create a per-euid, per-tty, lockfile in ${TMPDIR:-${TMP:-/tmp}} for the
* tty on the given file descriptor.
*/
static void pv_crs_open_lockfile(pvstate_t state, int fd)
{
char *ttydev;
char *tmpdir;
int openflags;
state->cursor.lock_fd = -1;
ttydev = ttyname(fd);
if (!ttydev) {
if (!state->control.force) {
pv_error(state, "%s: %s", _("failed to get terminal name"), strerror(errno));
}
/*
* If we don't know our terminal name, we can neither do IPC
* nor make a lock file, so turn off cursor positioning.
*/
state->control.cursor = 0;
debug("%s", "ttyname failed - cursor positioning disabled");
return;
}
tmpdir = (char *) getenv("TMPDIR"); /* flawfinder: ignore */
if ((NULL == tmpdir) || ('\0' == tmpdir[0]))
tmpdir = (char *) getenv("TMP"); /* flawfinder: ignore */
if ((NULL == tmpdir) || ('\0' == tmpdir[0]))
tmpdir = "/tmp";
/*
* flawfinder rationale: null and zero-size values of $TMPDIR and
* $TMP are rejected, and the destination buffer is bounded.
*/
memset(state->cursor.lock_file, 0, PV_SIZEOF_CRS_LOCK_FILE);
(void) pv_snprintf(state->cursor.lock_file,
PV_SIZEOF_CRS_LOCK_FILE, "%s/pv-%s-%i.lock", tmpdir, basename(ttydev), (int) geteuid());
/*
* Pawel Piatek - not everyone has O_NOFOLLOW, e.g. AIX doesn't.
*/
#ifdef O_NOFOLLOW
openflags = O_RDWR | O_CREAT | O_NOFOLLOW;
#else
openflags = O_RDWR | O_CREAT;
#endif
state->cursor.lock_fd = open(state->cursor.lock_file, openflags, 0600); /* flawfinder: ignore */
/*
* flawfinder rationale: we aren't truncating the lock file, we
* don't change its contents, and we are attempting to use
* O_NOFOLLOW where possible to avoid symlink attacks, so this
* open() is as safe as we can make it.
*/
if (state->cursor.lock_fd < 0) {
pv_error(state, "%s: %s: %s", state->cursor.lock_file, _("failed to open lock file"), strerror(errno));
state->control.cursor = 0;
return;
}
}
/*
* 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(pvstate_t state, int fd)
{
struct flock lock;
int lock_fd;
lock_fd = fd;
if (state->cursor.lock_fd >= 0)
lock_fd = state->cursor.lock_fd;
memset(&lock, 0, sizeof(lock));
lock.l_type = (short) F_WRLCK;
lock.l_whence = (short) SEEK_SET;
lock.l_start = 0;
lock.l_len = 1;
while (fcntl(lock_fd, F_SETLKW, &lock) < 0) {
if (errno != EINTR) {
if (state->cursor.lock_fd == -2) {
pv_crs_open_lockfile(state, fd);
if (state->cursor.lock_fd >= 0) {
lock_fd = state->cursor.lock_fd;
}
} else {
pv_error(state, "%s: %s", _("lock attempt failed"), strerror(errno));
return;
}
}
}
if (state->cursor.lock_fd >= 0) {
debug("%s: %s", state->cursor.lock_file, "terminal lockfile acquired");
} else {
debug("%s", "terminal lock acquired");
}
}
/*
* Unlock the terminal on the given file descriptor. If pv_crs_lock used
* lockfile locking, unlock the lockfile.
*/
static void pv_crs_unlock(pvstate_t state, int fd)
{
struct flock lock;
int lock_fd;
lock_fd = fd;
if (state->cursor.lock_fd >= 0)
lock_fd = state->cursor.lock_fd;
memset(&lock, 0, sizeof(lock));
lock.l_type = (short) F_UNLCK;
lock.l_whence = (short) SEEK_SET;
lock.l_start = 0;
lock.l_len = 1;
(void) fcntl(lock_fd, F_SETLK, &lock);
if (state->cursor.lock_fd >= 0) {
debug("%s: %s", state->cursor.lock_file, "terminal lockfile released");
} else {
debug("%s", "terminal lock released");
}
}
#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(pvstate_t state)
{
struct shmid_ds buf;
memset(&buf, 0, sizeof(buf));
buf.shm_nattch = 0;
(void) shmctl(state->cursor.shmid, IPC_STAT, &buf);
state->cursor.pvcount = (int) (buf.shm_nattch);
if (state->cursor.pvcount > state->cursor.pvmax)
state->cursor.pvmax = state->cursor.pvcount;
debug("%s: %d", "pvcount", state->cursor.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.
*
* This should only be called while the terminal is locked, to avoid
* confusing other "pv -c" instances about the current terminal attributes,
* since this function temporarily changes the terminal attributes and then
* puts them back.
*/
static int pv_crs_get_ypos(int terminalfd)
{
struct termios tty;
struct termios old_tty;
char cpr[32]; /* flawfinder: ignore - bounded, zeroed */
int ypos;
ssize_t r;
#ifdef CURSOR_ANSWERBACK_BYTE_BY_BYTE
int got;
#endif /* CURSOR_ANSWERBACK_BYTE_BY_BYTE */
if (0 != tcgetattr(terminalfd, &tty)) {
debug("%s: %s", "tcgetattr (1) failed", strerror(errno));
}
if (0 != tcgetattr(terminalfd, &old_tty)) {
debug("%s: %s", "tcgetattr (2) failed", strerror(errno));
}
tty.c_lflag &= ~(ICANON | ECHO);
if (0 != tcsetattr(terminalfd, TCSANOW | TCSAFLUSH, &tty)) {
debug("%s: %s", "tcsetattr (1) failed", strerror(errno));
}
pv_write_retry(terminalfd, "\033[6n", 4);
memset(cpr, 0, sizeof(cpr));
#ifdef CURSOR_ANSWERBACK_BYTE_BY_BYTE
/* Read answerback byte by byte - fails on AIX */
for (got = 0, r = 0; got < (int) (sizeof(cpr) - 2); got += r) {
r = read(terminalfd, cpr + got, 1); /* flawfinder: ignore */
/* flawfinder rationale: bounded to buffer size by "for" */
if (r <= 0) {
debug("got=%d, r=%d: %s", got, r, strerror(errno));
break;
}
if (cpr[got] == 'R')
break;
}
debug
("read answerback message from fd %d, length %d - buf = %02X %02X %02X %02X %02X %02X",
terminalfd, got, cpr[0], cpr[1], cpr[2], cpr[3], cpr[4], cpr[5]);
#else /* !CURSOR_ANSWERBACK_BYTE_BY_BYTE */
/* Read answerback in one big lump - may fail on Solaris */
r = read(terminalfd, cpr, sizeof(cpr) - 2); /* flawfinder: ignore */
/* flawfinder rationale: bounded to buffer size */
if (r <= 0) {
debug("r=%d: %s", r, strerror(errno));
} else {
debug
("read answerback message from fd %d, length %d - buf = %02X %02X %02X %02X %02X %02X",
terminalfd, r, cpr[0], cpr[1], cpr[2], cpr[3], cpr[4], cpr[5]);
}
#endif /* !CURSOR_ANSWERBACK_BYTE_BY_BYTE */
ypos = (int) pv_getnum_count(cpr + 2, false);
if (0 != tcsetattr(terminalfd, TCSANOW | TCSAFLUSH, &old_tty)) {
debug("%s: %s", "tcsetattr (2) failed", strerror(errno));
}
debug("%s: %d", "ypos", ypos);
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(pvstate_t state, 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, (int) 'p');
if (-1 == key) {
debug("%s: %s\n", "ftok failed", strerror(errno));
return 1;
}
pv_crs_lock(state, terminalfd);
if (!state->control.cursor) {
debug("%s", "early return - cursor has been disabled");
return 1;
}
state->cursor.shmid = shmget(key, sizeof(struct pvcursorstate_s), 0600 | IPC_CREAT);
if (state->cursor.shmid < 0) {
debug("%s: %s", "shmget failed", strerror(errno));
pv_crs_unlock(state, terminalfd);
return 1;
}
/*@-nullpass@ */
/* splint doesn't know shmaddr can be NULL */
state->cursor.shared = shmat(state->cursor.shmid, NULL, 0);
/*@+nullpass@ */
pv_crs_ipccount(state);
/*
* 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 and with an initial false value for
* the TOSTOP-added flag.
*/
if (state->cursor.pvcount < 2) {
state->cursor.y_start = pv_crs_get_ypos(terminalfd);
state->cursor.shared->y_topmost = state->cursor.y_start;
state->cursor.shared->tty_tostop_added = false;
state->cursor.y_lastread = state->cursor.y_start;
debug("%s", "we are the first to attach");
}
state->cursor.y_offset = state->cursor.pvcount - 1;
if (state->cursor.y_offset < 0)
state->cursor.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 (state->cursor.pvcount > 1) {
state->cursor.y_start = state->cursor.shared->y_topmost;
state->cursor.y_lastread = state->cursor.y_start;
debug("%s: %d", "not the first to attach - got top y", state->cursor.y_start);
}
pv_crs_unlock(state, terminalfd);
return 0;
}
#endif /* HAVE_IPC */
/*
* Initialise the terminal for cursor positioning.
*/
void pv_crs_init(pvstate_t state)
{
char *ttyfile;
int terminalfd;
state->cursor.lock_fd = -2;
state->cursor.lock_file[0] = '\0';
if (!state->control.cursor)
return;
debug("%s", "init");
ttyfile = ttyname(STDERR_FILENO);
if (NULL == ttyfile) {
debug("%s: %s", "disabling cursor positioning because ttyname failed", strerror(errno));
state->control.cursor = false;
return;
}
terminalfd = open(ttyfile, O_RDWR); /* flawfinder: ignore */
/*
* flawfinder rationale: the file we open won't be truncated but
* could be corrupted by writes attempting to get the current Y
* position; but we get the filename from ttyname() and it could be
* a symbolic link, so we can't do much more than trust it.
*/
if (terminalfd < 0) {
pv_error(state, "%s: %s: %s", _("failed to open terminal"), ttyfile, strerror(errno));
state->control.cursor = false;
return;
}
#ifdef HAVE_IPC
if (pv_crs_ipcinit(state, ttyfile, terminalfd) != 0) {
debug("%s", "ipcinit failed, setting noipc flag");
state->cursor.noipc = true;
}
/*
* If we have already set the terminal TOSTOP attribute, set the
* flag in shared memory to let the other instances know.
*/
if ((!state->cursor.noipc) && (1 == state->flag.clear_tty_tostop_on_exit) && (NULL != state->cursor.shared)) {
debug("%s", "propagating local clear_tty_tostop_on_exit true value to shared tty_tostop_added flag");
state->cursor.shared->tty_tostop_added = true;
}
/*
* 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 (state->cursor.noipc) {
#else /* ! HAVE_IPC */
if (1) {
#endif /* HAVE_IPC */
/*
* Get current cursor position + 1.
*/
pv_crs_lock(state, terminalfd);
state->cursor.y_start = pv_crs_get_ypos(terminalfd);
/*
* Move down a line while the terminal is locked, so that
* other processes in the pipeline will get a different
* initial ypos.
*/
if (state->cursor.y_start > 0)
pv_tty_write(state, "\n", 1);
pv_crs_unlock(state, terminalfd);
if (state->cursor.y_start < 1)
state->control.cursor = 0;
}
(void) close(terminalfd);
}
#ifdef HAVE_IPC
/*
* Set the "we need to reinitialise cursor positioning" flag.
*/
void pv_crs_needreinit(pvstate_t state)
{
state->cursor.needreinit += 2;
if (state->cursor.needreinit > 3)
state->cursor.needreinit = 3;
}
#endif
#ifdef HAVE_IPC
/*
* Reinitialise the cursor positioning code (called if we are backgrounded
* then foregrounded again).
*/
static void pv_crs_reinit(pvstate_t state)
{
debug("%s", "reinit");
if (1 == state->flag.suspend_stderr) {
debug("%s", "reinit abandoned - stderr is suspended");
return;
}
pv_crs_lock(state, STDERR_FILENO);
state->cursor.needreinit--;
if (state->cursor.y_offset < 1)
state->cursor.needreinit = 0;
if (state->cursor.needreinit > 0) {
pv_crs_unlock(state, STDERR_FILENO);
return;
}
debug("%s", "reinit full");
state->cursor.y_start = pv_crs_get_ypos(STDERR_FILENO);
if ((state->cursor.y_offset < 1) && (NULL != state->cursor.shared)) {
state->cursor.shared->y_topmost = state->cursor.y_start;
}
state->cursor.y_lastread = state->cursor.y_start;
pv_crs_unlock(state, STDERR_FILENO);
}
#endif
/*
* Output a single-line update (\0-terminated), using the ECMA-48 CSI "CUP"
* sequence to move the cursor to the correct position to do so.
*/
void pv_crs_update(pvstate_t state, const char *output_line)
{
char cup_cmd[32]; /* flawfinder: ignore */
size_t cup_cmd_length, output_line_length;
int y;
/*
* flawfinder rationale: the "cup_cmd" buffer is always zeroed
* before each use, and is only written to by pv_snprintf() bounded
* by its size; also, that function explictly zero-terminates the
* string it writes. The write() call is also bounded.
*/
output_line_length = strlen(output_line); /* flawfinder: ignore */
/* flawfinder - output_line is explictly expected to be \0-terminated. */
#ifdef HAVE_IPC
if (!state->cursor.noipc) {
if (state->cursor.needreinit > 0)
pv_crs_reinit(state);
pv_crs_ipccount(state);
if (NULL != state->cursor.shared) {
if (state->cursor.y_lastread != state->cursor.shared->y_topmost) {
state->cursor.y_start = state->cursor.shared->y_topmost;
state->cursor.y_lastread = state->cursor.y_start;
}
}
if (state->cursor.needreinit > 0)
return;
}
#endif /* HAVE_IPC */
y = state->cursor.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 (((state->cursor.y_start + state->cursor.pvmax) > (int) (state->control.height))
&& (!state->cursor.noipc)
) {
int offs;
offs = ((state->cursor.y_start + state->cursor.pvmax) - state->control.height);
state->cursor.y_start -= offs;
if (state->cursor.y_start < 1)
state->cursor.y_start = 1;
debug("%s: %d", "scroll offset", offs);
/*
* Scroll the screen if we're the first `pv'.
*/
if (0 == state->cursor.y_offset) {
pv_crs_lock(state, STDERR_FILENO);
memset(cup_cmd, 0, sizeof(cup_cmd));
(void) pv_snprintf(cup_cmd, sizeof(cup_cmd), "\033[%u;1H", state->control.height);
cup_cmd_length = strlen(cup_cmd); /* flawfinder: ignore */
pv_tty_write(state, cup_cmd, cup_cmd_length);
for (; offs > 0; offs--) {
pv_tty_write(state, "\n", 1);
}
pv_crs_unlock(state, STDERR_FILENO);
debug("%s", "we are the first - scrolled screen");
}
}
if (!state->cursor.noipc)
y = state->cursor.y_start + state->cursor.y_offset;
#endif /* HAVE_IPC */
/*
* Keep the Y co-ordinate within sensible bounds, so we can never
* overflow the "cup_cmd" buffer.
*/
if ((y < 1) || (y > 999999))
y = 1;
memset(cup_cmd, 0, sizeof(cup_cmd));
(void) pv_snprintf(cup_cmd, sizeof(cup_cmd), "\033[%d;1H", y);
cup_cmd_length = strlen(cup_cmd); /* flawfinder: ignore */
/*
* flawfinder rationale: "cup_cmd" is only written to by
* pv_snprintf(), which explicitly ensures that the string will be
* \0-terminated, and the buffer is zeroed beforehand so if
* pv_snprintf() fails to run the string will be of zero length.
*/
pv_crs_lock(state, STDERR_FILENO);
pv_tty_write(state, cup_cmd, cup_cmd_length);
pv_tty_write(state, output_line, output_line_length);
pv_crs_unlock(state, STDERR_FILENO);
}
/*
* Reposition the cursor to a final position.
*/
void pv_crs_fini(pvstate_t state)
{
char cup_cmd[32]; /* flawfinder: ignore */
unsigned int y;
/* flawfinder - "cup_cmd" is zeroed, and only written by pv_snprintf() */
debug("%s", "fini");
y = (unsigned int) (state->cursor.y_start);
#ifdef HAVE_IPC
if ((state->cursor.pvmax > 0) && (!state->cursor.noipc))
y += state->cursor.pvmax - 1;
#endif /* HAVE_IPC */
if (y > state->control.height)
y = state->control.height;
/*
* Absolute bounds check.
*/
if ((y < 1) || (y > 999999))
y = 1;
memset(cup_cmd, 0, sizeof(cup_cmd));
(void) pv_snprintf(cup_cmd, sizeof(cup_cmd), "\033[%u;1H\n", y);
pv_crs_lock(state, STDERR_FILENO);
pv_tty_write(state, cup_cmd, strlen(cup_cmd)); /* flawfinder: ignore */
/* flawfinder - pv_snprintf() always \0-terminates (see above). */
#ifdef HAVE_IPC
/*
* If any other "pv -c" instances have set the terminal TOSTOP
* attribute, set our local flag so pv_sig_fini() will know about
* it.
*/
if ((!state->cursor.noipc) && (NULL != state->cursor.shared) && state->cursor.shared->tty_tostop_added) {
if (0 == state->flag.clear_tty_tostop_on_exit) {
debug("%s",
"propagating shared tty_tostop_added true value to local clear_tty_tostop_on_exit flag");
state->flag.clear_tty_tostop_on_exit = 1;
}
}
pv_crs_ipccount(state);
if (NULL != state->cursor.shared) {
(void) shmdt(state->cursor.shared);
}
state->cursor.shared = NULL;
/*
* If we are the last instance detaching from the shared memory,
* delete it so it's not left lying around.
*/
if (state->cursor.pvcount < 2) {
struct shmid_ds shm_buf;
memset(&shm_buf, 0, sizeof(shm_buf));
(void) shmctl(state->cursor.shmid, IPC_RMID, &shm_buf);
}
#endif /* HAVE_IPC */
pv_crs_unlock(state, STDERR_FILENO);
if (state->cursor.lock_fd >= 0) {
(void) close(state->cursor.lock_fd);
/*
* We can get away with removing this on exit because all
* the other PVs will be finishing at the same sort of time.
*/
(void) remove(state->cursor.lock_file);
}
}