1#!/bin/sh
2# Exercise tail's behavior regarding missing files with/without --retry.
3
4# Copyright (C) 2013-2023 Free Software Foundation, Inc.
5
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15
16# You should have received a copy of the GNU General Public License
17# along with this program.  If not, see <https://www.gnu.org/licenses/>.
18
19. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
20print_ver_ tail
21
22# Function to count number of lines from tail
23# while ignoring transient errors due to resource limits
24countlines_ ()
25{
26  grep -Ev 'inotify (resources exhausted|cannot be used)' out | wc -l
27}
28
29# Function to check the expected line count in 'out'.
30# Called via retry_delay_().  Sleep some time - see retry_delay_() - if the
31# line count is still smaller than expected.
32wait4lines_ ()
33{
34  local delay=$1
35  local elc=$2   # Expected line count.
36  [ "$(countlines_)" -ge "$elc" ] || { sleep $delay; return 1; }
37}
38
39# Terminate any background tail process
40cleanup_() { kill $pid 2>/dev/null && wait $pid; }
41
42# Speedup the non inotify case
43fastpoll='-s.1 --max-unchanged-stats=1'
44
45# === Test:
46# Retry without --follow results in a warning.
47touch file
48tail --retry file > out 2>&1 || fail=1
49[ "$(countlines_)" = 1 ]                     || { cat out; fail=1; }
50grep -F 'tail: warning: --retry ignored' out || { cat out; fail=1; }
51
52# === Test:
53# The same with a missing file: expect error message and exit 1.
54returns_ 1 tail --retry missing > out 2>&1 || fail=1
55[ "$(countlines_)" = 2 ]                     || { cat out; fail=1; }
56grep -F 'tail: warning: --retry ignored' out || { cat out; fail=1; }
57
58for mode in '' '---disable-inotify'; do
59
60# === Test:
61# Ensure that "tail --retry --follow=name" waits for the file to appear.
62# Clear 'out' so that we can check its contents without races
63>out                            || framework_failure_
64timeout 10 \
65  tail $mode $fastpoll --follow=name --retry missing >out 2>&1 & pid=$!
66# Wait for "cannot open" error.
67retry_delay_ wait4lines_ .1 6 1 || { cat out; fail=1; }
68echo "X" > missing              || framework_failure_
69# Wait for the expected output.
70retry_delay_ wait4lines_ .1 6 3 || { cat out; fail=1; }
71cleanup_
72# Expect 3 lines in the output file.
73[ "$(countlines_)" = 3 ]   || { fail=1; cat out; }
74grep -F 'cannot open' out  || { fail=1; cat out; }
75grep -F 'has appeared' out || { fail=1; cat out; }
76grep '^X$' out             || { fail=1; cat out; }
77rm -f missing out          || framework_failure_
78
79# === Test:
80# Ensure that "tail --retry --follow=descriptor" waits for the file to appear.
81# tail-8.21 failed at this (since the implementation of the inotify support).
82timeout 10 \
83  tail $mode $fastpoll --follow=descriptor --retry missing >out 2>&1 & pid=$!
84# Wait for "cannot open" error.
85retry_delay_ wait4lines_ .1 6 2 || { cat out; fail=1; }
86echo "X1" > missing             || framework_failure_
87# Wait for the expected output.
88retry_delay_ wait4lines_ .1 6 4 || { cat out; fail=1; }
89# Ensure truncation is detected
90# tail-8.25 failed at this (as assumed non file and went into blocking mode)
91echo "X" > missing             || framework_failure_
92retry_delay_ wait4lines_ .1 6 6 || { cat out; fail=1; }
93cleanup_
94[ "$(countlines_)" = 6 ]   || { fail=1; cat out; }
95grep -F 'retry only effective for the initial open' out \
96                           || { fail=1; cat out; }
97grep -F 'cannot open' out  || { fail=1; cat out; }
98grep -F 'has appeared' out || { fail=1; cat out; }
99grep '^X1$' out            || { fail=1; cat out; }
100grep -F 'file truncated' out || { fail=1; cat out; }
101grep '^X$' out            || { fail=1; cat out; }
102rm -f missing out          || framework_failure_
103
104# === Test:
105# Ensure that tail --follow=descriptor --retry exits when the file appears
106# untailable. Expect exit status 1.
107timeout 10 \
108  tail $mode $fastpoll --follow=descriptor --retry missing >out 2>&1 & pid=$!
109# Wait for "cannot open" error.
110retry_delay_ wait4lines_ .1 6 2 || { cat out; fail=1; }
111mkdir missing                   || framework_failure_  # Create untailable
112# Wait for the expected output.
113retry_delay_ wait4lines_ .1 6 4 || { cat out; fail=1; }
114wait $pid
115rc=$?
116[ "$(countlines_)" = 4 ]                       || { fail=1; cat out; }
117grep -F 'retry only effective for the initial open' out \
118                                               || { fail=1; cat out; }
119grep -F 'cannot open' out                      || { fail=1; cat out; }
120grep -F 'replaced with an untailable file' out || { fail=1; cat out; }
121grep -F 'no files remaining' out               || { fail=1; cat out; }
122[ $rc = 1 ]                                    || { fail=1; cat out; }
123rm -fd missing out                             || framework_failure_
124
125# === Test:
126# Ensure that --follow=descriptor (without --retry) does *not* try
127# to open a file after an initial fail, even when there are other
128# tailable files.  This was an issue in <= 8.25.
129touch existing || framework_failure_
130tail $mode $fastpoll --follow=descriptor missing existing >out 2>&1 & pid=$!
131retry_delay_ wait4lines_ .1 6 2  || { cat out; fail=1; }
132[ "$(countlines_)" = 2 ]         || { fail=1; cat out; }
133grep -F 'cannot open' out        || { fail=1; cat out; }
134echo "Y" > missing               || framework_failure_
135echo "X" > existing              || framework_failure_
136retry_delay_ wait4lines_ .1 6 3  || { cat out; fail=1; }
137[ "$(countlines_)" = 3 ]         || { fail=1; cat out; }
138grep '^X$' out                   || { fail=1; cat out; }
139grep '^Y$' out                   && { fail=1; cat out; }
140cleanup_
141rm -f missing out existing       || framework_failure_
142
143# === Test:
144# Ensure that --follow=descriptor (without --retry) does *not wait* for the
145# file to appear.  Expect 2 lines in the output file ("cannot open" +
146# "no files remaining") and exit status 1.
147returns_ 1 tail $mode --follow=descriptor missing >out 2>&1 || fail=1
148[ "$(countlines_)" = 2 ]         || { fail=1; cat out; }
149grep -F 'cannot open' out        || { fail=1; cat out; }
150grep -F 'no files remaining' out || { fail=1; cat out; }
151rm -f out                        || framework_failure_
152
153# === Test:
154# Likewise for --follow=name (without --retry).
155returns_ 1 tail $mode --follow=name missing >out 2>&1 || fail=1
156[ "$(countlines_)" = 2 ]         || { fail=1; cat out; }
157grep -F 'cannot open' out        || { fail=1; cat out; }
158grep -F 'no files remaining' out || { fail=1; cat out; }
159rm -f out                        || framework_failure_
160
161# === Test:
162# Ensure that tail -F retries when the file is initially untailable.
163if ! cat . >/dev/null; then
164mkdir untailable || framework_failure_
165timeout 10 \
166  tail $mode $fastpoll -F untailable >out 2>&1 & pid=$!
167# Wait for "cannot follow" error.
168retry_delay_ wait4lines_ .1 6 2 || { cat out; fail=1; }
169{ rmdir untailable; echo foo > untailable; }   || framework_failure_
170# Wait for the expected output.
171retry_delay_ wait4lines_ .1 6 4 || { cat out; fail=1; }
172cleanup_
173[ "$(countlines_)" = 4 ]                       || { fail=1; cat out; }
174grep -F 'cannot follow' out                    || { fail=1; cat out; }
175# The first is the common case, "has appeared" arises with slow rmdir.
176grep -E 'become accessible|has appeared' out   || { fail=1; cat out; }
177grep -F 'giving up' out                        && { fail=1; cat out; }
178grep -F 'foo' out                              || { fail=1; cat out; }
179rm -fd untailable out                          || framework_failure_
180fi
181
182done
183
184Exit $fail
185