blob: eb12b7bb2cc634cb426b48b9213974e1b699b36e [file] [log] [blame]
// Copyright 2019 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package invoke
import (
"bytes"
"context"
"os/exec"
"sync"
"github.com/golang/protobuf/proto"
"go.chromium.org/luci/common/errors"
bbpb "go.chromium.org/luci/buildbucket/proto"
)
// Subprocess represents a running luciexe.
type Subprocess struct {
Step *bbpb.Step
parseOutput func() (*bbpb.Build, error)
cmd *exec.Cmd
closeChannels chan<- struct{}
allClosed <-chan error
waitOnce sync.Once
build *bbpb.Build
err error
}
// Start launches a binary implementing the luciexe protocol and returns
// immediately with a *Subprocess.
//
// Args:
// * ctx will be used for deadlines/cancellation of the started luciexe.
// * luciexePath must be the full absolute path to the luciexe binary.
// * input must be the Build message you wish to pass to the luciexe binary.
// * opts is optional (may be nil to take all defaults)
//
// Callers MUST call Wait and/or cancel the context or this will leak handles
// for the process' stdout/stderr.
//
// This assumes that the current process is already operating within a "host
// application" environment. See "go.chromium.org/luci/luciexe" for details.
//
// The caller SHOULD immediately take Subprocess.Step, append it to the current
// Build state, and send that (e.g. using `exe.BuildSender`). Otherwise this
// luciexe's steps will not show up in the Build.
func Start(ctx context.Context, luciexePath string, input *bbpb.Build, opts *Options) (*Subprocess, error) {
inputData, err := proto.Marshal(input)
if err != nil {
return nil, errors.Annotate(err, "marshalling input Build").Err()
}
launchOpts, _, err := opts.rationalize(ctx)
if err != nil {
return nil, errors.Annotate(err, "normalizing options").Err()
}
closeChannels := make(chan struct{})
allClosed := make(chan error)
go func() {
select {
case <-ctx.Done():
case <-closeChannels:
}
err := errors.NewLazyMultiError(2)
err.Assign(0, errors.Annotate(launchOpts.stdout.Close(), "closing stdout").Err())
err.Assign(1, errors.Annotate(launchOpts.stderr.Close(), "closing stderr").Err())
allClosed <- err.Get()
}()
cmd := exec.CommandContext(ctx, luciexePath, launchOpts.args...)
cmd.Env = launchOpts.env.Sorted()
cmd.Dir = launchOpts.workDir
cmd.Stdin = bytes.NewBuffer(inputData)
cmd.Stdout = launchOpts.stdout
cmd.Stderr = launchOpts.stderr
if err := cmd.Start(); err != nil {
// clean up stdout/stderr
close(closeChannels)
<-allClosed
return nil, errors.Annotate(err, "launching luciexe").Err()
}
return &Subprocess{
Step: launchOpts.step,
parseOutput: launchOpts.parseOutput,
cmd: cmd,
closeChannels: closeChannels,
allClosed: allClosed,
}, nil
}
// Wait waits for the subprocess to terminate.
//
// If Options.CollectOutput (default: false) was specified, this will return the
// final Build message, as reported by the luciexe.
//
// If you wish to cancel the subprocess (e.g. due to a timeout or deadline),
// make sure to pass a cancelable/deadline context to Start().
//
// Calling this multiple times is OK; it will return the same values every time.
func (s *Subprocess) Wait() (*bbpb.Build, error) {
s.waitOnce.Do(func() {
// No matter what, we want to close stdout/stderr; if none of the other
// return values have set `err`, it will be set to the result of closing
// stdout/stderr.
defer func() {
close(s.closeChannels)
closeErr := <-s.allClosed
if s.err != nil {
s.err = closeErr
}
}()
if s.err = s.cmd.Wait(); s.err != nil {
s.err = errors.Annotate(s.err, "waiting for luciexe").Err()
return
}
s.build, s.err = s.parseOutput()
})
return s.build, s.err
}