go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/exe/exe.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  package exe
    16  
    17  import (
    18  	"bytes"
    19  	"compress/zlib"
    20  	"context"
    21  	"flag"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"sync"
    26  
    27  	"google.golang.org/protobuf/proto"
    28  	"google.golang.org/protobuf/types/known/structpb"
    29  
    30  	bbpb "go.chromium.org/luci/buildbucket/proto"
    31  	"go.chromium.org/luci/buildbucket/protoutil"
    32  	"go.chromium.org/luci/common/errors"
    33  	"go.chromium.org/luci/common/logging"
    34  	"go.chromium.org/luci/common/logging/gologger"
    35  	"go.chromium.org/luci/common/system/environ"
    36  	"go.chromium.org/luci/common/system/signals"
    37  	"go.chromium.org/luci/logdog/client/butlerlib/bootstrap"
    38  	"go.chromium.org/luci/logdog/client/butlerlib/streamclient"
    39  	"go.chromium.org/luci/luciexe"
    40  )
    41  
    42  const (
    43  	// ArgsDelim separates args for user program from the args needed by this
    44  	// luciexe wrapper (e.g. `--output` flag). All args provided after the
    45  	// first ArgsDelim will passed to user program.
    46  	ArgsDelim = "--"
    47  )
    48  
    49  // BuildSender is a function which may be called within the callback of Run to
    50  // update this program's Build state.
    51  //
    52  // This function is bound to the Build message given to the `main` callback of
    53  // Run.
    54  //
    55  // Panics if it cannot send the Build (which is never expected in normal
    56  // operation).
    57  type BuildSender func()
    58  
    59  // InfraErrorTag should be set on errors returned from the `main` callback of
    60  // Run.
    61  //
    62  // Errors with this tag set will cause the overall build status to be
    63  // INFRA_FAILURE instead of FAILURE.
    64  var InfraErrorTag = errors.BoolTag{Key: errors.NewTagKey("infra_error")}
    65  
    66  // MainFn is the function signature you must implement in your callback to Run.
    67  //
    68  // Args:
    69  //   - ctx: The context will be canceled when the program receives the os
    70  //     Interrupt or SIGTERM (on unix) signal. The context also has standard go
    71  //     logging setup.
    72  //   - input: The initial Build state, as read from stdin. The build is not
    73  //     protected by a mutex of any sort, so the `MainFn` is responsible
    74  //     for protecting it if it can be modified from multiple goroutines.
    75  //   - userArgs: All command line arguments supplied after first `ArgsDelim`.
    76  //   - send: A send func which should be called after modifying the provided
    77  //     build. The BuildSender is synchronous and locked; it may only be called
    78  //     once at a time. It will marshal the current build, then send it. Writes
    79  //     to the build should be synchronized with calls to the BuildSender.
    80  //
    81  // input.Output.Properties is initialized to an empty Struct so you can use
    82  // WriteProperties right away.
    83  type MainFn func(ctx context.Context, input *bbpb.Build, userargs []string, send BuildSender) error
    84  
    85  func splitArgs(args []string) ([]string, []string) {
    86  	for i, arg := range args {
    87  		if arg == ArgsDelim {
    88  			return args[:i], args[i+1:]
    89  		}
    90  	}
    91  	return args, nil
    92  }
    93  
    94  func mkOutputHandler(exeArgs []string, build *bbpb.Build) func() {
    95  	fs := flag.NewFlagSet(exeArgs[0], flag.ExitOnError)
    96  	outputFlag := luciexe.AddOutputFlagToSet(fs)
    97  	fs.Parse(exeArgs[1:])
    98  	if outputFlag.Codec.IsNoop() {
    99  		return nil
   100  	}
   101  	return func() {
   102  		if err := outputFlag.Write(build); err != nil {
   103  			panic(errors.Annotate(err, "writing final build").Err())
   104  		}
   105  	}
   106  }
   107  
   108  func buildFrom(in io.Reader, build *bbpb.Build) {
   109  	data, err := io.ReadAll(in)
   110  	if err != nil {
   111  		panic(errors.Annotate(err, "reading Build from stdin").Err())
   112  	}
   113  	if err := proto.Unmarshal(data, build); err != nil {
   114  		panic(errors.Annotate(err, "parsing Build from stdin").Err())
   115  	}
   116  	// Initialize Output.Properties so that users can use exe.WriteProperties
   117  	// straight away.
   118  	if build.Output == nil {
   119  		build.Output = &bbpb.Build_Output{}
   120  	}
   121  	if build.Output.Properties == nil {
   122  		build.Output.Properties = &structpb.Struct{}
   123  	}
   124  }
   125  
   126  func mkBuildStream(ctx context.Context, build *bbpb.Build, zlibLevel int) (BuildSender, func() error) {
   127  	bs, err := bootstrap.GetFromEnv(environ.FromCtx(ctx))
   128  	if err != nil {
   129  		panic(errors.Annotate(err, "unable to make Logdog Client").Err())
   130  	}
   131  
   132  	cType := luciexe.BuildProtoContentType
   133  	if zlibLevel > 0 {
   134  		cType = luciexe.BuildProtoZlibContentType
   135  	}
   136  	buildStream, err := bs.Client.NewDatagramStream(
   137  		ctx, luciexe.BuildProtoStreamSuffix,
   138  		streamclient.WithContentType(cType))
   139  	if err != nil {
   140  		panic(err)
   141  	}
   142  
   143  	// TODO(iannucci): come up with a better API for this?
   144  	var sendBuildMu sync.Mutex
   145  	var buf *bytes.Buffer
   146  	var z *zlib.Writer
   147  
   148  	if zlibLevel > 0 {
   149  		buf = &bytes.Buffer{}
   150  		z, err = zlib.NewWriterLevel(buf, zlibLevel)
   151  		if err != nil {
   152  			panic(errors.Annotate(err, "unable to create zlib.Writer").Err())
   153  		}
   154  	}
   155  
   156  	return func() {
   157  		sendBuildMu.Lock()
   158  		defer sendBuildMu.Unlock()
   159  
   160  		data, err := proto.Marshal(build)
   161  		if err != nil {
   162  			panic(errors.Annotate(err, "unable to marshal Build state").Err())
   163  		}
   164  
   165  		if buf != nil {
   166  			buf.Reset()
   167  			z.Reset(buf)
   168  			if _, err := z.Write(data); err != nil {
   169  				panic(errors.Annotate(err, "unable to write to zlib.Writer").Err())
   170  			}
   171  			if err := z.Close(); err != nil {
   172  				panic(errors.Annotate(err, "unable to close zlib.Writer").Err())
   173  			}
   174  			data = buf.Bytes()
   175  		}
   176  
   177  		if err := buildStream.WriteDatagram(data); err != nil {
   178  			panic(errors.Annotate(err, "unable to write Build state").Err())
   179  		}
   180  	}, buildStream.Close
   181  }
   182  
   183  // Run executes the `main` callback with a basic Context.
   184  //
   185  // This calls os.Exit on completion of `main`, or panics if something went
   186  // wrong. If main panics, this is converted to an INFRA_FAILURE. If main returns
   187  // a non-nil error, this is converted to FAILURE, unless the InfraErrorTag is
   188  // set on the error (in which case it's converted to INFRA_FAILURE).
   189  func Run(main MainFn, options ...Option) {
   190  	os.Exit(runCtx(gologger.StdConfig.Use(context.Background()), os.Args, options, main))
   191  }
   192  
   193  func appendError(build *bbpb.Build, flavor string, errlike any) {
   194  	if build.SummaryMarkdown != "" {
   195  		build.SummaryMarkdown += "\n\n"
   196  	}
   197  	build.SummaryMarkdown += fmt.Sprintf("Final %s: %s", flavor, errlike)
   198  	if build.Output == nil {
   199  		build.Output = &bbpb.Build_Output{}
   200  	}
   201  	build.Output.SummaryMarkdown = build.SummaryMarkdown
   202  }
   203  
   204  func runCtx(ctx context.Context, args []string, opts []Option, main MainFn) int {
   205  	cfg := &config{}
   206  	for _, o := range opts {
   207  		if o != nil {
   208  			o(cfg)
   209  		}
   210  	}
   211  	exeArgs, userArgs := splitArgs(args)
   212  
   213  	build := &bbpb.Build{}
   214  	if handleFn := mkOutputHandler(exeArgs, build); handleFn != nil {
   215  		defer handleFn()
   216  	}
   217  
   218  	buildFrom(os.Stdin, build)
   219  	sendBuild, closer := mkBuildStream(ctx, build, cfg.zlibLevel)
   220  	defer func() {
   221  		if err := closer(); err != nil {
   222  			panic(err)
   223  		}
   224  	}()
   225  	defer sendBuild()
   226  
   227  	return runUserCode(ctx, build, userArgs, sendBuild, main)
   228  }
   229  
   230  // runUserCode should convert all user code errors/panic's into non-panicing
   231  // state in `build`.
   232  func runUserCode(ctx context.Context, build *bbpb.Build, userArgs []string, sendBuild BuildSender, main MainFn) (retcode int) {
   233  	defer func() {
   234  		if errI := recover(); errI != nil {
   235  			retcode = 2
   236  			build.Status = bbpb.Status_INFRA_FAILURE
   237  			build.Output.Status = bbpb.Status_INFRA_FAILURE
   238  			appendError(build, "panic", errI)
   239  			logging.Errorf(ctx, "main function paniced: %s", errI)
   240  			if err, ok := errI.(error); ok {
   241  				errors.Log(ctx, err)
   242  			}
   243  		}
   244  	}()
   245  
   246  	cCtx, cancel := context.WithCancel(ctx)
   247  	defer cancel()
   248  	signals.HandleInterrupt(cancel)
   249  	if build.Output == nil {
   250  		build.Output = &bbpb.Build_Output{}
   251  	}
   252  	if err := main(cCtx, build, userArgs, sendBuild); err != nil {
   253  		if InfraErrorTag.In(err) {
   254  			build.Status = bbpb.Status_INFRA_FAILURE
   255  			build.Output.Status = bbpb.Status_INFRA_FAILURE
   256  			appendError(build, "infra error", err)
   257  		} else {
   258  			build.Status = bbpb.Status_FAILURE
   259  			build.Output.Status = bbpb.Status_FAILURE
   260  			appendError(build, "error", err)
   261  		}
   262  		logging.Errorf(ctx, "main function failed: %s", err)
   263  		errors.Log(ctx, err)
   264  		retcode = 1
   265  	} else {
   266  		if !protoutil.IsEnded(build.Status) {
   267  			build.Status = bbpb.Status_SUCCESS
   268  		}
   269  		if !protoutil.IsEnded(build.Output.Status) {
   270  			build.Output.Status = build.Status
   271  		}
   272  		if build.Output.SummaryMarkdown == "" {
   273  			build.Output.SummaryMarkdown = build.SummaryMarkdown
   274  		}
   275  	}
   276  	return
   277  }