Close piped processes on cancellation
This commit is contained in:
@@ -55,6 +55,8 @@ type PipedProcess struct {
|
||||
stdout io.ReadCloser
|
||||
parent context.Context
|
||||
cancel context.CancelFunc
|
||||
stopIO func() bool
|
||||
ioDone <-chan struct{}
|
||||
waitOnce sync.Once
|
||||
waitErr error
|
||||
}
|
||||
@@ -80,6 +82,9 @@ func (child *PipedProcess) Cancel() {
|
||||
func (child *PipedProcess) Wait() error {
|
||||
child.waitOnce.Do(func() {
|
||||
err := child.command.Wait()
|
||||
if !child.stopIO() {
|
||||
<-child.ioDone
|
||||
}
|
||||
child.cancel()
|
||||
if err == nil {
|
||||
return
|
||||
@@ -121,12 +126,20 @@ func StartPiped(ctx context.Context, invocation Invocation) (*PipedProcess, erro
|
||||
cancel()
|
||||
return nil, fmt.Errorf("start %q: %w", invocation.Args[0], err)
|
||||
}
|
||||
done := make(chan struct{})
|
||||
stopIO := context.AfterFunc(childContext, func() {
|
||||
defer close(done)
|
||||
_ = stdin.Close()
|
||||
_ = stdout.Close()
|
||||
})
|
||||
return &PipedProcess{
|
||||
command: command,
|
||||
stdin: stdin,
|
||||
stdout: stdout,
|
||||
parent: ctx,
|
||||
cancel: cancel,
|
||||
stopIO: stopIO,
|
||||
ioDone: done,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -260,6 +260,52 @@ func TestStartPipedCancellationAfterReadinessReapsChild(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPipedCancellationInterruptsInheritedOutputDrain(t *testing.T) {
|
||||
ready := filepath.Join(t.TempDir(), "descendant")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
child, err := StartPiped(ctx, Invocation{
|
||||
Args: []string{helperExecutable(t), "-test.run=TestProcessHelper", "--", "unused", "exit-descendant"},
|
||||
Env: map[string]string{"RELEASEKIT_PROCESS_HELPER": "1", "RELEASEKIT_DESCENDANT_READY": ready},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = child.Stdin().Close()
|
||||
drained := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.Copy(io.Discard, child.Stdout())
|
||||
drained <- err
|
||||
}()
|
||||
deadline := time.After(10 * time.Second)
|
||||
var pid int
|
||||
for pid == 0 {
|
||||
if data, err := os.ReadFile(ready); err == nil {
|
||||
pid, _ = strconv.Atoi(string(data))
|
||||
}
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatal("descendant did not become ready")
|
||||
default:
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if descendant, err := os.FindProcess(pid); err == nil {
|
||||
_ = descendant.Kill()
|
||||
}
|
||||
})
|
||||
cancel()
|
||||
select {
|
||||
case <-drained:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("inherited stdout did not close after cancellation")
|
||||
}
|
||||
if err := child.Wait(); err != nil && !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("wait after cancellation: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartPipedCancelDoesNotCancelParentContext(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
child, err := StartPiped(ctx, Invocation{
|
||||
|
||||
Reference in New Issue
Block a user