@@ -83,8 +83,8 @@ func (r *Runner) Run(ctx context.Context, _ string, logs io.Writer) error {
8383 }
8484
8585 copyCtx , copyCancel := context .WithTimeout (ctx , time .Duration (copyTimeout ))
86+ defer copyCancel ()
8687 matched , err := copyContext (copyCtx , copyOutput , conn , r .cfg .ExpectOutput )
87- copyCancel ()
8888 if r .cfg .ExpectTimeout {
8989 if err == nil {
9090 return xerrors .Errorf ("expected timeout, but the command exited successfully" )
@@ -107,11 +107,27 @@ func copyContext(ctx context.Context, dst io.Writer, src io.Reader, expectOutput
107107 copyErr = make (chan error )
108108 matched = expectOutput == ""
109109 )
110+
111+ // Guard goroutine for loop body to ensure reading `matched` is safe on
112+ // context cancellation and that `dst` won't be written to after we
113+ // return from this function.
114+ processing := make (chan struct {}, 1 )
115+ processing <- struct {}{}
116+
110117 go func () {
118+ defer close (processing )
111119 defer close (copyErr )
112120
113121 scanner := bufio .NewScanner (src )
114122 for scanner .Scan () {
123+ select {
124+ case <- processing :
125+ default :
126+ }
127+ if ctx .Err () != nil {
128+ return
129+ }
130+
115131 if expectOutput != "" && strings .Contains (scanner .Text (), expectOutput ) {
116132 matched = true
117133 }
@@ -121,6 +137,7 @@ func copyContext(ctx context.Context, dst io.Writer, src io.Reader, expectOutput
121137 copyErr <- xerrors .Errorf ("write to logs: %w" , err )
122138 return
123139 }
140+ processing <- struct {}{}
124141 }
125142 if scanner .Err () != nil {
126143 copyErr <- xerrors .Errorf ("read from reconnecting PTY: %w" , scanner .Err ())
@@ -130,6 +147,10 @@ func copyContext(ctx context.Context, dst io.Writer, src io.Reader, expectOutput
130147
131148 select {
132149 case <- ctx .Done ():
150+ select {
151+ case <- processing :
152+ case <- copyErr :
153+ }
133154 return matched , ctx .Err ()
134155 case err := <- copyErr :
135156 return matched , err
0 commit comments