Skip to content

Commit

Permalink
Get signal handling into the land of defined behavior
Browse files Browse the repository at this point in the history
As part of that also:
- Extract function `redraw_screen`
- Use `select(2)` to wait until stdin can be read without blocking
- Increment n before putting new data in, not after, to prevents
  redrawing issues with a rightmost empty column.
- Fix handling of sigwinch while in "waiting for stdin" mode:
  would jump into a broken plot display previously
- Fix handling of sigwinch while in "stdin closed" mode:
  would produce empty new columns on the right side for every
  signal received.
  • Loading branch information
hartwork committed Nov 17, 2023
1 parent fb7b99d commit e022751
Showing 1 changed file with 197 additions and 59 deletions.
256 changes: 197 additions & 59 deletions ttyplot.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
// ttyplot: a realtime plotting utility for terminal with data input from stdin
// Copyright (c) 2018 by Antoni Sawicki
// Copyright (c) 2019-2023 by Google LLC
// Copyright (c) 2023 by Sebastian Pipping
// Apache License 2.0
//

#include <assert.h>
#include <ctype.h> // isspace
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
Expand Down Expand Up @@ -45,6 +49,8 @@
#endif

sigset_t sigmsk;
volatile sig_atomic_t sigint_pending = 0;
volatile sig_atomic_t sigwinch_pending = 0;
chtype plotchar, max_errchar, min_errchar;
time_t t1,t2,td;
struct tm *lt;
Expand All @@ -56,7 +62,7 @@ double cval1=FLT_MAX, pval1=FLT_MAX;
double cval2=FLT_MAX, pval2=FLT_MAX;
double min1=FLT_MAX, max1=FLT_MIN, avg1=0;
double min2=FLT_MAX, max2=FLT_MIN, avg2=0;
int width=0, height=0, n=0, r=0, v=0, c=0, rate=0, two=0, plotwidth=0, plotheight=0;
int width=0, height=0, n=-1, r=0, v=0, c=0, rate=0, two=0, plotwidth=0, plotheight=0;
const char *verstring = "https://github.com/tenox7/ttyplot " VERSION_STR;

void usage(void) {
Expand Down Expand Up @@ -174,14 +180,6 @@ void paint_plot(void) {
erase();
gethw();

if(!window_big_enough_to_draw()) {
show_window_size_error();
sigprocmask(SIG_BLOCK, &sigmsk, NULL);
refresh();
sigprocmask(SIG_UNBLOCK, &sigmsk, NULL);
return;
}

plotheight=height-4;
plotwidth=width-4;
if(plotwidth>=(int)((sizeof(values1)/sizeof(double))-1))
Expand Down Expand Up @@ -225,36 +223,77 @@ void paint_plot(void) {
mvaddstr(0, (width/2)-(strlen(title)/2), title);

move(0,0);
sigprocmask(SIG_BLOCK, &sigmsk, NULL);
refresh();
sigprocmask(SIG_UNBLOCK, &sigmsk, NULL);
}

void resize(int signum) {
(void)signum;
sigprocmask(SIG_BLOCK, &sigmsk, NULL);
endwin();
refresh();
clear();
sigprocmask(SIG_UNBLOCK, &sigmsk, NULL);
signal(SIGWINCH, resize);
paint_plot();
sigwinch_pending = 1;
}

void finish(int signum) {
(void)signum;
sigprocmask(SIG_BLOCK, &sigmsk, NULL);
curs_set(FALSE);
echo();
sigint_pending = 1;
}

// Finds the end of a record so that we know how much input to
// feed to `sscanf` right after. This scanner matches the `scanf` patterns
// "%lf %lf" and "%lf" as used in `main` plus one or more characters of
// trailing whitespace. In contrast to `sscanf`, it considers any group
// of non-whitespace to be a floating point number in text form.
//
// Returns `NULL` when a record is not complete.
//
char * find_record_end(char * record_start) {
char * walker = record_start;

while (isspace(*walker)) walker++;

if (*walker == '\0')
return NULL;

while ((*walker != '\0') && ! isspace(*walker)) walker++; // i.e. a supposed double

if (two) {
while (isspace(*walker)) walker++;

if (*walker == '\0')
return NULL;

while ((*walker != '\0') && ! isspace(*walker)) walker++; // i.e. a supposed double
}

if (*walker == '\0')
return NULL;

return walker;
}

void redraw_screen(const char * errstr, bool block_signals) {
if (window_big_enough_to_draw()) {
paint_plot();

if (errstr != NULL) {
show_all_centered(errstr);
} else if (v < 1) {
show_all_centered("waiting for data from stdin");
}
} else {
show_window_size_error();
}

if (block_signals)
sigprocmask(SIG_BLOCK, &sigmsk, NULL);

refresh();
endwin();
sigprocmask(SIG_UNBLOCK, &sigmsk, NULL);
exit(0);

if (block_signals)
sigprocmask(SIG_UNBLOCK, &sigmsk, NULL);
}

int main(int argc, char *argv[]) {
int i;
char *errstr;
char *errstr = NULL;
bool stdin_is_open = true;
int cached_opterr;
const char *optstring = "2rc:e:E:s:m:M:t:u:vh";
int show_ver;
Expand Down Expand Up @@ -365,47 +404,151 @@ int main(int argc, char *argv[]) {
erase();
refresh();
gethw();
if (window_big_enough_to_draw()) {
show_all_centered("waiting for data from stdin");
} else {
show_window_size_error();
}
refresh();

redraw_screen(errstr, /* block_signals= */ false);

signal(SIGWINCH, resize);
signal(SIGINT, finish);
sigemptyset(&sigmsk);
sigaddset(&sigmsk, SIGWINCH);

char input_buf[4096] = "";
size_t input_len = 0;

while(1) {
if (sigint_pending) {
break;
}

if (sigwinch_pending) {
sigwinch_pending = 0;

sigprocmask(SIG_BLOCK, &sigmsk, NULL);
endwin();
initscr();
clear();
refresh();
sigprocmask(SIG_UNBLOCK, &sigmsk, NULL);

gethw();
}

// Block until (a) we receive a signal or (b) stdin can be read without blocking
// to reduce use of CPU and power in the absense of both
if (stdin_is_open) {
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(STDIN_FILENO, &read_fds);
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 100 * 1000; // i.e. 100 milliseconds

const int select_ret = select(STDIN_FILENO + 1, &read_fds, NULL, NULL, &timeout);

const bool signal_received = ((select_ret == -1) && (errno == EINTR));

if (signal_received) {
continue; // i.e. skip right to signal handling
}

const bool stdin_can_be_read_without_blocking = ((select_ret == 1) && FD_ISSET(STDIN_FILENO, &read_fds));

// Read as much from stdin as we can (first read after select is non-blocking)
if (stdin_can_be_read_without_blocking) {
char * const read_target = input_buf + input_len;
const size_t max_bytes_to_read = sizeof(input_buf) - (input_len + 1 /* terminator */);
const ssize_t bytes_read_or_error = read(STDIN_FILENO, read_target, max_bytes_to_read);

if (bytes_read_or_error > 0) {
input_len += bytes_read_or_error;

// Last resort: truncate existing input if input line ever is
// too long
if (input_len >= sizeof(input_buf) - 1) {
input_len = 0;
}

assert(input_len < sizeof(input_buf));
input_buf[input_len] = '\0';
} else {
assert(bytes_read_or_error <= 0);
if (bytes_read_or_error == 0) {
close(STDIN_FILENO);
errstr = "input stream closed";
} else {
assert(bytes_read_or_error == -1);
if ((errno == EINTR) || (errno == EAGAIN)) {
continue;
}
errstr = strerror(errno);
}
stdin_is_open = false;
}
}
}

char * record_end = find_record_end(input_buf);

// Is the is the last input ever, force a parse of even an incomplete
// record, or we'd lose the last record ever.
if ((record_end == NULL) && ! stdin_is_open) {
record_end = input_buf + input_len;
}

const bool have_complete_record = record_end != NULL;

if (! have_complete_record) {
time(&t1); // to animate the clock display
redraw_screen(errstr, /* block_signals= */ true);
continue;
}

// Extract values from record
double d1 = 0.0;
double d2 = 0.0;
const char original_record_end_char = *record_end;
*record_end = '\0';
if(two)
r=scanf("%lf %lf", &values1[n], &values2[n]);
r = sscanf(input_buf, "%lf %lf", &d1, &d2);
else
r=scanf("%lf", &values1[n]);
r = sscanf(input_buf, "%lf", &d1);
*record_end = original_record_end_char;

// If the input is considered "an all garbage line", skip the rest of it.
// The idea is to remain compatibility with previous releases which
// has blocking implemenation "while(getchar()!='\n');" for this.
if (r < 1) {
char * first_line_feed = strchr(record_end, '\n');
if (first_line_feed) {
record_end = first_line_feed;
}
}

// Drop the record we just processed from the input buffer
const size_t record_len = record_end - input_buf;
if (input_len > 0) {
assert(input_len > record_len);
const size_t bytes_to_move = input_len - (record_len + 1);
memmove(input_buf, record_end + 1, bytes_to_move);
input_len = bytes_to_move;
input_buf[input_len] = '\0';
}

v++;

if(r==0) {
while(getchar()!='\n');
if (r < 1) {
time(&t1); // to animate the clock display
redraw_screen(errstr, /* block_signals= */ true);
continue;
}
else if(r<0) {
if (errno==EINTR)
continue;
else if(errno==0)
errstr = "input stream closed";
else
errstr = strerror(errno);
if (window_big_enough_to_draw()) {
show_all_centered(errstr);
} else {
show_window_size_error();
}
sigprocmask(SIG_BLOCK, &sigmsk, NULL);
refresh();
sigprocmask(SIG_UNBLOCK, &sigmsk, NULL);
pause();
}

if (n < plotwidth - 1)
n++;
else
n=0;

values1[n] = d1;
values2[n] = d2;

if(values1[n] < 0)
values1[n] = 0;
Expand Down Expand Up @@ -446,12 +589,7 @@ int main(int argc, char *argv[]) {
time(&t1);
}

paint_plot();

if(n<(int)((plotwidth)-1))
n++;
else
n=0;
redraw_screen(errstr, /* block_signals= */ true);
}

endwin();
Expand Down

0 comments on commit e022751

Please sign in to comment.