go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/legacy/annotee/executor/executor.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package executor
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"os/exec"
    23  
    24  	"github.com/golang/protobuf/proto"
    25  	log "go.chromium.org/luci/common/logging"
    26  	"go.chromium.org/luci/common/system/exitcode"
    27  	"go.chromium.org/luci/logdog/common/types"
    28  	"go.chromium.org/luci/luciexe/legacy/annotee"
    29  	"go.chromium.org/luci/luciexe/legacy/annotee/annotation"
    30  )
    31  
    32  // Executor bootstraps an application, running its output through a Processor.
    33  type Executor struct {
    34  	// Options are the set of Annotee options to use.
    35  	Options annotee.Options
    36  
    37  	// Stdin, if not nil, will be used as standard input for the bootstrapped
    38  	// process.
    39  	Stdin io.Reader
    40  
    41  	// TeeStdout, if not nil, is a Writer where bootstrapped process standard
    42  	// output will be tee'd.
    43  	TeeStdout io.Writer
    44  	// TeeStderr, if not nil, is a Writer where bootstrapped process standard
    45  	// error will be tee'd.
    46  	TeeStderr io.Writer
    47  
    48  	executed   bool
    49  	returnCode int
    50  
    51  	// step is the serialized milo.Step protobuf taken from the end of the
    52  	// Processor at execution finish.
    53  	step []byte
    54  }
    55  
    56  // Run executes the bootstrapped process, blocking until it completes.
    57  func (e *Executor) Run(ctx context.Context, command []string) error {
    58  	// Clear any previous state.
    59  	e.executed = false
    60  	e.returnCode = 0
    61  	e.step = nil
    62  
    63  	if len(command) == 0 {
    64  		return errors.New("no command")
    65  	}
    66  
    67  	ctx, cancelFunc := context.WithCancel(ctx)
    68  	defer cancelFunc()
    69  	cmd := exec.CommandContext(ctx, command[0], command[1:]...)
    70  
    71  	// STDOUT
    72  	stdoutRC, err := cmd.StdoutPipe()
    73  	if err != nil {
    74  		return fmt.Errorf("failed to create STDOUT pipe: %s", err)
    75  	}
    76  	defer stdoutRC.Close()
    77  	stdout := e.configStream(stdoutRC, annotee.STDOUT, e.TeeStdout, true)
    78  
    79  	stderrRC, err := cmd.StderrPipe()
    80  	if err != nil {
    81  		return fmt.Errorf("failed to create STDERR pipe: %s", err)
    82  	}
    83  	defer stderrRC.Close()
    84  	stderr := e.configStream(stderrRC, annotee.STDERR, e.TeeStderr, false)
    85  
    86  	// Start our process.
    87  	if err := cmd.Start(); err != nil {
    88  		return fmt.Errorf("failed to start bootstrapped process: %s", err)
    89  	}
    90  
    91  	// Cleanup the process on exit, and record its status and return code.
    92  	defer func() {
    93  		if err := cmd.Wait(); err != nil {
    94  			var ok bool
    95  			if e.returnCode, ok = exitcode.Get(err); ok {
    96  				e.executed = true
    97  			} else {
    98  				log.WithError(err).Errorf(ctx, "Failed to Wait() for bootstrapped process.")
    99  			}
   100  		} else {
   101  			e.returnCode = 0
   102  			e.executed = true
   103  		}
   104  	}()
   105  
   106  	// Probe our execution information.
   107  	options := e.Options
   108  	if options.Execution == nil {
   109  		options.Execution = annotation.ProbeExecution(command, nil, "")
   110  	}
   111  
   112  	// Configure our Processor.
   113  	streams := []*annotee.Stream{
   114  		stdout,
   115  		stderr,
   116  	}
   117  
   118  	// Process the bootstrapped I/O. We explicitly defer a Finish here to ensure
   119  	// that we clean up any internal streams if our Processor fails/panics.
   120  	//
   121  	// If we fail to process the I/O, terminate the bootstrapped process
   122  	// immediately, since it may otherwise block forever on I/O.
   123  	proc := annotee.New(ctx, options)
   124  	defer proc.Finish()
   125  
   126  	if err := proc.RunStreams(streams); err != nil {
   127  		return fmt.Errorf("failed to process bootstrapped I/O: %v", err)
   128  	}
   129  
   130  	// Finish and record our annotation steps on completion.
   131  	if e.step, err = proto.Marshal(proc.Finish().RootStep().Proto()); err != nil {
   132  		log.WithError(err).Errorf(ctx, "Failed to Marshal final Step protobuf on completion.")
   133  		return err
   134  	}
   135  	return nil
   136  }
   137  
   138  // Step returns the root Step protobuf from the latest run.
   139  func (e *Executor) Step() []byte { return e.step }
   140  
   141  // ReturnCode returns the executed process' return code.
   142  //
   143  // If the process hasn't completed its execution (see Executed), then this will
   144  // return 0.
   145  func (e *Executor) ReturnCode() int {
   146  	return e.returnCode
   147  }
   148  
   149  // Executed returns true if the bootstrapped process' execution completed
   150  // successfully. This is independent of the return value, and can be used to
   151  // differentiate execution errors from process errors.
   152  func (e *Executor) Executed() bool {
   153  	return e.executed
   154  }
   155  
   156  func (e *Executor) configStream(r io.Reader, name types.StreamName, tee io.Writer, emitAll bool) *annotee.Stream {
   157  	s := &annotee.Stream{
   158  		Reader:      r,
   159  		Name:        name,
   160  		Tee:         tee,
   161  		Alias:       "stdio",
   162  		Annotate:    true,
   163  		EmitAllLink: emitAll,
   164  	}
   165  	return s
   166  }