diff options
author | Kaz Kylheku <kaz@kylheku.com> | 2022-04-26 22:42:43 -0700 |
---|---|---|
committer | Kaz Kylheku <kaz@kylheku.com> | 2022-04-26 22:42:43 -0700 |
commit | 66ba7fdd8752bc46c9cf14605226f044d259ab05 (patch) | |
tree | 8412216d0e33ac15239a9db9befc991741188988 | |
parent | 9c38bc2efd72beb59ba686a5e323bc7cc4fedcf1 (diff) | |
download | pw-66ba7fdd8752bc46c9cf14605226f044d259ab05.tar.gz pw-66ba7fdd8752bc46c9cf14605226f044d259ab05.tar.bz2 pw-66ba7fdd8752bc46c9cf14605226f044d259ab05.zip |
New project, Pipe Watch.
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | pw.1 | 248 | ||||
-rw-r--r-- | pw.c | 365 |
3 files changed, 615 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b9ff47b --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +CFLAGS ?= -g -O2 -W -Wall -std=c99 +pw: @@ -0,0 +1,248 @@ +.\" pw: pipe watch +.\" Copyright 2022 Kaz Kylheku <kaz@kylheku.com> +.\" +.\" BSD-2 License +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions are met: +.\" +.\" 1. Redistributions of source code must retain the above copyright notice, +.\" this list of conditions and the following disclaimer. +.\" +.\" 2. Redistributions in binary form must reproduce the above copyright notice, +.\" this list of conditions and the following disclaimer in the documentation +.\" and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +.\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +.\" LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +.\" CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +.\" SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +.\" INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +.\" CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +.\" ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +.\" POSSIBILITY OF SUCH DAMAGE. +.TH PW 1 "26 April 2022" "Utility Commands" "Pipe Watch" + +.SH NAME +pw \- Pipe Watch: monitor recent lines of output from pipe + +.SH SYNOPSIS +command | pw [-i interval] [-l interval] [-n number-of-lines] [-d] + +.SH DESCRIPTION +.I pw +stands for Pipe Watch. This is a utility which continuously reads textual input +from a pipe or pipe-like source, and maintains a dynamic display of the +most recently read +.I N +lines. + +If +.I pw +is invoked such that its standard input is a TTY, it simply reads lines +and prints them in its characteristic way, with control characters replaced by +caret codes, until end-of-file is encountered. Long lines aren't clipped, +and there is no interactive mode. + +The intended use of +.I pw +is that its standard input is a pipe, such as the output of another command, +or a pipe-like device such as a socket or whatever. +In this situation, +.I pw +expects to be executed in a TTY session in which a +.B /dev/tty +device can be opened, for the purposes of obtaining interactive input. +The remaining description pertains to this interactive mode. + +In interactive mode, +.I pw +simultaneously monitors its standard input for the arrival of new data, +as well as the TTY for interactive commands. +Lines from standard input are placed into a FIFO buffer. +While the FIFO buffer is not yet full, lines are displayed immediately. +After the FIFO buffer fills up with the specified number of lines +(controlled by the +.B -n +option) then +.I pw +transitions into a mode in which, old lines are bumped from the tail of the +FIFO as new ones are added to the head, and refresh operations are required in +order to display the current FIFO contents. + +The display only refreshes with the latest FIFO data when +.IP 1. +there is some keyboard activity from the terminal; or +.IP 2. +when the interval period has expired +without new input having been seen; or else +.IP 3. +whenever the long period elapses. +.PP + +In other words, while the pipe is spewing, and there is +no keyboard input, the display is updated infrequently, only according to the +long interval. + +The display is also updated when standard input indicates end-of-data. +In this situation, +.I pw +terminates, unless the +.B -d +(do not quit) option has been specified. In this situation, +.I pw +stays in interactive mode. The the end-of-data status is +indicated by the string +.B EOF +being displayed in the status line after the data. + +.SH DISPLAY + +Lines displayed by +.I pw +are trimmed not to exceed the number of terminal columns. When a line is +trimmed, it is terminated by the +.B > +character to indicate that there are more characters. The display may be +scrolled interactively to read the long lines. + +The ASCII DEL character (127) is displayed as +.B ^? +and ASCII control characters are displayed as +.BR ^@ , +.BR ^A ", ..." +.BR ^Z ", ..." +.BR ^_ . +All other characters are sent to the display as-is. No attempt is made +to account for the width of East Asian Unicode characters, and such. + +When the display is scrolled horizontally, the +.B > +character appears at the start of each line to indicate this state. + +.SH COMMANDS + +When +.I pw +enters closed loop operation, the following single-key commands are +available: + +.IP "\fBq\fP, \fBCtrl-C\fP" +Quit the program. + +.IP "\fBl\fP, \fILeft Arrow\fP" +Scroll the display to the left. + +.IP "\fBh\fP, \fIRight Arrow\fP" +Scroll the display to the right. + +.IP "\fB0\fP, \fIHome\fP" +Reset scroll to first column. + +.IP \fISpace\fP +Suspend the entry of lines of new lines of input into the display FIFO. +In suspended mode, input continues to be read from the pipe, but is discarded. +The status string +.B SUSPENDED +is displayed below the data. + +.IP \fIEnter\fP +Cancel suspended mode, and resume adding data to the FIFO. The +.B SUSPENDED +status string disappears. If the status is already +.B EOF +then suspend mode cannot be entered. + +.SH OPTIONS + +.IP "\fB-i\fP \fIreal\fP" +Set the poll interval to number of seconds specified by +.IR real . +This is is a floating-point constant such as 3 or 1.5. Exponential notation +such as 5E-1 (equivalent to 0.5) is permitted. +This interval determines the input poll timeout of +.IR pw 's +input processing loop. If no data arrives from the primary input, or +from the TTY for this amount of time, a timeout occurs, and the display is +refreshed if the FIFO has changed since the last refresh. +The default poll interval is 1s. + +.IP "\fB-l\fP \fIreal\fP" +Set the long update interval to number of seconds specified by +.IR real . +The format is the same as +.BR -i , +and the default value is 10s. Every time the long update interval passes, +the display is updated, if the FIFO has changed since the last update. +This happens even when input is arriving too rapidly to permit the poll +timeout to take place. The purpose of the long interval is to ensure that +there are updates even when the data source continuously and rapidly produces +data, and there is no TTY input activity. The long interval should be at +least several times longer than the short interval. The granularity of the +timing of the long interval updates depends on the poll interval; in the +absence of TTY input, +.I pw +will not perform any display updates more often that the poll interval, +even if the long interval is made smaller than the poll interval. + +.IP "\fB-n\fP \fIinteger\fP" +Set the number of lines +.I N +to the specified decimal integer value. The default value is 15. +This value must be positive, and is clipped to the number of display lines +available in the terminal, less one. + +.IP \fB-d\fP +Disable auto-quit: when no more input is available, instead of updating +the display one last time and terminating, +.I pw +will updating the status to +.B EOF +and staying in the interactive mode. This is useful when the last portion +of the input is of interest, and contains long lines that require +horizontal scrolling. + +.SH TERMINATION STATUS + +If +.I pw +reaches EOF on standard input without encountering a read error, then its +termination status will be successful. This is regardless of whether it +automatically quits, or whether it stays in interactive mode and then quits due +to a quit command. If the data ends prematurely due to a read error, or if the +program is asked to quit before all of the data has been read, then +unsuccessful termination status will be indicated. + +Incorrect usage, such as nonexistent options or bad arguments to options, +result in a diagnostic on standard error and an unsuccessful termination. + +Unexpected conditions like out of memory result in abnormal termination (abort). + +.SH BUGS + +This was written over the course of a couple of hours, and tested only +interactively. + +The program doesn't respond to window size changes. The display format, such +as the handling of control characters, is hard-coded. + +The program uses hard-coded ANSI sequences, so it doesn't support interesting +old terminals. On the other hand, it carries no dependency on any +terminal abstraction library/data. + +The intervals and number of lines cannot be dynamically adjusted. + +Suspend mode is confusing; it doesn't necessarily freeze what is on the +screen. + +There is no support for unwrapping long lines, which would be useful for +copy and paste. + +.SH AUTHOR +Kaz Kylheku <kaz@kylheku.com> + +.SH COPYRIGHT +Copyright 2022, BSD2 License. @@ -0,0 +1,365 @@ +#include <stdio.h> +#include <stdlib.h> +#include <ctype.h> +#include <string.h> +#include <limits.h> +#include <stdarg.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/poll.h> +#include <termios.h> +#include <sys/ioctl.h> +#include <sys/time.h> + +enum status_flags { + stat_dirty = 1, + stat_eof = 2, + stat_susp = 4 +}; + +static void panic(const char *fmt, ...) +{ + va_list vl; + va_start (vl, fmt); + vfprintf(stderr, fmt, vl); + abort(); +} + +static void error(const char *fmt, ...) +{ + va_list vl; + va_start (vl, fmt); + vfprintf(stderr, fmt, vl); + exit(EXIT_FAILURE); +} + +static char *addch(char *line, size_t len, int ch) +{ + if (len + 2 > len) { + char *nline = realloc(line, len + 2); + + if (nline == 0) + panic("out of memory"); + + nline[len] = ch; + nline[len + 1] = 0; + return nline; + } + + panic("line overflow"); + abort(); +} + +static char *getln(FILE *stream) +{ + char *line = 0; + size_t len = 0; + + for (;;) { + int ch = getc(stream); + + if (ch == EOF) + return line; + + if (ch == '\n') { + if (line) + return line; + return addch(line, len, 0); + } + + if (ch == 127) { + line = addch(line, len++, '^'); + line = addch(line, len++, '?'); + } else if (ch < 32) { + line = addch(line, len++, '^'); + line = addch(line, len++, ch + 64); + } else { + line = addch(line, len++, ch); + } + } +} + +static void usage(const char *name) +{ + fprintf(stderr, + "\nUsage: %s [options]\n\n" + "-i realnum poll interval (s)\n" + "-l realnum long update interval (s)\n" + "-n integer display size (# of lines)\n" + "-d do not quit on end-of-input\n\n" + "For a full description, see the manual page.\n\n", + name); + exit(EXIT_FAILURE); +} + +static void drawline(const char *line, int hpos, int columns) +{ + size_t len = strlen(line); + + if ((size_t) hpos <= len) { + if (hpos) { + line += hpos; + len -= hpos; + putchar('>'); + } + if (len < (size_t) columns - 1) { + puts(line); + } else { + for (int i = 0; i < columns - 2; i++) + putchar(line[i]); + puts("<"); + } + } else { + puts(">"); + } +} + +static void drawstatus(unsigned stat) +{ + if ((stat & (stat_eof | stat_susp))) { + if ((stat & stat_eof)) + printf("EOF "); + if ((stat & stat_susp)) + printf("SUSPENDED "); + fflush(stdout); + } +} + +static void redraw(char **circbuf, int nlines, int hpos, + int columns, unsigned stat) +{ + printf("\r\033[%dA\033[J", nlines); + for (int i = 0; i < nlines; i++) + drawline(circbuf[i], hpos, columns); + drawstatus(stat); +} + +static void clear_cur_line() +{ + printf("\r\033[J"); +} + +int main(int argc, char **argv) +{ + char *line; + FILE *tty = fopen("/dev/tty", "r+"); + int maxlines = 15, nlines = 0; + int poll_interval = 1000; + int long_interval = 10000; + int opt; + int fd = fileno(stdin); + int ttyfd = tty ? fileno(tty) : -1; + char **circbuf; + struct termios tty_saved, tty_new; + struct winsize ws = { 0 }; + int columns = 80; + enum kbd_state { kbd_cmd, kbd_esc, kbd_bkt, kbd_exit }; + int auto_quit = 1; + int exit_status = EXIT_FAILURE; + + if (fd < 0) + panic("unable to obtain input file descriptor"); + + if (ttyfd < 0) + panic("unable to open /dev/tty"); + + while ((opt = getopt(argc, argv, "ni:l:d")) != -1) { + switch (opt) { + case 'n': + maxlines = atoi(optarg); + break; + case 'i': case 'l': + { + double interval = atof(optarg) * 1000; + if (interval > (double) INT_MAX) + error("maximum interval is %f\n", INT_MAX / 1000.0); + if (opt == 'i') + poll_interval = interval; + else + long_interval = interval; + } + break; + case 'd': + auto_quit = 0; + break; + default: + usage(argv[0]); + } + } + + if (maxlines <= 0 || maxlines > 1000) + error("%d is an unreasonable number of lines to display", maxlines); + + if ((circbuf = malloc(sizeof *circbuf * maxlines)) == 0) + panic("out of memory"); + + if (isatty(fd)) { + while ((line = getln(stdin))) { + puts(line); + free(line); + } + + return 0; + } + + if (ioctl(ttyfd, TIOCGWINSZ, &ws) == 0 && ws.ws_row != 0) { + if (maxlines >= ws.ws_row) + maxlines = ws.ws_row - 1; + columns = ws.ws_col; + } + + if (tcgetattr(ttyfd, &tty_saved) < 0) + panic("unable to get TTY parameters"); + + tty_new = tty_saved; + + tty_new.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | + INLCR | IGNCR | ICRNL); + tty_new.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + + tty_new.c_cc[VMIN] = 1; + tty_new.c_cc[VTIME] = 0; + + if (tcsetattr(ttyfd, TCSANOW, &tty_new) < 0) + panic("unable to set TTY parameters"); + + setvbuf(tty, NULL, _IONBF, 0); + + for (unsigned stat = stat_dirty, hpos = 0, kbd_state = kbd_cmd, lasttime = ~0U; + kbd_state != kbd_exit ;) + { + struct pollfd pe[2] = { + { .fd = ttyfd, .events = POLLIN | POLLHUP | POLLERR }, + { .fd = fd, .events = POLLIN | POLLHUP | POLLERR }, + }; + + { + struct timeval tv; + unsigned now; + + gettimeofday(&tv, NULL); + now = (((unsigned) tv.tv_sec)%1000000)*1000 + tv.tv_usec/1000; + if (lasttime == ~0U || now - lasttime > (unsigned) long_interval) { + if ((stat & stat_dirty) && nlines == maxlines) + redraw(circbuf, nlines, hpos, columns, stat); + lasttime = now; + stat &= ~stat_dirty; + } + } + + if (poll(pe, ((stat & stat_eof)) ? 1 : 2, poll_interval) <= 0) { + if ((stat & stat_dirty) && nlines == maxlines) { + redraw(circbuf, nlines, hpos, columns, stat); + stat &= ~stat_dirty; + } + kbd_state = kbd_cmd; + } else { + if ((stat & stat_eof) == 0 && pe[1].revents) { + if ((line = getln(stdin))) { + if (nlines == maxlines) { + if ((stat & stat_susp)) { + free(line); + } else { + free(circbuf[0]); + memmove(circbuf, circbuf + 1, (nlines - 1) * sizeof *circbuf); + circbuf[nlines - 1] = line; + stat |= stat_dirty; + } + } else { + circbuf[nlines++] = line; + clear_cur_line(); + drawline(line, hpos, columns); + drawstatus(stat); + } + } else { + if (auto_quit) + kbd_state = kbd_exit; + else + stat |= stat_eof; + redraw(circbuf, nlines, hpos, columns, stat); + stat |= stat_eof; + stat &= ~stat_dirty; + if (!ferror(stdin)) + exit_status = 0; + continue; + } + } + + if ((pe[0].revents)) { + int ch = getc(tty); + + fakecmd: + switch (kbd_state) { + case kbd_cmd: + switch (ch) { + case 'q': case 3: + kbd_state = kbd_exit; + if ((stat & (stat_eof | stat_susp))) { + clear_cur_line(); + fflush(stdout); + } + break; + case 'h': + if (hpos >= 8) { + hpos -= 8; + stat |= stat_dirty; + } + break; + case 'l': + if (hpos < 10000) { + hpos += 8; + stat |= stat_dirty; + } + break; + case '0': + hpos = 0; + stat |= stat_dirty; + break; + case ' ': + if ((stat & stat_eof) == 0) + stat |= (stat_dirty | stat_susp); + break; + case 13: + stat &= ~stat_susp; + stat |= stat_dirty; + break; + case 27: + kbd_state = kbd_esc; + break; + } + break; + case kbd_esc: + kbd_state = kbd_cmd; + if (ch == '[') + kbd_state = kbd_bkt; + break; + case kbd_bkt: + kbd_state = kbd_cmd; + switch (ch) { + case 'D': + ch = 'h'; + goto fakecmd; + case 'C': + ch = 'l'; + goto fakecmd; + case 'H': + ch = '0'; + goto fakecmd; + } + break; + case kbd_exit: + break; + } + + if ((stat & stat_dirty)) { + redraw(circbuf, nlines, hpos, columns, stat); + stat &= ~stat_dirty; + } + } + } + } + + if (tcsetattr(ttyfd, TCSANOW, &tty_saved) < 0) + panic("unable to restore TTY parameters"); + return exit_status; +} |