fix: avoid EOF hang on unread pipe output (#189)

At EOF, pv waits for bytes already written to an output pipe to be consumed before finishing. If the downstream reader exits without reading those bytes, the pipe can keep reporting unread data forever. Since pv has no more input, there is no later write that would fail with EPIPE and break the wait.

Detect this EOF drain-wait case with a non-blocking poll() on the output pipe. When the pipe reports that its readers are gone, mark the pipe closed and stop waiting for bytes that can no longer be consumed. While a reader is still present, keep the existing behaviour and continue waiting for the pipe buffer to drain.

Add regression coverage for both ways pv can reach this state: direct input that leaves unread output bytes behind, and a pipeline where the original producer fails but an intermediate filter such as gzip still emits a valid empty stream. The latter matters because pv can still have pending output at EOF even when the command at the start of the pipeline failed immediately.

Signed-off-by: Wolfgang Hoschek <wolfgang.hoschek@mac.com>
This commit is contained in:
Wolfgang Hoschek
2026-04-30 04:47:09 -04:00
parent 79a6420fea
commit e7d188ac65
5 changed files with 130 additions and 2 deletions
+32
View File
@@ -0,0 +1,32 @@
#!/bin/sh
#
# Check that EOF does not wait forever when the output pipe reader exits
# without consuming bytes already written into the pipe.
# Allow all tests to be skipped, e.g. during a release build
test "${SKIP_ALL_TESTS}" = "1" && exit 77
true "${testSubject:?not set - call this from 'make check'}"
TIMEOUT=""
if command -v timeout >/dev/null 2>&1; then
TIMEOUT="timeout"
elif command -v gtimeout >/dev/null 2>&1; then
TIMEOUT="gtimeout"
else
echo "timeout command not available"
exit 77
fi
# The reader keeps the pipe open briefly but reads nothing. When it exits,
# PV has no further input, so it must notice the closed reader without
# relying on a later write returning EPIPE.
if ! "${TIMEOUT}" 4 sh <<EOF
printf "x" | "${testSubject}" -q -B 1 | sleep 1
EOF
then
echo "PV did not finish after the output pipe reader exited"
exit 1
fi
exit 0
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
#
# Check that EOF does not wait forever when the upstream producer exits with
# failure and the output pipe reader exits without consuming buffered bytes.
# Allow all tests to be skipped, e.g. during a release build
test "${SKIP_ALL_TESTS}" = "1" && exit 77
true "${testSubject:?not set - call this from 'make check'}"
TIMEOUT=""
if command -v timeout >/dev/null 2>&1; then
TIMEOUT="timeout"
elif command -v gtimeout >/dev/null 2>&1; then
TIMEOUT="gtimeout"
else
echo "timeout command not available"
exit 77
fi
if ! command -v gzip >/dev/null 2>&1; then
echo "gzip command not available"
exit 77
fi
# The producer fails immediately and writes no data. gzip still emits a small
# valid empty stream, so PV writes bytes to its output pipe before reaching EOF.
# The reader keeps the pipe open briefly but consumes nothing.
if ! "${TIMEOUT}" 4 sh <<EOF
false | gzip -1 -c -n | "${testSubject}" -q -B 1 | sleep 1
EOF
then
echo "PV did not finish after the input producer failed and the output reader exited"
exit 1
fi
exit 0