Skip to content

Commit 6b12e62

Browse files
edquistpixelb
authored andcommitted
tee: enhance -p mode using iopoll() to detect broken pipe outputs
If input is intermittent (a tty, pipe, or socket), and all remaining outputs are pipes (eg, >(cmd) process substitutions), exit early when they have all become broken pipes (and thus future writes will fail), without waiting for more input to become available, as future write attempts to these outputs will fail (SIGPIPE/EPIPE). Only provide this enhancement when pipe errors are ignored (-p mode). Note that only one output needs to be monitored at a time with iopoll(), as we only want to exit early if _all_ outputs have been removed. * src/tee.c (pipe_check): New global for iopoll mode. (main): enable pipe_check for -p, as long as output_error ignores EPIPE, and input is suitable for iopoll(). (get_next_out): Helper function for finding next valid output. (fail_output, tee_files): Break out write failure/output removal logic to helper function. (tee_files): Add out_pollable array to track which outputs are suitable for iopoll() (ie, that are pipes); track first output index that is still valid; add iopoll() broken pipe detection before calling read(), removing an output that becomes a broken pipe. * src/local.mk (src_tee_SOURCES): include src/iopoll.c. * NEWS: Mention tee -p enhancement in Improvements. * doc/coreutils.texi: Mention the new early exit behavior in the nopipe modes for the tee -p option. Suggested-by: Arsen Arsenović <arsen@aarsen.me>
1 parent b5c421a commit 6b12e62

5 files changed

Lines changed: 95 additions & 16 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ GNU coreutils NEWS -*- outline -*-
153153
when their modification time doesn't change when new data is available.
154154
Previously tail would not show any new data in this case.
155155

156+
tee -p detects when all remaining outputs have become broken pipes, and
157+
exits, rather than waiting for more input to induce an exit when written.
158+
159+
156160

157161
* Noteworthy changes in release 9.1 (2022-04-15) [stable]
158162

doc/coreutils.texi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14198,13 +14198,15 @@ This is the default @var{mode} when not specified,
1419814198
or when the short form @option{-p} is used.
1419914199
Warn on error opening or writing any output, except pipes.
1420014200
Writing is continued to still open files/pipes.
14201+
Exit early if all remaining outputs become broken pipes.
1420114202
Exit status indicates failure if any non pipe output had an error.
1420214203

1420314204
@item exit
1420414205
Exit on error opening or writing any output, including pipes.
1420514206

1420614207
@item exit-nopipe
1420714208
Exit on error opening or writing any output, except pipes.
14209+
Exit early if all remaining outputs become broken pipes.
1420814210
@end table
1420914211

1421014212
@end table

src/iopoll.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#define IOPOLL_BROKEN_OUTPUT -2
22
#define IOPOLL_ERROR -3
33

4-
int iopoll(int fdin, int fdout);
5-
bool iopoll_input_ok(int fdin);
6-
bool iopoll_output_ok(int fdout);
4+
int iopoll (int fdin, int fdout);
5+
bool iopoll_input_ok (int fdin);
6+
bool iopoll_output_ok (int fdout);

src/local.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ src_arch_SOURCES = src/uname.c src/uname-arch.c
396396
src_cut_SOURCES = src/cut.c src/set-fields.c
397397
src_numfmt_SOURCES = src/numfmt.c src/set-fields.c
398398

399+
src_tee_SOURCES = src/tee.c src/iopoll.c
399400
src_sum_SOURCES = src/sum.c src/sum.h src/digest.c
400401
src_sum_CPPFLAGS = -DHASH_ALGO_SUM=1 $(AM_CPPFLAGS)
401402

src/tee.c

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "fadvise.h"
2929
#include "stdio--.h"
3030
#include "xbinary-io.h"
31+
#include "iopoll.h"
3132

3233
/* The official name of this program (e.g., no 'g' prefix). */
3334
#define PROGRAM_NAME "tee"
@@ -45,6 +46,9 @@ static bool append;
4546
/* If true, ignore interrupts. */
4647
static bool ignore_interrupts;
4748

49+
/* If true, detect if next output becomes broken while waiting for input. */
50+
static bool pipe_check;
51+
4852
enum output_error
4953
{
5054
output_error_sigpipe, /* traditional behavior, sigpipe enabled. */
@@ -149,6 +153,12 @@ main (int argc, char **argv)
149153
output_error_args, output_error_types);
150154
else
151155
output_error = output_error_warn_nopipe;
156+
157+
/* Detect and close a broken pipe output when ignoring EPIPE. */
158+
if (output_error == output_error_warn_nopipe
159+
|| output_error == output_error_exit_nopipe)
160+
pipe_check = true;
161+
152162
break;
153163

154164
case_GETOPT_HELP_CHAR;
@@ -166,6 +176,10 @@ main (int argc, char **argv)
166176
if (output_error != output_error_sigpipe)
167177
signal (SIGPIPE, SIG_IGN);
168178

179+
/* No need to poll outputs if input is always ready for reading. */
180+
if (pipe_check && !iopoll_input_ok (STDIN_FILENO))
181+
pipe_check = false;
182+
169183
/* Do *not* warn if tee is given no file arguments.
170184
POSIX requires that it work when given no arguments. */
171185

@@ -176,6 +190,42 @@ main (int argc, char **argv)
176190
return ok ? EXIT_SUCCESS : EXIT_FAILURE;
177191
}
178192

193+
194+
/* Return the index of the first non-NULL descriptor after idx,
195+
or -1 if all are NULL. */
196+
197+
static int
198+
get_next_out (FILE **descriptors, int nfiles, int idx)
199+
{
200+
for (idx++; idx <= nfiles; idx++)
201+
if (descriptors[idx])
202+
return idx;
203+
return -1; /* no outputs remaining */
204+
}
205+
206+
/* Remove descriptors[i] due to write failure or broken pipe.
207+
Return true if this indicates a reportable error. */
208+
209+
static bool
210+
fail_output (FILE **descriptors, char **files, int i)
211+
{
212+
int w_errno = errno;
213+
bool fail = errno != EPIPE
214+
|| output_error == output_error_exit
215+
|| output_error == output_error_warn;
216+
if (descriptors[i] == stdout)
217+
clearerr (stdout); /* Avoid redundant close_stdout diagnostic. */
218+
if (fail)
219+
{
220+
error (output_error == output_error_exit
221+
|| output_error == output_error_exit_nopipe,
222+
w_errno, "%s", quotef (files[i]));
223+
}
224+
descriptors[i] = NULL;
225+
return fail;
226+
}
227+
228+
179229
/* Copy the standard input into each of the NFILES files in FILES
180230
and into the standard output. As a side effect, modify FILES[-1].
181231
Return true if successful. */
@@ -185,9 +235,11 @@ tee_files (int nfiles, char **files)
185235
{
186236
size_t n_outputs = 0;
187237
FILE **descriptors;
238+
bool *out_pollable;
188239
char buffer[BUFSIZ];
189240
ssize_t bytes_read = 0;
190241
int i;
242+
int first_out = 0; /* idx of first non-NULL output in descriptors */
191243
bool ok = true;
192244
char const *mode_string =
193245
(O_BINARY
@@ -202,8 +254,12 @@ tee_files (int nfiles, char **files)
202254
In both arrays, entry 0 corresponds to standard output. */
203255

204256
descriptors = xnmalloc (nfiles + 1, sizeof *descriptors);
257+
if (pipe_check)
258+
out_pollable = xnmalloc (nfiles + 1, sizeof *out_pollable);
205259
files--;
206260
descriptors[0] = stdout;
261+
if (pipe_check)
262+
out_pollable[0] = iopoll_output_ok (fileno (descriptors[0]));
207263
files[0] = bad_cast (_("standard output"));
208264
setvbuf (stdout, NULL, _IONBF, 0);
209265
n_outputs++;
@@ -212,6 +268,8 @@ tee_files (int nfiles, char **files)
212268
{
213269
/* Do not treat "-" specially - as mandated by POSIX. */
214270
descriptors[i] = fopen (files[i], mode_string);
271+
if (pipe_check)
272+
out_pollable[i] = iopoll_output_ok (fileno (descriptors[i]));
215273
if (descriptors[i] == NULL)
216274
{
217275
error (output_error == output_error_exit
@@ -228,6 +286,28 @@ tee_files (int nfiles, char **files)
228286

229287
while (n_outputs)
230288
{
289+
if (pipe_check && out_pollable[first_out])
290+
{
291+
/* Monitor for input, or errors on first valid output. */
292+
int err = iopoll (STDIN_FILENO, fileno (descriptors[first_out]));
293+
294+
/* Close the output if it became a broken pipe. */
295+
if (err == IOPOLL_BROKEN_OUTPUT)
296+
{
297+
errno = EPIPE; /* behave like write produced EPIPE */
298+
if (fail_output (descriptors, files, first_out))
299+
ok = false;
300+
n_outputs--;
301+
first_out = get_next_out (descriptors, nfiles, first_out);
302+
continue;
303+
}
304+
else if (err == IOPOLL_ERROR)
305+
{
306+
error (0, errno, _("iopoll error"));
307+
ok = false;
308+
}
309+
}
310+
231311
bytes_read = read (STDIN_FILENO, buffer, sizeof buffer);
232312
if (bytes_read < 0 && errno == EINTR)
233313
continue;
@@ -240,21 +320,11 @@ tee_files (int nfiles, char **files)
240320
if (descriptors[i]
241321
&& fwrite (buffer, bytes_read, 1, descriptors[i]) != 1)
242322
{
243-
int w_errno = errno;
244-
bool fail = errno != EPIPE || (output_error == output_error_exit
245-
|| output_error == output_error_warn);
246-
if (descriptors[i] == stdout)
247-
clearerr (stdout); /* Avoid redundant close_stdout diagnostic. */
248-
if (fail)
249-
{
250-
error (output_error == output_error_exit
251-
|| output_error == output_error_exit_nopipe,
252-
w_errno, "%s", quotef (files[i]));
253-
}
254-
descriptors[i] = NULL;
255-
if (fail)
323+
if (fail_output (descriptors, files, i))
256324
ok = false;
257325
n_outputs--;
326+
if (i == first_out)
327+
first_out = get_next_out (descriptors, nfiles, first_out);
258328
}
259329
}
260330

@@ -273,6 +343,8 @@ tee_files (int nfiles, char **files)
273343
}
274344

275345
free (descriptors);
346+
if (pipe_check)
347+
free (out_pollable);
276348

277349
return ok;
278350
}

0 commit comments

Comments
 (0)