Skip to content

Commit

Permalink
Refactor the handling of input data
Browse files Browse the repository at this point in the history
Split the task into separate functions for clarity. Completely handle
every read() within a single loop iteration, even if it yields multiple
data records. The main loop should be as simple as possible, and
strictly follow this pattern:

    forever:
        wait for next event
        handle the events we got
  • Loading branch information
edgar-bonet committed Nov 25, 2023
1 parent 05cb651 commit c6e6244
Showing 1 changed file with 116 additions and 157 deletions.
273 changes: 116 additions & 157 deletions ttyplot.c
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ char title[256]=".: ttyplot :.", unit[64]={0}, ls[256]={0};
double values1[1024]={0}, values2[1024]={0};
double min1=FLT_MAX, max1=FLT_MIN, avg1=0;
double min2=FLT_MAX, max2=FLT_MIN, avg2=0;
int width=0, height=0, n=-1, r=0, v=0, c=0, rate=0, two=0, plotwidth=0, plotheight=0;
int width=0, height=0, n=-1, v=0, c=0, rate=0, two=0, plotwidth=0, plotheight=0;
char *errstr = NULL;
const char *verstring = "https://github.com/tenox7/ttyplot " VERSION_STR;

void usage(void) {
Expand Down Expand Up @@ -289,9 +290,99 @@ void redraw_screen(const char * errstr) {
refresh();
}

// Handle a single value from the input stream.
// Return whether we got a full data record.
int handle_value(double value)
{
static double saved_value;
static int saved_value_valid = 0;

// First value of a 2-value record: save it for later.
if (two && !saved_value_valid) {
saved_value = value;
saved_value_valid = 1;
return 0;
}

// Otherwise we have a full record.
n = (n+1) % plotwidth;
if (two) {
values1[n] = saved_value;
values2[n] = value;
saved_value_valid = 0;
} else {
values1[n] = value;
}
if (rate)
td = derivative(&values1[n], two ? &values2[n] : NULL, &now);
return 1;
}

// Parse the input data. Return the number of bytes consumed.
size_t parse_input(char *buffer, size_t length)
{
static const char delimiters[] = " \t\r\n"; // white space

// Find the last delimiter.
int end;
for (end = length - 1; end >= 0; end--) {
if (strchr(delimiters, buffer[end])) // found
break;
}
if (end < 0) // no delimiter
return 0;
buffer[end] = '\0';

// Tokenize and parse.
int records = 0; // number or records found
char *str = buffer;
char *token;
while ((token = strtok(str, delimiters)) != NULL) {
records += handle_value(strtod(token, NULL));
str = NULL; // tell strtok() to stay on the same string
}
v += records;
if (records > 0)
redraw_screen(errstr);
return end + 1;
}

// Handle an "input ready" event, where only a single read() is guaranteed to not block.
// Return whether the input stream got closed.
int handle_input(void)
{
static char buffer[4096];
static size_t buffer_pos = 0;

// Buffer incoming data.
int bytes_read = read(STDIN_FILENO, buffer + buffer_pos, sizeof buffer - 1 - buffer_pos);
if (bytes_read < 0) { // read error
if (errno == EINTR || errno == EAGAIN) // we should try again later
return 0;
errstr = strerror(errno); // other errors are considered fatal
return 1;
}
if (bytes_read == 0) {
errstr = "input stream closed";
buffer[buffer_pos++] = '\n'; // attempt to extract one last value
parse_input(buffer, buffer_pos);
redraw_screen(errstr); // redraw to display the error message
return 1;
}
buffer_pos += bytes_read;

// Handle this new data.
size_t bytes_consumed = parse_input(buffer, buffer_pos);
if (buffer_pos - bytes_consumed > sizeof buffer / 2)
bytes_consumed += sizeof buffer / 4; // discard excessive garbage
if (bytes_consumed > 0 && bytes_consumed < buffer_pos)
memmove(buffer, buffer + bytes_consumed, buffer_pos - bytes_consumed);
buffer_pos -= bytes_consumed;
return 0;
}

int main(int argc, char *argv[]) {
int i;
char *errstr = NULL;
bool stdin_is_open = true;
int cached_opterr;
const char *optstring = "2rc:e:E:s:m:M:t:u:vh";
Expand Down Expand Up @@ -429,29 +520,9 @@ int main(int argc, char *argv[]) {
signal(SIGWINCH, resize);
signal(SIGINT, finish);

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

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

if (sigwinch_pending) {
sigwinch_pending = 0;

endwin();
initscr();
clear();
refresh();

gethw();

goto redraw_and_continue;
}

// Block until (a) we receive a signal or (b) stdin can be read without blocking
// or (c) timeout expires, in oder to reduce use of CPU and power while idle
// or (c) timeout expires, in order to reduce use of CPU and power while idle
fd_set read_fds;
FD_ZERO(&read_fds);
int select_nfds = 0;
Expand All @@ -464,20 +535,25 @@ int main(int argc, char *argv[]) {
if (tty >= select_nfds)
select_nfds = tty + 1;
}
const bool previous_parse_succeeded = (r == (two ? 2 : 1));
struct timespec timeout;
timeout.tv_sec = 0;
if (previous_parse_succeeded) {
timeout.tv_nsec = 0; // we may have more input pressing, let's not throttle it down
} else {
timeout.tv_nsec = 500 * 1000 * 1000; // <=500 milliseconds for a healthy clock display
}
struct timespec timeout = { .tv_sec = 0, .tv_nsec = 500e6 }; // 500 milliseconds for refreshing the clock
const int select_ret = pselect(select_nfds, &read_fds, NULL, NULL, &timeout, &empty_sigset);

const bool signal_received = ((select_ret == -1) && (errno == EINTR));
gettimeofday(&now, NULL);

// Refresh the clock on timeouts.
if (select_ret == 0)
redraw_screen(errstr);

if (signal_received) {
continue; // i.e. skip right to signal handling
// Handle signals.
if (sigint_pending) {
break;
}
if (sigwinch_pending) {
sigwinch_pending = 0;
endwin();
initscr();
gethw();
redraw_screen(errstr);
}

// Handle user's keystrokes.
Expand All @@ -495,131 +571,14 @@ int main(int argc, char *argv[]) {
}
}

const bool stdin_can_be_read_without_blocking = ((select_ret > 0) && 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";
stdin_is_open = false;
} else {
assert(bytes_read_or_error == -1);
if ((errno != EINTR) && (errno != EAGAIN)) {
errstr = strerror(errno);
stdin_is_open = false;
}
}
}
}

// Extract values from record; note that the record may turn out incomplete
double d1 = 0.0;
double d2 = 0.0;
int record_len = -1;

if(two)
r = sscanf(input_buf, "%lf %lf%*[ \t\r\n]%n", &d1, &d2, &record_len);
else
r = sscanf(input_buf, "%lf%*[ \t\r\n]%n", &d1, &record_len);

// We need to detect and avoid mis-parsing "1.23" as two records "1.2" and "3"
const bool supposedly_complete_record = (r == (two ? 2 : 1));
const bool trailing_whitespace_present = (record_len != -1);

if (supposedly_complete_record && ! trailing_whitespace_present) {
const bool need_more_input = stdin_is_open;
if (need_more_input) {
r = 0; // so that the parse is not mis-classified as a success further up
goto redraw_and_continue;
}

record_len = input_len; // i.e. the whole thing
}

// In order to not get stuck with non-doubles garbage input forever,
// we need to drop input that we know(!) will never parse as doubles later.
if (! supposedly_complete_record && (input_len > 0)) {
char * walker = input_buf;

while (isspace(*walker)) walker++; // skip leading whitespace (if any)

while ((*walker != '\0') && ! isspace(*walker)) walker++; // skip non-double

if (two) {
if (*walker == '\0') {
goto redraw_and_continue;
}

while (isspace(*walker)) walker++; // skip gap whitespace

if (*walker == '\0') {
goto redraw_and_continue;
}

while ((*walker != '\0') && ! isspace(*walker)) walker++; // skip non-double
// Handle input data.
if (select_ret > 0 && FD_ISSET(STDIN_FILENO, &read_fds)) {
int input_closed = handle_input();
if (input_closed) {
close(STDIN_FILENO);
stdin_is_open = false;
}

if (*walker == '\0') {
goto redraw_and_continue;
}

while (isspace(*walker)) walker++; // skip trailing whitespace (if any)

record_len = walker - input_buf; // i.e. how much to drop
}

// Drop the record that we just processed (well-formed or not) from the input buffer
if ((input_len > 0) && (record_len > 0)) {
char * move_source = input_buf + record_len;
const size_t bytes_to_move = input_len - record_len;
memmove(input_buf, move_source, bytes_to_move);
input_len = bytes_to_move;
input_buf[input_len] = '\0';
}

if (! supposedly_complete_record) {
goto redraw_and_continue;
}

v++;

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

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

if(values1[n] < 0)
values1[n] = 0;
if(values2[n] < 0)
values2[n] = 0;

gettimeofday(&now, NULL);
if (rate)
td=derivative(&values1[n], two ? &values2[n] : NULL, &now);

redraw_and_continue:
redraw_screen(errstr);
}

endwin();
Expand Down

0 comments on commit c6e6244

Please sign in to comment.