On 07/06/17 01:00, Pádraig Brady wrote:
> The following will hang indefinitely, due to no further data
> being written through the pipe, and thus no SIGPIPE generated:
> 
>   $ tail -f /etc/hosts | sleep 1
> 
> A more practical use case might be:
> 
>   tail -f file.log | grep -q trigger &&
>   process_immediately
> 
> Another case where one might notice this is that sometimes
> (depending on how slow the reader is) the following will hang,
> and you may not notice the issue until data arrives
> 
>   tail -f file.log | grep_typo filter
> 
> Below is a quick hack to support this,
> and tail now terminates promptly for the above cases.
> 
> The implementation is a proof on concept done in a few minutes
> (and may change from poll() to select() if possible).
> A disadvantage is the extra syscalls to do the polling.
> Actually I may be able to combine the existing select()
> with this new poll(), and thus wait for changes in the
> output as well as the inotify descriptor.
> 
> cheers,
> Pádraig.
> 
> 
> diff --git a/src/tail.c b/src/tail.c

> +/* If the output has gone away, then terminate
> +   as we would if we had written to this output.  */
> +static void
> +check_output (void)
> +{
> +  struct pollfd pfd = {.fd = STDOUT_FILENO, .events = POLLERR};
> +  if (poll (&pfd, 1, 0) >= 0 && (pfd.revents & POLLERR))
> +    raise (SIGPIPE);
> +}
> +

The attached changes from using poll() to using select(),
which was already used in tail(1), and avoids the repeated
poll() calls in the normal case.

cheers,
Pádraig
>From de5659a015d4e3e2d5dce65e8fb16cebf2b09d26 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email protected]>
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

Reply via email to