Skip to content

Commit f3c9101

Browse files
committed
interp: test and fix read with regular files as stdin
This worked on v3.8.0, but was broken by v3.9.0 starting to use the cancelreader library to support cancellable reads from standard input. The use of cancelreader works OK for stdin files being OS pipes, but it does not work for stdin files which are regular files on Linux, as regular files on Linux are always ready for reading and do not support polling or cancelling in any way. As such, if cancelreader fails to create a cancellable reader, fall back to reading directly from the file without cancellation. This approach is not ideal, so leave a TODO to improve it with some form of new API proposed upstream. Thanks to Andrew Imeson for reporting the bug, providing multiple test cases which reproduced it, and doing a git bisect as well. Fixes #1099.
1 parent 26182ab commit f3c9101

File tree

2 files changed

+38
-24
lines changed

2 files changed

+38
-24
lines changed

interp/builtin.go

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"context"
1111
"errors"
1212
"fmt"
13+
"io"
1314
"os"
1415
"path/filepath"
1516
"strconv"
@@ -942,34 +943,43 @@ func (r *Runner) readLine(ctx context.Context, raw bool) ([]byte, error) {
942943
var line []byte
943944
esc := false
944945

945-
cr, err := cancelreader.NewReader(r.stdin)
946-
if err != nil {
947-
return nil, err
946+
stdin := io.Reader(r.stdin)
947+
// [cancelreader.NewReader] may fail under some circumstances, such as r.stdin being
948+
// a regular file on Linux, in which case epoll returns an "operation not permitted" error
949+
// given that regular files can always be read immediately. Polling them makes no sense.
950+
// As such, if cancelreader fails, fall back to no cancellation, meaning this is best-effort.
951+
//
952+
// TODO: it would be nice if the cancelreader library classified errors so that we could
953+
// safely handle "this file does not need polling" by skipping the polling as we do below
954+
// but still fail on other errors, which may be unexpected or hide bugs.
955+
// See the upstream issue: https://github.com/muesli/cancelreader/issues/23
956+
if cr, err := cancelreader.NewReader(r.stdin); err == nil {
957+
done := make(chan struct{})
958+
var wg sync.WaitGroup
959+
wg.Add(1)
960+
go func() {
961+
select {
962+
case <-ctx.Done():
963+
cr.Cancel()
964+
case <-done:
965+
}
966+
wg.Done()
967+
}()
968+
defer func() {
969+
close(done)
970+
wg.Wait()
971+
// Could put the Close in the above goroutine, but if "read" is
972+
// immediately called again, the Close might overlap with creating a
973+
// new cancelreader. Want this cancelreader to be completely closed
974+
// by the time readLine returns.
975+
cr.Close()
976+
}()
977+
stdin = cr
948978
}
949-
done := make(chan struct{})
950-
var wg sync.WaitGroup
951-
wg.Add(1)
952-
go func() {
953-
select {
954-
case <-ctx.Done():
955-
cr.Cancel()
956-
case <-done:
957-
}
958-
wg.Done()
959-
}()
960-
defer func() {
961-
close(done)
962-
wg.Wait()
963-
// Could put the Close in the above goroutine, but if "read" is
964-
// immediately called again, the Close might overlap with creating a
965-
// new cancelreader. Want this cancelreader to be completely closed
966-
// by the time readLine returns.
967-
cr.Close()
968-
}()
969979

970980
for {
971981
var buf [1]byte
972-
n, err := cr.Read(buf[:])
982+
n, err := stdin.Read(buf[:])
973983
if n > 0 {
974984
b := buf[0]
975985
switch {

interp/interp_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2905,6 +2905,10 @@ done <<< 2`,
29052905
"while read a; do echo $a; GOSH_CMD=exec_ok $GOSH_PROG; done <<EOF\na\nb\nc\nEOF",
29062906
"a\nexec ok\nb\nexec ok\nc\nexec ok\n",
29072907
},
2908+
{
2909+
"echo file1 >f; echo file2 >>f; while read a; do echo $a; done <f",
2910+
"file1\nfile2\n",
2911+
},
29082912
// TODO: our final exit status here isn't right.
29092913
// {
29102914
// "while read a; do echo $a; GOSH_CMD=exec_fail $GOSH_PROG; done <<< 'a\nb\nc'",

0 commit comments

Comments
 (0)