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  }