Skip to content

Commit 8af7034

Browse files
committed
os: record the blocking pipe WaitDelay failure
1 parent a993b16 commit 8af7034

2 files changed

Lines changed: 93 additions & 0 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# WaitDelay with a background child
2+
3+
This manual test uses the Go standard library `os/exec` with TinyGo's `os`.
4+
The shell exits and a background `sleep` keeps the output pipe open for two
5+
seconds. `WaitDelay` is 100 ms. The test returns status 1 if the wait takes
6+
one second or more, or if the error is not `exec.ErrWaitDelay`.
7+
8+
Build and run from the repository root with the process changes in TINYGOROOT.
9+
10+
```sh
11+
tinygo build -p 1 -o /tmp/waitdelay ./testdata/os-exec-waitdelay/main.go
12+
timeout 10 /tmp/waitdelay
13+
timeout 10 /tmp/waitdelay pipe
14+
go build -p 1 -o /tmp/waitdelay-go ./testdata/os-exec-waitdelay/main.go
15+
timeout 10 /tmp/waitdelay-go
16+
timeout 10 /tmp/waitdelay-go pipe
17+
```
18+
19+
`timeout` is an external limit for Linux. The background child exits after
20+
two seconds. The `pipe` mode closes a reader during a read. It closes the
21+
writer two seconds later so that the test can finish if the read stays blocked.
22+
23+
Measured on Linux arm64 with the released TinyGo 0.42.0 compiler, Go 1.27.0,
24+
and a TINYGOROOT copy with PR #5634 and the fd remapping fix.
25+
26+
| Test | TinyGo | Go |
27+
| --- | --- | --- |
28+
| WaitDelay 100 ms | 2.004 s, ErrWaitDelay | 101 ms, ErrWaitDelay |
29+
| Read after reader close | 2.003 s, EOF | 57 us, file already closed |
30+
31+
`src/os/file_anyos.go` calls `syscall.Read` and `syscall.Close` directly.
32+
Closing the reader does not stop the active blocking read in this test.
33+
Go's `Cmd.awaitGoroutines` closes the pipes when the timer expires, then waits
34+
for the copy goroutines. That wait lasts until the background child closes
35+
the pipe. The timer fires, but it cannot enforce the limit.
36+
37+
This remains open. A fix needs interruptible pipe I/O and coordination between
38+
close and active I/O. It must also prevent an old operation from using a reused
39+
descriptor. Changes to spawn file actions or Darwin fcntl do not fix this Linux
40+
failure. PR #5630 addresses lock contention, not this blocked read.
41+
42+
The follow-up must test WaitDelay after normal exit and context cancellation,
43+
blocked pipe reads and writes, prompt close, and descriptor reuse on hosted
44+
Linux and Darwin. A process that keeps its inherited output open must not
45+
keep `Cmd.Wait` blocked after the configured limit.

testdata/os-exec-waitdelay/main.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//go:build linux || darwin
2+
3+
package main
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
"io"
9+
"os"
10+
"os/exec"
11+
"time"
12+
)
13+
14+
func main() {
15+
if len(os.Args) > 1 && os.Args[1] == "pipe" {
16+
r, w, err := os.Pipe()
17+
if err != nil {
18+
panic(err)
19+
}
20+
done := make(chan error, 1)
21+
go func() {
22+
var buf [1]byte
23+
_, err := r.Read(buf[:])
24+
done <- err
25+
}()
26+
time.Sleep(100 * time.Millisecond)
27+
go func() {
28+
time.Sleep(2 * time.Second)
29+
w.Close()
30+
}()
31+
start := time.Now()
32+
closeErr := r.Close()
33+
err = <-done
34+
fmt.Printf("pipe close=%v read=%v elapsed=%v\n", closeErr, err, time.Since(start))
35+
return
36+
}
37+
cmd := exec.Command("/bin/sh", "-c", "sleep 2 &")
38+
cmd.Stdout = io.Discard
39+
cmd.Stderr = io.Discard
40+
cmd.WaitDelay = 100 * time.Millisecond
41+
start := time.Now()
42+
err := cmd.Run()
43+
elapsed := time.Since(start)
44+
fmt.Printf("WaitDelay=%v elapsed=%v error=%v\n", cmd.WaitDelay, elapsed, err)
45+
if !errors.Is(err, exec.ErrWaitDelay) || elapsed >= time.Second {
46+
os.Exit(1)
47+
}
48+
}

0 commit comments

Comments
 (0)