There are several possibilities, ranging from a huge hack, to proper (but complicated).
The simplest thing is to have the SIGUSR1 from parent reopen standard input to /dev/null. Then, when scanf() fails, instead of complaining and retrying, you can break out of the loop if feof(stdin) is true. Unfortunately, freopen() is not async-signal safe, so this is not a standards (POSIX, in this case) compliant way of doing things.
The standards-compliant way of doing things is to implement your own read input line into a dynamically allocated string -type of function, which detects when the signal handler sets the flag. The flag should also be of volatile sig_atomic_t type, not an int; the volatile in particular tells the compiler that the value may be changed unexpectedly (by the signal handler), so whenever referenced, the compiler must re-read the variable value, instead of remembering it from a previous access. The sig_atomic_t type is an atomic integer type: the process and the signal handler will only ever see either the new, or the old value, never a mix of the two, but might have as small valid range as 0 to 127, inclusive.
Signal delivery to an userspace handler (installed without SA_RESTART) does interrupt a blocking I/O operation (like read or write; in the thread used for signal delivery – you only have one, so that will always be used), but it might occur between the flag check and the scanf(), so in this case, it is not reliable.
The proper solution here is to not use stdin at all, and instead use the low-level <unistd.h> I/O for this. Note that it is imperative to not mix stdin/scanf() and low-level I/O for the same stream. You can safely use printf(), fprintf(stdout, ...), fprintf(stderr, ...), and so on. The reason is that the C library internal stdin stream structure will not be updated correctly by our low-level access, and will be out-of-sync with reality if we mix both (for the same stream).
Here is an example program showing one implementation (licensed under Creative Commons Zero v1.0 International – do as you wish with it, no guarantees though):
// SPDX-License-Identifier: CC0-1.0
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
/* Maximum poll() timeout, in milliseconds, so that done flag is checked often enough.
*/
#ifndef DONE_POLL_INTERVAL_MS
#define DONE_POLL_INTERVAL_MS 100
#endif
static volatile sig_atomic_t done = 0;
static void handle_done(int signum, siginfo_t *info, void *context)
{
/* This silences warnings about context not being used. It does nothing. */
(void)context;
if (signum == SIGUSR1 && info->si_pid == getppid()) {
/* SIGUSR1 is only accepted if it comes from the parent process */
done = 1;
} else {
/* All other signals are accepted from all processes (that have the necessary privileges) */
done = 1;
}
}
static int install_done(const int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&(act.sa_mask));
act.sa_sigaction = handle_done;
act.sa_flags = SA_SIGINFO;
return sigaction(signum, &act, NULL);
}
/* Our own input stream structure type. */
struct input {
int descriptor;
char *data;
size_t size;
size_t head;
size_t tail;
};
/* Associating an input stream with a file descriptor.
Do not mix stdin use and input stream on descriptor STDIN_FILENO!
*/
static int input_use(struct input *const in, const int descriptor)
{
/* Check that the parameters are not obviously invalid. */
if (!in || descriptor == -1) {
errno = EINVAL;
return -1;
}
/* Set the descriptor nonblocking. */
{
int flags = fcntl(descriptor, F_GETFL);
if (flags == -1) {
/* errno set by fcntl(). */
return -1;
}
if (fcntl(descriptor, F_SETFL, flags | O_NONBLOCK) == -1) {
/* errno set by fcntl(). */
return -1;
}
}
/* Initialize the stream structure. */
in->descriptor = descriptor;
in->data = NULL;
in->size = 0;
in->head = 0;
in->tail = 0;
/* Success. */
return 0;
}
/* Read until delimiter from an input stream.
* If 'done' is set at any point, will return 0 with errno==EINTR.
* Returns 0 if an error occurs, with errno set.
* Returns 0 with errno==0 when end of input stream.
*/
static size_t input_getdelim(struct input *const in,
int const delim,
char **const dataptr,
size_t *const sizeptr,
const double timeout)
{
const clockid_t timeout_clk = CLOCK_BOOTTIME;
struct timespec then;
/* Verify none of the pointers are NULL. */
if (!in || !dataptr || !sizeptr) {
errno = EINVAL;
return 0;
}
/* Record current time for timeout measurement. */
clock_gettime(timeout_clk, &then);
char *line_data = *dataptr;
size_t line_size = *sizeptr;
/* If (*sizeptr) is zero, then we ignore dataptr value, like getline() does. */
if (!line_size)
line_data = NULL;
while (1) {
struct timespec now;
struct pollfd fds[1];
ssize_t n;
int ms = DONE_POLL_INTERVAL_MS;
/* Done flag set? */
if (done) {
errno = EINTR;
return 0;
}
/* Is there a complete line in the input buffer? */
if (in->tail > in->head) {
const char *ptr = memchr(in->data + in->head, delim, in->tail - in->head);
if (ptr) {
const size_t len = ptr - (in->data + in->head);
if (len + 2 > line_size) {
/* Since we do not have any meaningful data in line_data,
and it would be overwritten anyway if there was,
instead of reallocating it we just free an allocate it. */
free(line_data); /* Note: free(null) is safe. */
line_size = len + 2;
line_data = malloc(line_size);
if (!line_data) {
/* Oops, we lost the buffer. */
*dataptr = NULL;
*sizeptr = 0;
errno = ENOMEM;
return 0;
}
*dataptr = line_data;
*sizeptr = line_size;
}
/* Copy the line, including the separator, */
memcpy(line_data, in->data + in->head, len + 1);
/* add a terminating nul char, */
line_data[len + 1] = '\0';
/* and update stream buffer state. */
in->head += len + 1;
return len + 1;
}
/* No, we shall read more data. Prepare the buffer. */
if (in->head > 0) {
memmove(in->data, in->data + in->head, in->tail - in->head);
in->tail -= in->head;
in->head = 0;
}
} else {
/* Input buffer is empty. */
in->head = 0;
in->tail = 0;
}
/* Do we need to grow input stream buffer? */
if (in->head >= in->tail) {
/* TODO: Better buffer size growth policy! */
const size_t size = (in->tail + 65535) | 65537;
char *data;
data = realloc(in->data, size);
if (!data) {
errno = ENOMEM;
return 0;
}
in->data = data;
in->size = size;
}
/* Try to read additional data. It is imperative that the descriptor
has been marked nonblocking, as otherwise this will block. */
n = read(in->descriptor, in->data + in->tail, in->size - in->tail);
if (n > 0) {
/* We read more data without blocking. */
in->tail += n;
continue;
} else
if (n == 0) {
/* End of input mark (Ctrl+D at the beginning of line, if a terminal) */
const size_t len = in->tail - in->head;
if (len < 1) {
/* No data buffered, read end of input. */
if (line_size < 1) {
line_size = 1;
line_data = malloc(line_size);
if (!line_data) {
errno = ENOMEM;
return 0;
}
*dataptr = line_data;
*sizeptr = line_size;
}
line_data[0] = '\0';
errno = 0;
return 0;
}
if (len + 1 > line_size) {
/* Since we do not have any meaningful data in line_data,
and it would be overwritten anyway if there was,
instead of reallocating it we just free an allocate it. */
free(line_data); /* Note: free(null) is safe. */
line_size = len + 1;
line_data = malloc(line_size);
if (!line_data) {
/* Oops, we lost the buffer. */
*dataptr = NULL;
*sizeptr = 0;
errno = ENOMEM;
return 0;
}
*dataptr = line_data;
*sizeptr = line_size;
}
memmove(line_data, in->data, len);
line_data[len] = '\0';
in->head = 0;
in->tail = 0;
return 0;
} else
if (n != -1) {
/* This should never occur; it would be a C library bug. */
errno = EIO;
return 0;
} else {
const int err = errno;
if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR)
return 0;
/* EAGAIN, EWOULDBLOCK, and EINTR are not real errors. */
}
/* Nonblocking operation, with timeout == 0.0? */
if (timeout == 0.0) {
errno = ETIMEDOUT;
return 0;
} else
if (timeout > 0.0) {
/* Obtain current time. */
clock_gettime(timeout_clk, &now);
const double elapsed = (double)(now.tv_sec - then.tv_sec)
+ (double)(now.tv_nsec - then.tv_nsec) / 1000000000.0;
/* Timed out? */
if (elapsed >= (double)timeout / 1000.0) {
errno = ETIMEDOUT;
return 0;
}
if (timeout - elapsed < (double)DONE_POLL_INTERVAL_MS / 1000.0) {
ms = (int)(1000 * (timeout - elapsed));
if (ms < 1) {
errno = ETIMEDOUT;
return 0;
}
}
}
/* Negative timeout values means no timeout check,
and ms retains its initialized value. */
/* Another done check; it's cheap. */
if (done) {
errno = 0;
return EINTR;
}
/* Wait for input, but not longer than ms milliseconds. */
fds[0].fd = in->descriptor;
fds[0].events = POLLIN;
fds[0].revents = 0;
poll(fds, 1, ms);
/* We don't actually care about the result at this point. */
}
/* Never reached. */
}
static inline size_t input_getline(struct input *const in,
char **const dataptr,
size_t *const sizeptr,
const double timeout)
{
return input_getdelim(in, '\n', dataptr, sizeptr, timeout);
}
int main(void)
{
struct input in;
char *line = NULL;
size_t size = 0;
size_t len;
if (install_done(SIGINT) == -1 ||
install_done(SIGHUP) == -1 ||
install_done(SIGTERM) == -1 ||
install_done(SIGUSR1) == -1) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
if (input_use(&in, STDIN_FILENO)) {
fprintf(stderr, "BUG in input_use(): %s.\n", strerror(errno));
return EXIT_FAILURE;
}
while (!done) {
/* Wait for input for five seconds. */
len = input_getline(&in, &line, &size, 5000);
if (len > 0) {
/* Remove the newline at end, if any. */
line[strcspn(line, "\n")] = '\0';
printf("Received: \"%s\" (%zu chars)\n", line, len);
fflush(stdout);
continue;
} else
if (errno == 0) {
/* This is the special case: input_getline() returns 0 with
errno == 0 when there is no more input. */
fprintf(stderr, "End of standard input.\n");
return EXIT_SUCCESS;
} else
if (errno == ETIMEDOUT) {
printf("(No input for five seconds.)\n");
fflush(stdout);
} else
if (errno == EINTR) {
/* Break or continue works here, since input_getline() only
returns 0 with errno==EINTR if done==1. */
break;
} else {
fprintf(stderr, "Error reading from standard input: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
}
printf("Signal received; done.\n");
return EXIT_SUCCESS;
}
Save it as e.g. example.c, compile using e.g. gcc -Wall -Wextra -O2 example.c -o example, and run using ./example. Type input and enter to supply lines, or Ctrl+D at the beginning of a line to end input, or Ctrl+C to send the process a SIGINT signal.
Note the compile-time constant DONE_POLL_INTERVAL_MS. If the signal is delivered between a done check and poll(), this is the maximum delay, in milliseconds (1000ths of a second), that the poll may block; and therefore is roughly the maximum delay from receiving the signal and acting upon it.
To make the example more interesting, it also implements a timeout on reading a full line also. The above example prints when it is reached, but that messes up how the user sees the input they're typing. (It does not affect the input.)
This is by no means a perfect example of such functions, but I hope it is a readable one, with the comments explaining the reasoning behind each code block.