go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/legacy/cmd/run_annotations/main.go (about)

     1  // Copyright 2019 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  // Command run_annotations is a LUCI executable that wraps a command that
    16  // produces @@@ANNOTATIONS@@@, and converts annotations to build message.
    17  package main
    18  
    19  import (
    20  	"context"
    21  	"os"
    22  	"os/exec"
    23  	"sync"
    24  	"time"
    25  
    26  	"github.com/golang/protobuf/proto"
    27  
    28  	pb "go.chromium.org/luci/buildbucket/proto"
    29  	"go.chromium.org/luci/common/errors"
    30  	"go.chromium.org/luci/logdog/client/butlerlib/bootstrap"
    31  	"go.chromium.org/luci/luciexe/exe"
    32  	"go.chromium.org/luci/luciexe/legacy/annotee"
    33  	"go.chromium.org/luci/luciexe/legacy/annotee/annotation"
    34  	annopb "go.chromium.org/luci/luciexe/legacy/annotee/proto"
    35  )
    36  
    37  const warningMsg = `WARNING: This step is launched using deprecated allow_subannotation feature
    38  that exists in the legacy @@@annotator@@@ protocol. Although a replacement
    39  is built using the latest luciexe protocol for backwards compatibility,
    40  please consider using 'step.presentation' object to mutate the step/build
    41  state instead in the recipe.
    42  
    43  NOTE: The original stdout/stderr log for this step is available at
    44  annotation.stdout/annotation.stderr log stream.
    45  `
    46  
    47  func check(err error) {
    48  	if err != nil {
    49  		panic(err)
    50  	}
    51  }
    52  
    53  func main() {
    54  	exe.Run(func(ctx context.Context, build *pb.Build, userArgs []string, sendBuild exe.BuildSender) error {
    55  		os.Stderr.WriteString(warningMsg)
    56  		if len(userArgs) == 0 {
    57  			return errors.New("No arguments were provided")
    58  		}
    59  
    60  		cwd, err := os.Getwd()
    61  		check(err)
    62  
    63  		// Start the subprocess.
    64  		cmd := exec.CommandContext(ctx, userArgs[0], userArgs[1:]...)
    65  		stdout, err := cmd.StdoutPipe()
    66  		check(err)
    67  		stderr, err := cmd.StderrPipe()
    68  		check(err)
    69  		err = cmd.Start()
    70  		check(errors.Annotate(err, "failed to start subprocess").Err())
    71  
    72  		ldBootstrap, err := bootstrap.Get()
    73  		check(err)
    74  
    75  		var buildMU sync.Mutex
    76  		sendAnnotations := func(ann *annopb.Step) {
    77  			latest, err := annotee.ConvertRootStep(ctx, ann)
    78  			check(errors.Annotate(err, "failed to convert an annotation root step to a build").Err())
    79  
    80  			buildMU.Lock()
    81  			defer buildMU.Unlock()
    82  			updateBaseBuild(build, latest)
    83  			sendBuild()
    84  		}
    85  
    86  		// Run STDOUT/STDERR streams through the processor.
    87  		// Run the process' output streams through Annotee. This will block until
    88  		// they are all consumed.
    89  		processor := annotee.New(ctx, annotee.Options{
    90  			Client:                 ldBootstrap.Client,
    91  			Execution:              annotation.ProbeExecution(userArgs, os.Environ(), cwd),
    92  			MetadataUpdateInterval: 30 * time.Second,
    93  			Offline:                false,
    94  			CloseSteps:             true,
    95  			AnnotationUpdated: func(annBytes []byte) {
    96  				ann := &annopb.Step{}
    97  				check(errors.Annotate(proto.Unmarshal(annBytes, ann), "failed to parse annotation proto").Err())
    98  				sendAnnotations(ann)
    99  			},
   100  		})
   101  		streams := []*annotee.Stream{
   102  			{
   103  				Reader:   stdout,
   104  				Name:     annotee.STDOUT,
   105  				Annotate: true,
   106  			},
   107  			{
   108  				Reader:   stderr,
   109  				Name:     annotee.STDERR,
   110  				Annotate: true,
   111  			},
   112  		}
   113  		err = processor.RunStreams(streams)
   114  		check(errors.Annotate(err, "failed to process annotations").Err())
   115  
   116  		// Wait for the subprocess to exit.
   117  		switch err := cmd.Wait().(type) {
   118  		case *exec.ExitError:
   119  		case nil:
   120  		default:
   121  			check(errors.Annotate(err, "failed to wait for the subprocess to exit").Err())
   122  		}
   123  
   124  		// Send the final state.
   125  		sendAnnotations(processor.Finish().RootStep().Proto())
   126  		return nil
   127  	})
   128  }
   129  
   130  func updateBaseBuild(base, latest *pb.Build) {
   131  	base.Status = latest.Status
   132  	base.StartTime = latest.StartTime
   133  	base.EndTime = latest.EndTime
   134  	base.SummaryMarkdown = latest.SummaryMarkdown
   135  	base.Output = latest.Output
   136  	for _, log := range base.GetOutput().GetLogs() {
   137  		// Rename the annotation's std logs so that it won't conflict with the
   138  		// std log of this command/luciexe when merging. More specifically,
   139  		// recipe engine will try to merge all logs in output into the step
   140  		// logs which already opens up stdout and stderr log.
   141  		if log.Name == "stdout" || log.Name == "stderr" {
   142  			log.Name = "annotation." + log.Name
   143  		}
   144  	}
   145  	base.Steps = latest.Steps
   146  }