>From de5659a015d4e3e2d5dce65e8fb16cebf2b09d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draig=20Brady?= Date: Wed, 7 Jun 2017 01:00:28 -0700 Subject: [PATCH] tail: exit promptly when output no longer writable This will support use cases like: tail -f file.log | grep -q trigger && process_immediately * src/tail.c (check_output_alive): A new function that uses select on fifos or pipes to detect if they're broken. (tail_forever): Call check_output_alive() periodically. (tail_forever_inotify): Merge the select() call from check_output_alive() into the select() originally present for the --pid case, and adjust accordingly. * tests/tail-2/pipe-f.sh: Add test cases. * NEWS: Mention the improvement. --- NEWS | 3 ++ src/tail.c | 77 +++++++++++++++++++++++++++++++++++++++++--------- tests/tail-2/pipe-f.sh | 21 +++++++++++--- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/NEWS b/NEWS index 508c08f..d2672e8 100644 --- a/NEWS +++ b/NEWS @@ -53,6 +53,9 @@ GNU coreutils NEWS -*- outline -*- mv --verbose now distinguishes rename and copy operations. + tail -f will now exit immediately if the output is piped + and the reader of the pipe terminates. + * Noteworthy changes in release 8.27 (2017-03-08) [stable] diff --git a/src/tail.c b/src/tail.c index 3582321..9329145 100644 --- a/src/tail.c +++ b/src/tail.c @@ -175,6 +175,9 @@ static enum Follow_mode follow_mode = Follow_descriptor; /* If true, read from the ends of all specified files until killed. */ static bool forever; +/* If true, monitor output so we exit if pipe reader terminates. */ +static bool monitor_output; + /* If true, count from start of file instead of end. */ static bool from_start; @@ -327,6 +330,27 @@ named file in a way that accommodates renaming, removal and creation.\n\ exit (status); } +/* If the output has gone away, then terminate + as we would if we had written to this output. */ +static void +check_output_alive (void) +{ + if (! monitor_output) + return; + + struct timeval delay; + delay.tv_sec = delay.tv_usec = 0; + + fd_set rfd; + FD_ZERO (&rfd); + FD_SET (STDOUT_FILENO, &rfd); + + /* readable event on STDOUT is equivalent to POLLERR, + and implies an error condition on output like broken pipe. */ + if (select (STDOUT_FILENO + 1, &rfd, NULL, NULL, &delay) == 1) + raise (SIGPIPE); +} + static bool valid_file_spec (struct File_spec const *f) { @@ -1244,6 +1268,8 @@ tail_forever (struct File_spec *f, size_t n_files, double sleep_interval) if ((!any_input || blocking) && fflush (stdout) != 0) die (EXIT_FAILURE, errno, _("write error")); + check_output_alive (); + /* If nothing was read, sleep and/or check for dead writers. */ if (!any_input) { @@ -1577,32 +1603,48 @@ tail_forever_inotify (int wd, struct File_spec *f, size_t n_files, /* When watching a PID, ensure that a read from WD will not block indefinitely. */ - if (pid && (len <= evbuf_off)) + while (len <= evbuf_off) { - if (writer_is_dead) - exit (EXIT_SUCCESS); - - writer_is_dead = (kill (pid, 0) != 0 && errno != EPERM); - struct timeval delay; /* how long to wait for file changes. */ - if (writer_is_dead) - delay.tv_sec = delay.tv_usec = 0; - else + + if (pid) { - delay.tv_sec = (time_t) sleep_interval; - delay.tv_usec = 1000000 * (sleep_interval - delay.tv_sec); + if (writer_is_dead) + exit (EXIT_SUCCESS); + + writer_is_dead = (kill (pid, 0) != 0 && errno != EPERM); + + if (writer_is_dead) + delay.tv_sec = delay.tv_usec = 0; + else + { + delay.tv_sec = (time_t) sleep_interval; + delay.tv_usec = 1000000 * (sleep_interval - delay.tv_sec); + } } fd_set rfd; FD_ZERO (&rfd); FD_SET (wd, &rfd); + if (monitor_output) + FD_SET (STDOUT_FILENO, &rfd); - int file_change = select (wd + 1, &rfd, NULL, NULL, &delay); + int file_change = select (MAX (wd, STDOUT_FILENO) + 1, + &rfd, NULL, NULL, pid ? &delay: NULL); if (file_change == 0) continue; else if (file_change == -1) - die (EXIT_FAILURE, errno, _("error monitoring inotify event")); + die (EXIT_FAILURE, errno, + _("error waiting for inotify and output events")); + else if (FD_ISSET (STDOUT_FILENO, &rfd)) + { + /* readable event on STDOUT is equivalent to POLLERR, + and implies an error on output like broken pipe. */ + raise (SIGPIPE); + } + else + break; } if (len <= evbuf_off) @@ -2350,6 +2392,15 @@ main (int argc, char **argv) if (forever && ignore_fifo_and_pipe (F, n_files)) { + /* If stdout is a fifo or pipe, then monitor it + so that we exit if the reader goes away. + Note select() on a regular file is always readable. */ + struct stat out_stat; + if (fstat (STDOUT_FILENO, &out_stat) < 0) + die (EXIT_FAILURE, errno, _("standard output")); + monitor_output = (S_ISFIFO (out_stat.st_mode) + || (HAVE_FIFO_PIPES != 1 && isapipe (STDOUT_FILENO))); + #if HAVE_INOTIFY /* tailable_stdin() checks if the user specifies stdin via "-", or implicitly by providing no arguments. If so, we won't use inotify. diff --git a/tests/tail-2/pipe-f.sh b/tests/tail-2/pipe-f.sh index 00b99d9..b2ea506 100755 --- a/tests/tail-2/pipe-f.sh +++ b/tests/tail-2/pipe-f.sh @@ -1,5 +1,5 @@ #!/bin/sh -# ensure that :|tail -f doesn't hang, per POSIX +# ensure that tail -f doesn't hang in various cases # Copyright (C) 2009-2017 Free Software Foundation, Inc. @@ -19,15 +19,28 @@ . "${srcdir=.}/tests/init.sh"; path_prepend_ ./src print_ver_ tail +# Speedup the non inotify case +fastpoll='-s.1 --max-unchanged-stats=1' + +for mode in '' '---disable-inotify'; do +# Ensure :|tail -f doesn't hang, per POSIX echo oo > exp || framework_failure_ -echo foo | timeout 10 tail -f -c3 > out || fail=1 +echo foo | timeout 10 tail -f $mode $fastpoll -c3 > out || fail=1 compare exp out || fail=1 - cat <<\EOF > exp || framework_failure_ ==> standard input <== ar EOF -echo bar | returns_ 1 timeout 10 tail -f -c3 - missing > out || fail=1 +echo bar | returns_ 1 \ + timeout 10 tail -f $mode $fastpoll -c3 - missing > out || fail=1 compare exp out || fail=1 +# This would wait indefinitely before v8.28 due to no EPIPE being +# generated due to no data written after the first small amount +timeout 10 tail -f $mode $fastpoll out | sleep .1 || fail=1 + +# This would wait indefinitely before v8.28 (until first write) +returns_ 1 timeout 10 tail -f $mode $fastpoll /dev/null >&- || fail=1 +done + Exit $fail -- 2.9.3