diff --git a/Makefile.am b/Makefile.am index 3a873e9..acf2b21 100644 --- a/Makefile.am +++ b/Makefile.am @@ -82,6 +82,8 @@ tests/Integrity_-_Basic.test \ tests/Integrity_-_Binary_data.test \ tests/Integrity_-_From_bursty_source.test \ tests/Integrity_-_Large_file_support.test \ +tests/Integrity_-_On_closed_output_pipe_at_EOF.test \ +tests/Integrity_-_On_failed_input_pipe_at_EOF.test \ tests/Integrity_-_On_output_pipe_close.test \ tests/Integrity_-_When_adjusted_remotely.test \ tests/Memory_safety_-_Basic.test \ diff --git a/configure.ac b/configure.ac index 7819931..b031a4d 100644 --- a/configure.ac +++ b/configure.ac @@ -45,6 +45,8 @@ AC_CHECK_HEADERS([sys/ioctl.h]) AC_CHECK_HEADERS([libintl.h locale.h]) AC_CHECK_HEADERS([sys/sysmacros.h]) AC_CHECK_HEADERS([ftw.h]) +AC_CHECK_HEADERS([poll.h sys/poll.h]) +AC_CHECK_FUNCS([poll]) AC_CHECK_MEMBERS([struct stat.st_blksize]) AC_CHECK_DECLS([SA_SIGINFO], [], [], [[#include ]]) diff --git a/src/pv/loop.c b/src/pv/loop.c index cdc7bb4..b0132a5 100644 --- a/src/pv/loop.c +++ b/src/pv/loop.c @@ -25,6 +25,12 @@ #include #include +#if HAVE_POLL_H +#include +#elif HAVE_SYS_POLL_H +#include +#endif + #if HAVE_MATH_H #include #endif @@ -84,6 +90,48 @@ static bool pv__resize_display_on_signal(pvstate_t state) } +/* + * Return true if the write end of a pipe reports that its readers have gone + * away. This is only used after PV has reached EOF and is waiting for the + * output pipe buffer to drain; live readers still get the existing behaviour. + */ +static bool pv__output_pipe_has_no_reader(int output_fd) +{ +#if HAVE_POLL && (HAVE_POLL_H || HAVE_SYS_POLL_H) + struct pollfd pfd; + int result; + + memset(&pfd, 0, sizeof(pfd)); + pfd.fd = output_fd; + pfd.events = POLLOUT; + + result = poll(&pfd, 1, 0); + if (result < 0) { + if (EINTR != errno) { + debug("%s(%d): %s", "poll", output_fd, strerror(errno)); + } + return false; + } + if (0 == result) + return false; + +#if defined(POLLERR) + if (0 != (pfd.revents & POLLERR)) + return true; +#endif +#if defined(POLLHUP) + if (0 != (pfd.revents & POLLHUP)) + return true; +#endif + + return false; +#else + (void) output_fd; + return false; +#endif +} + + /* * Calculate and display the transfer statistics at the end of the transfer, * if stats are enabled and any measurements were taken. @@ -673,8 +721,15 @@ int pv_main_loop(pvstate_t state) * buffer to empty (#164). */ if (eof_in && eof_out && state->transfer.written_but_not_consumed > 0) { - debug("%s", "EOF but bytes remain in output pipe - sleeping"); - pv_nanosleep(50000000); + if (pv__output_pipe_has_no_reader(output_fd)) { + debug("%s", + "EOF but output pipe readers are gone - clearing written_but_not_consumed"); + state->flags.pipe_closed = 1; + state->transfer.written_but_not_consumed = 0; + } else { + debug("%s", "EOF but bytes remain in output pipe - sleeping"); + pv_nanosleep(50000000); + } } /* diff --git a/tests/Integrity_-_On_closed_output_pipe_at_EOF.test b/tests/Integrity_-_On_closed_output_pipe_at_EOF.test new file mode 100755 index 0000000..2874970 --- /dev/null +++ b/tests/Integrity_-_On_closed_output_pipe_at_EOF.test @@ -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 </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 <