Drain std{out,err} when a plugin fails to load.

If a plugin fails to load after we've set up the goroutines that copy
from its std{out,err} streams, then those goroutines can end up writing
to a closed event channel. This change ensures that we properly drain
those streams in this case.
This commit is contained in:
Pat Gavlin 2017-10-16 20:51:18 -07:00
parent 908d081e88
commit 546612a354
2 changed files with 22 additions and 7 deletions

View file

@ -47,7 +47,11 @@ func (eng *Engine) Preview(stack tokens.QName, events chan<- Event, opts Preview
defer close(events)
result, err := eng.plan(info, deployOpts)
return eng.previewLatest(info, deployOpts)
}
func (eng *Engine) previewLatest(info *planContext, opts deployOptions) error {
result, err := eng.plan(info, opts)
if err != nil {
return err
}
@ -57,10 +61,11 @@ func (eng *Engine) Preview(stack tokens.QName, events chan<- Event, opts Preview
return err
}
}
if !diag.Success() {
if !opts.Diag.Success() {
// If any error occurred while walking the plan, be sure to let the developer know. Otherwise,
// although error messages may have spewed to the output, the final lines of the plan may look fine.
return errors.New("One or more errors occurred during the creation of this preview")
}
return nil
}

View file

@ -61,6 +61,13 @@ func newPlugin(ctx *Context, bin string, prefix string, args []string) (*plugin,
}
contract.Assert(plug != nil)
// If we did not successfully launch the plugin, we still need to wait for stderr and stdout to drain.
defer func() {
if plug.Conn == nil {
contract.IgnoreError(plug.Close())
}
}()
// For now, we will spawn goroutines that will spew STDOUT/STDERR to the relevant diag streams.
runtrace := func(t io.Reader, stderr bool, done chan<- bool) {
reader := bufio.NewReader(t)
@ -79,10 +86,9 @@ func newPlugin(ctx *Context, bin string, prefix string, args []string) (*plugin,
close(done)
}
stderrDone, stdoutDone := make(chan bool), make(chan bool)
plug.stderrDone, plug.stdoutDone = stderrDone, stdoutDone
// Set up a tracer on stderr before going any further, since important errors might get communicated this way.
stderrDone := make(chan bool)
plug.stderrDone = stderrDone
go runtrace(plug.Stderr, true, stderrDone)
// Now that we have a process, we expect it to write a single line to STDOUT: the port it's listening on. We only
@ -114,6 +120,8 @@ func newPlugin(ctx *Context, bin string, prefix string, args []string) (*plugin,
}
// After reading the port number, set up a tracer on stdout just so other output doesn't disappear.
stdoutDone := make(chan bool)
plug.stdoutDone = stdoutDone
go runtrace(plug.Stdout, false, stdoutDone)
// Now that we have the port, go ahead and create a gRPC client connection to it.
@ -196,8 +204,10 @@ func execPlugin(bin string, args []string) (*plugin, error) {
}
func (p *plugin) Close() error {
closerr := p.Conn.Close()
contract.IgnoreError(closerr)
if p.Conn != nil {
closerr := p.Conn.Close()
contract.IgnoreError(closerr)
}
// IDEA: consider a more graceful termination than just SIGKILL.
err := p.Proc.Kill()