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 }