go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/cli/printrun.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 cli 16 17 import ( 18 "bufio" 19 "context" 20 "fmt" 21 "io" 22 "os" 23 "strings" 24 25 "github.com/golang/protobuf/proto" 26 "google.golang.org/genproto/protobuf/field_mask" 27 28 "go.chromium.org/luci/common/data/stringset" 29 "go.chromium.org/luci/common/sync/parallel" 30 31 pb "go.chromium.org/luci/buildbucket/proto" 32 ) 33 34 var idFieldMask = &field_mask.FieldMask{Paths: []string{"id"}} 35 var allFieldMask = &field_mask.FieldMask{Paths: []string{"*"}} 36 var defaultFieldMask = &field_mask.FieldMask{ 37 Paths: []string{ 38 "builder", 39 "create_time", 40 "created_by", 41 "end_time", 42 "id", 43 "input.experimental", 44 "input.gerrit_changes", 45 "input.gitiles_commit", 46 "number", 47 "start_time", 48 "status", 49 "status_details", 50 "summary_markdown", 51 "tags", 52 "update_time", 53 }, 54 } 55 56 // extraFields are fields that will be printed even if not specified 57 // in the `-field` flag value. 58 var extraFields = []string{ 59 "id", 60 "status", 61 "builder", 62 } 63 var extraFieldsStr = strings.Join(extraFields, ", ") 64 65 // printRun is a base command run for subcommands that print 66 // builds. 67 type printRun struct { 68 baseCommandRun 69 all bool 70 properties bool 71 steps bool 72 id bool 73 fields string 74 eager bool 75 } 76 77 func (r *printRun) RegisterDefaultFlags(p Params) { 78 r.baseCommandRun.RegisterDefaultFlags(p) 79 r.baseCommandRun.RegisterJSONFlag() 80 } 81 82 // RegisterIDFlag registers -id flag. 83 func (r *printRun) RegisterIDFlag() { 84 r.Flags.BoolVar(&r.id, "id", false, doc(` 85 Print only build ids. 86 87 Intended for piping the output into another bb subcommand: 88 bb ls -cl myCL -id | bb cancel 89 `)) 90 } 91 92 // RegisterFieldFlags registers -A, -steps, -p and -field flags. 93 func (r *printRun) RegisterFieldFlags() { 94 r.Flags.BoolVar(&r.all, "A", false, doc(` 95 Print builds in their entirety. 96 With -json, prints all build fields. 97 Without -json, implies -steps and -p. 98 `)) 99 r.Flags.BoolVar(&r.steps, "steps", false, "Print steps") 100 r.Flags.BoolVar(&r.properties, "p", false, "Print input/output properties") 101 r.Flags.StringVar(&r.fields, "fields", "", doc(fmt.Sprintf(` 102 Print only provided fields. Fields should be passed as a comma separated 103 string to match the JSON encoding schema of FieldMask. Fields: [%s] will 104 also be printed for better result readability even if not requested. 105 106 This flag is mutually exclusive with -A, -p, -steps and -id. 107 108 See: https://developers.google.com/protocol-buffers/docs/proto3#json 109 `, extraFieldsStr))) 110 r.Flags.BoolVar(&r.eager, "eager", false, "return upon the first finished build") 111 } 112 113 // FieldMask returns the field mask to use in buildbucket requests. 114 func (r *printRun) FieldMask() (*field_mask.FieldMask, error) { 115 if err := r.validateFieldFlags(); err != nil { 116 return nil, err 117 } 118 119 switch { 120 case r.id: 121 return proto.Clone(idFieldMask).(*field_mask.FieldMask), nil 122 case r.all: 123 return proto.Clone(allFieldMask).(*field_mask.FieldMask), nil 124 case r.fields != "": 125 // TODO(crbug/1039823): Use Unmarshal feature in JSONPB when protobuf v2 126 // API is released. Currently, there's an existing issue in Go JSONPB 127 // implementation which results in serialization and deserialization of 128 // FieldMask not working as expected. 129 // See: https://github.com/golang/protobuf/issues/745 130 pathSet := stringset.NewFromSlice(strings.Split(r.fields, ",")...) 131 pathSet.AddAll(extraFields) 132 return &field_mask.FieldMask{Paths: pathSet.ToSortedSlice()}, nil 133 default: 134 ret := proto.Clone(defaultFieldMask).(*field_mask.FieldMask) 135 if r.properties { 136 ret.Paths = append(ret.Paths, "input.properties", "output.properties") 137 } 138 if r.steps { 139 ret.Paths = append(ret.Paths, "steps") 140 } 141 return ret, nil 142 } 143 } 144 145 // validateFieldFlags validates the combination of provided field flags. 146 func (r *printRun) validateFieldFlags() error { 147 switch { 148 case r.fields != "" && (r.all || r.properties || r.steps || r.id): 149 return fmt.Errorf("-fields is mutually exclusive with -A, -p, -steps and -id") 150 case r.id && (r.all || r.properties || r.steps): 151 return fmt.Errorf("-id is mutually exclusive with -A, -p and -steps") 152 case r.all && (r.properties || r.steps): 153 return fmt.Errorf("-A is mutually exclusive with -p and -steps") 154 default: 155 return nil 156 } 157 } 158 159 func (r *printRun) printBuild(p *printer, build *pb.Build, first bool) error { 160 if r.json { 161 if r.id { 162 p.f(`{"id": "%d"}`, build.Id) 163 p.f("\n") 164 } else { 165 p.JSONPB(build, true) 166 } 167 } else { 168 if r.id { 169 p.f("%d\n", build.Id) 170 } else { 171 if !first { 172 // Print a new line so it is easier to differentiate builds. 173 p.f("\n") 174 } 175 p.Build(build) 176 } 177 } 178 return p.Err 179 } 180 181 type runOrder int 182 183 const ( 184 unordered runOrder = iota 185 argOrder 186 ) 187 188 // PrintAndDone calls fn for each argument, prints builds and returns exit code. 189 // fn is called concurrently, but builds are printed in the same order 190 // as args. 191 func (r *printRun) PrintAndDone(ctx context.Context, args []string, order runOrder, fn buildFunc) int { 192 stdout, stderr := newStdioPrinters(r.noColor) 193 194 jobs := len(args) 195 if jobs == 0 { 196 jobs = 32 197 } 198 199 resultC := make(chan buildResult, 256) 200 go func() { 201 defer close(resultC) 202 argC := argChan(args) 203 if order == argOrder { 204 r.runOrdered(ctx, jobs, argC, resultC, fn) 205 } else { 206 r.runUnordered(ctx, jobs, argC, resultC, fn) 207 } 208 }() 209 210 // Print the results in the order of args. 211 first := true 212 perfect := true 213 for res := range resultC { 214 if res.err != nil { 215 perfect = false 216 if !first { 217 stderr.f("\n") 218 } 219 stderr.f("arg %q: ", res.arg) 220 stderr.Error(res.err) 221 stderr.f("\n") 222 if stderr.Err != nil { 223 return r.done(ctx, stderr.Err) 224 } 225 } else { 226 if err := r.printBuild(stdout, res.build, first); err != nil { 227 return r.done(ctx, err) 228 } 229 } 230 first = false 231 if r.eager { 232 // return upon the first build. 233 if !perfect { 234 return 1 235 } 236 return 0 237 } 238 } 239 if !perfect { 240 return 1 241 } 242 return 0 243 } 244 245 type buildResult struct { 246 arg string 247 build *pb.Build 248 err error 249 } 250 251 type buildFunc func(c context.Context, arg string) (*pb.Build, error) 252 253 // runOrdered runs fn for each arg in argC and reports results to resultC 254 // in the same order. 255 func (r *printRun) runOrdered(ctx context.Context, jobs int, argC <-chan string, resultC chan<- buildResult, fn buildFunc) { 256 // Prepare workspace. 257 type workItem struct { 258 arg string 259 build *pb.Build 260 done chan error 261 } 262 work := make(chan *workItem) 263 264 // Prepare concurrent workers. 265 for i := 0; i < jobs; i++ { 266 go func() { 267 for item := range work { 268 var err error 269 item.build, err = fn(ctx, item.arg) 270 item.done <- err 271 } 272 }() 273 } 274 275 // Add work. Close the workspace when the work is done. 276 resultItems := make(chan *workItem) 277 go func() { 278 for a := range argC { 279 item := &workItem{arg: a, done: make(chan error)} 280 work <- item 281 resultItems <- item 282 } 283 close(work) 284 close(resultItems) 285 }() 286 287 for i := range resultItems { 288 resultC <- buildResult{ 289 arg: i.arg, 290 build: i.build, 291 err: <-i.done, 292 } 293 } 294 } 295 296 // runUnordered is like runOrdered, but unordered. 297 func (r *printRun) runUnordered(ctx context.Context, jobs int, argC <-chan string, resultC chan<- buildResult, fn buildFunc) { 298 parallel.WorkPool(jobs, func(work chan<- func() error) { 299 for arg := range argC { 300 if ctx.Err() != nil { 301 break 302 } 303 arg := arg 304 work <- func() error { 305 build, err := fn(ctx, arg) 306 resultC <- buildResult{arg, build, err} 307 return nil 308 } 309 } 310 }) 311 } 312 313 // argChan returns a channel of args. 314 // 315 // If args is empty, reads from stdin. Trims whitespace and skips blank lines. 316 // Panics if reading from stdin fails. 317 func argChan(args []string) chan string { 318 ret := make(chan string) 319 go func() { 320 defer close(ret) 321 322 if len(args) > 0 { 323 for _, a := range args { 324 ret <- strings.TrimSpace(a) 325 } 326 return 327 } 328 329 reader := bufio.NewReader(os.Stdin) 330 for { 331 line, err := reader.ReadString('\n') 332 line = strings.TrimSpace(line) 333 switch { 334 case err == io.EOF: 335 return 336 case err != nil: 337 panic(err) 338 case len(line) == 0: 339 continue 340 default: 341 ret <- line 342 } 343 } 344 }() 345 return ret 346 }