]> git.ipfire.org Git - thirdparty/coreutils.git/commitdiff
tail: consistently output all data for truncated files
authorPádraig Brady <P@draigBrady.com>
Mon, 11 May 2015 13:25:19 +0000 (14:25 +0100)
committerPádraig Brady <P@draigBrady.com>
Mon, 11 May 2015 22:16:14 +0000 (23:16 +0100)
Generally if logs are truncated, they're truncated to 0 length,
so output all existing data when our heuristic determines truncation.
Note with inotify, truncate() and write() are often determined
independently and so all data would be written if that was the case.

* src/tail.c (check_fspec): Reset file offset to 0 upon truncation.
(tail_forever): Likewise.
(recheck): Add a FIXME for the related issue where tail may lose
data due to tail discounting older log files too early.
* tests/tail-2/truncate.sh: A new test.
* tests/local.mk: Reference the new test.
* NEWS: Mention the fix.

NEWS
src/tail.c
tests/local.mk
tests/tail-2/truncate.sh [new file with mode: 0755]

diff --git a/NEWS b/NEWS
index da2b53d2e8712931ac2f2d61a96ec73c48d4ca93..0fbd4ce6ef5a4b0de19ed008e08a417c5bd3f5b9 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -49,6 +49,9 @@ GNU coreutils NEWS                                    -*- outline -*-
   replaced before inotify watches were created.
   [bug introduced in coreutils-7.5]
 
+  tail --follow consistently outputs all data for a truncated file.
+  [bug introduced in the beginning]
+
 ** New features
 
   chroot accepts the new --skip-chdir option to not change the working directory
index bc1d04a8fef0df722e74283f2ccfe6222b2a8521..c9736ca5b80c6d436db8f0465c3f070e0bbc5bf1 100644 (file)
@@ -1025,7 +1025,7 @@ recheck (struct File_spec *f, bool blocking)
       if (f->fd == -1)
         {
           error (0, 0,
-                 _("%s has appeared;  following end of new file"),
+                 _("%s has appeared;  following new file"),
                  quote (pretty_name (f)));
         }
       else
@@ -1036,7 +1036,7 @@ recheck (struct File_spec *f, bool blocking)
           /* File has been replaced (e.g., via log rotation) --
              tail the new one.  */
           error (0, 0,
-                 _("%s has been replaced;  following end of new file"),
+                 _("%s has been replaced;  following new file"),
                  quote (pretty_name (f)));
         }
     }
@@ -1055,6 +1055,12 @@ recheck (struct File_spec *f, bool blocking)
         }
     }
 
+  /* FIXME: When a log is rotated, daemons tend to log to the
+     old file descriptor until the new file is present and
+     the daemon is sent a signal.  Therefore tail may miss entries
+     being written to the old file.  Perhaps we should keep
+     the older file open and continue to monitor it until
+     data is written to a new file.  */
   if (new_file)
     {
       /* Start at the beginning of the file.  */
@@ -1193,13 +1199,16 @@ tail_forever (struct File_spec *f, size_t n_files, double sleep_interval)
               /* reset counter */
               f[i].n_unchanged_stats = 0;
 
+              /* XXX: This is only a heuristic, as the file may have also
+                 been truncated and written to if st_size >= size
+                 (in which case we ignore new data <= size).  */
               if (S_ISREG (mode) && stats.st_size < f[i].size)
                 {
                   error (0, 0, _("%s: file truncated"), name);
-                  last = i;
-                  xlseek (fd, stats.st_size, SEEK_SET, name);
-                  f[i].size = stats.st_size;
-                  continue;
+                  /* Assume the file was truncated to 0,
+                     and therefore output all "new" data.  */
+                  xlseek (fd, 0, SEEK_SET, name);
+                  f[i].size = 0;
                 }
 
               if (i != last)
@@ -1330,12 +1339,17 @@ check_fspec (struct File_spec *fspec, int wd, int *prev_wd)
       return;
     }
 
+  /* XXX: This is only a heuristic, as the file may have also
+     been truncated and written to if st_size >= size
+     (in which case we ignore new data <= size).
+     Though in the inotify case it's more likely we'll get
+     separate events for truncate() and write().  */
   if (S_ISREG (fspec->mode) && stats.st_size < fspec->size)
     {
       error (0, 0, _("%s: file truncated"), name);
       *prev_wd = wd;
-      xlseek (fspec->fd, stats.st_size, SEEK_SET, name);
-      fspec->size = stats.st_size;
+      xlseek (fspec->fd, 0, SEEK_SET, name);
+      fspec->size = 0;
     }
   else if (S_ISREG (fspec->mode) && stats.st_size == fspec->size
            && timespec_cmp (fspec->mtime, get_stat_mtime (&stats)) == 0)
index 0252763322158f7f4f10103e4ff8625817694774..8d01064bf6c3c9a5b9b8dbaacc53b5421997a95b 100644 (file)
@@ -408,6 +408,7 @@ all_tests =                                 \
   tests/tail-2/retry.sh                                \
   tests/tail-2/symlink.sh                      \
   tests/tail-2/tail-c.sh                       \
+  tests/tail-2/truncate.sh                     \
   tests/chmod/c-option.sh                      \
   tests/chmod/equal-x.sh                       \
   tests/chmod/equals.sh                                \
diff --git a/tests/tail-2/truncate.sh b/tests/tail-2/truncate.sh
new file mode 100755 (executable)
index 0000000..82720fd
--- /dev/null
@@ -0,0 +1,56 @@
+#!/bin/sh
+# Ensure all logs are output upon file truncation
+
+# Copyright (C) 2015 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ tail
+
+check_tail_output()
+{
+  local delay="$1"
+  grep "$tail_re" out > /dev/null ||
+    { sleep $delay; return 1; }
+}
+
+# Terminate any background tail process
+cleanup_() { kill $pid 2>/dev/null && wait $pid; }
+
+# Speedup the non inotify case
+fastpoll='-s.1 --max-unchanged-stats=1'
+
+for follow in '-f' '-F'; do
+  for mode in '' '---disable-inotify'; do
+    rm -f out
+    seq 10 > f || framework_failure_
+
+    tail $follow $mode $fastpoll f > out 2>&1 & pid=$!
+
+    # Wait up to 12.7s for tail to start
+    tail_re='^10$' retry_delay_ check_tail_output .1 7 ||
+      { cat out; fail=1; }
+
+    seq 11 15 > f || framework_failure_
+
+    # Wait up to 12.7s for new data
+    tail_re='^15$' retry_delay_ check_tail_output .1 7 ||
+      { cat out; fail=1; }
+
+    cleanup_
+  done
+done
+
+Exit $fail