Skip to content

Commit c3ccdc3

Browse files
committed
Read logs from Stderr/out Pipe to avoid hanging
`TestContext_sleepTimeoutExpired` can occasionally hang when killing a command that has Stdout or Stderr set to anything besides `nil` or `*os.File`. golang/go#23019 Use workaround to read from StdoutPipe and StderrPipe rather than setting Stdout / Stderr
1 parent b4e272e commit c3ccdc3

File tree

3 files changed

+119
-12
lines changed

3 files changed

+119
-12
lines changed

tfexec/cmd.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package tfexec
22

33
import (
4+
"bufio"
45
"bytes"
56
"context"
67
"encoding/json"
8+
"errors"
79
"fmt"
810
"io"
911
"io/ioutil"
@@ -230,3 +232,22 @@ func mergeWriters(writers ...io.Writer) io.Writer {
230232
}
231233
return io.MultiWriter(compact...)
232234
}
235+
236+
func writeOutput(r io.ReadCloser, w io.Writer) error {
237+
buf := bufio.NewReader(r)
238+
for {
239+
line, err := buf.ReadBytes('\n')
240+
if len(line) > 0 {
241+
if _, err := w.Write(line); err != nil {
242+
return err
243+
}
244+
}
245+
if err != nil {
246+
if errors.As(err, &io.EOF) {
247+
return nil
248+
}
249+
250+
return err
251+
}
252+
}
253+
}

tfexec/cmd_default.go

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,10 @@ import (
1212
func (tf *Terraform) runTerraformCmd(ctx context.Context, cmd *exec.Cmd) error {
1313
var errBuf strings.Builder
1414

15-
cmd.Stdout = mergeWriters(cmd.Stdout, tf.stdout)
16-
cmd.Stderr = mergeWriters(cmd.Stderr, tf.stderr, &errBuf)
17-
1815
go func() {
1916
<-ctx.Done()
2017
if ctx.Err() == context.DeadlineExceeded || ctx.Err() == context.Canceled {
2118
if cmd != nil && cmd.Process != nil {
22-
tf.logger.Printf("killing process. cmd.ProcessState=%v", cmd.ProcessState)
2319
err := cmd.Process.Kill()
2420
if err != nil {
2521
tf.logger.Printf("error from kill: %s", err)
@@ -35,13 +31,60 @@ func (tf *Terraform) runTerraformCmd(ctx context.Context, cmd *exec.Cmd) error {
3531
default:
3632
}
3733

38-
err := cmd.Run()
34+
// Read stdout / stderr logs from pipe instead of setting cmd.Stdout and
35+
// cmd.Stderr because it can cause hanging when killing the command
36+
// https://github.com/golang/go/issues/23019
37+
stdoutWriter := mergeWriters(cmd.Stdout, tf.stdout)
38+
stderrWriter := mergeWriters(tf.stderr, &errBuf)
39+
40+
cmd.Stderr = nil
41+
cmd.Stdout = nil
42+
43+
stdoutPipe, err := cmd.StdoutPipe()
44+
if err != nil {
45+
return err
46+
}
47+
48+
stderrPipe, err := cmd.StderrPipe()
49+
if err != nil {
50+
return err
51+
}
52+
53+
err = cmd.Start()
54+
if err == nil && ctx.Err() != nil {
55+
err = ctx.Err()
56+
}
57+
if err != nil {
58+
return tf.wrapExitError(ctx, err, "")
59+
}
60+
61+
exitChLen := 2
62+
exitCh := make(chan error, exitChLen)
63+
go func() {
64+
exitCh <- writeOutput(stdoutPipe, stdoutWriter)
65+
}()
66+
go func() {
67+
exitCh <- writeOutput(stderrPipe, stderrWriter)
68+
}()
69+
70+
err = cmd.Wait()
3971
if err == nil && ctx.Err() != nil {
4072
err = ctx.Err()
4173
}
4274
if err != nil {
4375
return tf.wrapExitError(ctx, err, errBuf.String())
4476
}
4577

46-
return nil
78+
// Wait for the logs to finish writing
79+
counter := 0
80+
for {
81+
counter++
82+
err := <-exitCh
83+
if err != nil && err != context.Canceled {
84+
return tf.wrapExitError(ctx, err, errBuf.String())
85+
}
86+
if counter >= exitChLen {
87+
return ctx.Err()
88+
}
89+
}
4790
}

tfexec/cmd_linux.go

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ import (
1010
func (tf *Terraform) runTerraformCmd(ctx context.Context, cmd *exec.Cmd) error {
1111
var errBuf strings.Builder
1212

13-
cmd.Stdout = mergeWriters(cmd.Stdout, tf.stdout)
14-
cmd.Stderr = mergeWriters(cmd.Stderr, tf.stderr, &errBuf)
15-
1613
cmd.SysProcAttr = &syscall.SysProcAttr{
1714
// kill children if parent is dead
1815
Pdeathsig: syscall.SIGKILL,
@@ -24,7 +21,6 @@ func (tf *Terraform) runTerraformCmd(ctx context.Context, cmd *exec.Cmd) error {
2421
<-ctx.Done()
2522
if ctx.Err() == context.DeadlineExceeded || ctx.Err() == context.Canceled {
2623
if cmd != nil && cmd.Process != nil {
27-
tf.logger.Printf("killing process. cmd.ProcessState=%v", cmd.ProcessState)
2824
// send SIGINT to process group
2925
err := syscall.Kill(-cmd.Process.Pid, syscall.SIGINT)
3026
if err != nil {
@@ -43,13 +39,60 @@ func (tf *Terraform) runTerraformCmd(ctx context.Context, cmd *exec.Cmd) error {
4339
default:
4440
}
4541

46-
err := cmd.Run()
42+
// Read stdout / stderr logs from pipe instead of setting cmd.Stdout and
43+
// cmd.Stderr because it can cause hanging when killing the command
44+
// https://github.com/golang/go/issues/23019
45+
stdoutWriter := mergeWriters(cmd.Stdout, tf.stdout)
46+
stderrWriter := mergeWriters(tf.stderr, &errBuf)
47+
48+
cmd.Stderr = nil
49+
cmd.Stdout = nil
50+
51+
stdoutPipe, err := cmd.StdoutPipe()
52+
if err != nil {
53+
return err
54+
}
55+
56+
stderrPipe, err := cmd.StderrPipe()
57+
if err != nil {
58+
return err
59+
}
60+
61+
err = cmd.Start()
62+
if err == nil && ctx.Err() != nil {
63+
err = ctx.Err()
64+
}
65+
if err != nil {
66+
return tf.wrapExitError(ctx, err, "")
67+
}
68+
69+
exitChLen := 2
70+
exitCh := make(chan error, exitChLen)
71+
go func() {
72+
exitCh <- writeOutput(stdoutPipe, stdoutWriter)
73+
}()
74+
go func() {
75+
exitCh <- writeOutput(stderrPipe, stderrWriter)
76+
}()
77+
78+
err = cmd.Wait()
4779
if err == nil && ctx.Err() != nil {
4880
err = ctx.Err()
4981
}
5082
if err != nil {
5183
return tf.wrapExitError(ctx, err, errBuf.String())
5284
}
5385

54-
return nil
86+
// Wait for the logs to finish writing
87+
counter := 0
88+
for {
89+
counter++
90+
err := <-exitCh
91+
if err != nil && err != context.Canceled {
92+
return tf.wrapExitError(ctx, err, errBuf.String())
93+
}
94+
if counter >= exitChLen {
95+
return ctx.Err()
96+
}
97+
}
5598
}

0 commit comments

Comments
 (0)