github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/tool/tool.go (about) 1 // Package tool provides implementation of the debugging related operations 2 // supported by go/cmd/remotetool package. 3 package tool 4 5 import ( 6 "bufio" 7 "bytes" 8 "context" 9 "fmt" 10 "os" 11 "path/filepath" 12 "sort" 13 "strings" 14 "time" 15 16 log "github.com/golang/glog" 17 "github.com/pkg/errors" 18 "golang.org/x/sync/errgroup" 19 "google.golang.org/protobuf/encoding/prototext" 20 "google.golang.org/protobuf/proto" 21 22 "github.com/bazelbuild/remote-apis-sdks/go/pkg/cas" 23 rc "github.com/bazelbuild/remote-apis-sdks/go/pkg/client" 24 "github.com/bazelbuild/remote-apis-sdks/go/pkg/command" 25 "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" 26 "github.com/bazelbuild/remote-apis-sdks/go/pkg/filemetadata" 27 "github.com/bazelbuild/remote-apis-sdks/go/pkg/outerr" 28 "github.com/bazelbuild/remote-apis-sdks/go/pkg/rexec" 29 "github.com/bazelbuild/remote-apis-sdks/go/pkg/uploadinfo" 30 31 cpb "github.com/bazelbuild/remote-apis-sdks/go/api/command" 32 repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 33 ) 34 35 const ( 36 stdoutFile = "stdout" 37 stderrFile = "stderr" 38 ) 39 40 // testOnlyStartDeterminismExec 41 var testOnlyStartDeterminismExec = func() {} 42 43 // Client is a remote execution client. 44 type Client struct { 45 GrpcClient *rc.Client 46 } 47 48 // CheckDeterminism executes the action the given number of times and compares 49 // output digests, reporting failure if a mismatch is detected. 50 func (c *Client) CheckDeterminism(ctx context.Context, actionDigest, actionRoot string, attempts int) error { 51 oe := outerr.SystemOutErr 52 firstMd, firstRes := c.ExecuteAction(ctx, actionDigest, actionRoot, "", oe) 53 for i := 1; i < attempts; i++ { 54 testOnlyStartDeterminismExec() 55 md, res := c.ExecuteAction(ctx, actionDigest, actionRoot, "", oe) 56 gotErr := false 57 if (firstRes == nil) != (res == nil) { 58 log.Errorf("action does not produce a consistent result, got %v and %v from consecutive executions", res, firstRes) 59 gotErr = true 60 } 61 if len(md.OutputFileDigests) != len(firstMd.OutputFileDigests) { 62 log.Errorf("action does not produce a consistent number of outputs, got %v and %v from consecutive executions", len(md.OutputFileDigests), len(firstMd.OutputFileDigests)) 63 gotErr = true 64 } 65 for p, d := range md.OutputFileDigests { 66 firstD, ok := firstMd.OutputFileDigests[p] 67 if !ok { 68 log.Errorf("action does not produce %v consistently", p) 69 gotErr = true 70 continue 71 } 72 if d != firstD { 73 log.Errorf("action does not produce a consistent digest for %v, got %v and %v", p, d, firstD) 74 gotErr = true 75 continue 76 } 77 } 78 if gotErr { 79 return fmt.Errorf("action is not deterministic, check error log for more details") 80 } 81 } 82 return nil 83 } 84 85 func (c *Client) prepCommand(ctx context.Context, client *rexec.Client, actionDigest, actionRoot string) (*command.Command, error) { 86 acDg, err := digest.NewFromString(actionDigest) 87 if err != nil { 88 return nil, err 89 } 90 actionProto := &repb.Action{} 91 if _, err := c.GrpcClient.ReadProto(ctx, acDg, actionProto); err != nil { 92 return nil, err 93 } 94 95 commandProto := &repb.Command{} 96 cmdDg, err := digest.NewFromProto(actionProto.GetCommandDigest()) 97 if err != nil { 98 return nil, err 99 } 100 101 log.Infof("Reading command from action digest..") 102 if _, err := c.GrpcClient.ReadProto(ctx, cmdDg, commandProto); err != nil { 103 return nil, err 104 } 105 fetchInputs := actionRoot == "" 106 if fetchInputs { 107 curTime := time.Now().Format(time.RFC3339) 108 actionRoot = filepath.Join(os.TempDir(), acDg.Hash+"_"+curTime) 109 } 110 inputRoot := filepath.Join(actionRoot, "input") 111 var nodeProperties map[string]*cpb.NodeProperties 112 if fetchInputs { 113 dg, err := digest.NewFromProto(actionProto.GetInputRootDigest()) 114 if err != nil { 115 return nil, err 116 } 117 log.Infof("Fetching input tree from input root digest %s into %s", dg, inputRoot) 118 ts, _, err := c.GrpcClient.DownloadDirectory(ctx, dg, inputRoot, client.FileMetadataCache) 119 if err != nil { 120 return nil, err 121 } 122 nodeProperties = make(map[string]*cpb.NodeProperties) 123 for path, t := range ts { 124 if t.NodeProperties != nil { 125 nodeProperties[path] = command.NodePropertiesFromAPI(t.NodeProperties) 126 } 127 } 128 } else if nodeProperties, err = readNodePropertiesFromFile(filepath.Join(actionRoot, "input_node_properties.textproto")); err != nil { 129 return nil, err 130 } 131 contents, err := os.ReadDir(inputRoot) 132 if err != nil { 133 return nil, err 134 } 135 inputPaths := []string{} 136 for _, f := range contents { 137 inputPaths = append(inputPaths, f.Name()) 138 } 139 // Construct Command object. 140 cmd := command.FromREProto(commandProto) 141 cmd.InputSpec.Inputs = inputPaths 142 cmd.InputSpec.InputNodeProperties = nodeProperties 143 cmd.ExecRoot = inputRoot 144 if actionProto.Timeout != nil { 145 cmd.Timeout = actionProto.Timeout.AsDuration() 146 } 147 return cmd, nil 148 } 149 150 func readNodePropertiesFromFile(path string) (nps map[string]*cpb.NodeProperties, err error) { 151 if _, err = os.Stat(path); err != nil { 152 if !errors.Is(err, os.ErrNotExist) { 153 return nil, fmt.Errorf("error accessing input node properties file: %v", err) 154 } 155 return nil, nil 156 } 157 inTxt, err := os.ReadFile(path) 158 if err != nil { 159 return nil, fmt.Errorf("error reading input node properties from file: %v", err) 160 } 161 ipb := &cpb.InputSpec{} 162 if err := prototext.Unmarshal(inTxt, ipb); err != nil { 163 return nil, fmt.Errorf("error unmarshalling input node properties from file %s: %v", path, err) 164 } 165 return ipb.InputNodeProperties, nil 166 } 167 168 // DownloadActionResult downloads the action result of the given action digest 169 // if it exists in the remote cache. 170 func (c *Client) DownloadActionResult(ctx context.Context, actionDigest, pathPrefix string) error { 171 acDg, err := digest.NewFromString(actionDigest) 172 if err != nil { 173 return err 174 } 175 actionProto := &repb.Action{} 176 if _, err := c.GrpcClient.ReadProto(ctx, acDg, actionProto); err != nil { 177 return err 178 } 179 commandProto := &repb.Command{} 180 cmdDg, err := digest.NewFromProto(actionProto.GetCommandDigest()) 181 if err != nil { 182 return err 183 } 184 log.Infof("Reading command from action digest..") 185 if _, err := c.GrpcClient.ReadProto(ctx, cmdDg, commandProto); err != nil { 186 return err 187 } 188 // Construct Command object. 189 cmd := command.FromREProto(commandProto) 190 191 resPb, err := c.getActionResult(ctx, actionDigest) 192 if err != nil { 193 return err 194 } 195 if resPb == nil { 196 return fmt.Errorf("action digest %v not found in cache", actionDigest) 197 } 198 199 log.Infof("Cleaning contents of %v.", pathPrefix) 200 os.RemoveAll(pathPrefix) 201 os.Mkdir(pathPrefix, 0755) 202 203 log.Infof("Downloading action results of %v to %v.", actionDigest, pathPrefix) 204 // We don't really need an in-memory filemetadata cache for debugging operations. 205 noopCache := filemetadata.NewNoopCache() 206 if _, err := c.GrpcClient.DownloadActionOutputs(ctx, resPb, filepath.Join(pathPrefix, cmd.WorkingDir), noopCache); err != nil { 207 log.Errorf("Failed downloading action outputs: %v.", err) 208 } 209 210 // We have not requested for stdout/stderr to be inlined in GetActionResult, so the server 211 // should be returning the digest instead of sending raw data. 212 outMsgs := map[string]*repb.Digest{ 213 filepath.Join(pathPrefix, stdoutFile): resPb.StdoutDigest, 214 filepath.Join(pathPrefix, stderrFile): resPb.StderrDigest, 215 } 216 for path, reDg := range outMsgs { 217 if reDg == nil { 218 continue 219 } 220 dg := &digest.Digest{ 221 Hash: reDg.GetHash(), 222 Size: reDg.GetSizeBytes(), 223 } 224 log.Infof("Downloading stdout/stderr to %v.", path) 225 bytes, _, err := c.GrpcClient.ReadBlob(ctx, *dg) 226 if err != nil { 227 log.Errorf("Unable to read blob for %v with digest %v.", path, dg) 228 } 229 if err := os.WriteFile(path, bytes, 0644); err != nil { 230 log.Errorf("Unable to write output of digest %v to file %v.", dg, path) 231 } 232 } 233 log.Infof("Successfully downloaded results of %v to %v.", actionDigest, pathPrefix) 234 return nil 235 } 236 237 // DownloadBlob downloads a blob from the remote cache into the specified path. 238 // If the path is empty, it writes the contents to stdout instead. 239 func (c *Client) DownloadBlob(ctx context.Context, blobDigest, path string) (string, error) { 240 outputToStdout := false 241 if path == "" { 242 outputToStdout = true 243 // Create a temp file. 244 tmpFile, err := os.CreateTemp(os.TempDir(), "") 245 if err != nil { 246 return "", err 247 } 248 if err := tmpFile.Close(); err != nil { 249 return "", err 250 } 251 path = tmpFile.Name() 252 defer os.Remove(path) 253 } 254 dg, err := digest.NewFromString(blobDigest) 255 if err != nil { 256 return "", err 257 } 258 log.Infof("Downloading blob of %v to %v.", dg, path) 259 if _, err := c.GrpcClient.ReadBlobToFile(ctx, dg, path); err != nil { 260 return "", err 261 } 262 if !outputToStdout { 263 return "", nil 264 } 265 contents, err := os.ReadFile(path) 266 if err != nil { 267 return "", err 268 } 269 return string(contents), nil 270 } 271 272 // UploadBlob uploads a blob from the specified path into the remote cache. 273 func (c *Client) UploadBlob(ctx context.Context, path string) error { 274 dg, err := digest.NewFromFile(path) 275 if err != nil { 276 return err 277 } 278 279 log.Infof("Uploading blob of %v from %v.", dg, path) 280 ue := uploadinfo.EntryFromFile(dg, path) 281 if _, _, err := c.GrpcClient.UploadIfMissing(ctx, ue); err != nil { 282 return err 283 } 284 return nil 285 } 286 287 // UploadBlobV2 uploads a blob from the specified path into the remote cache using newer cas implementation. 288 func (c *Client) UploadBlobV2(ctx context.Context, path string) error { 289 casC, err := cas.NewClient(ctx, c.GrpcClient.Connection, c.GrpcClient.InstanceName) 290 if err != nil { 291 return errors.WithStack(err) 292 } 293 inputC := make(chan *cas.UploadInput) 294 295 eg, ctx := errgroup.WithContext(ctx) 296 297 eg.Go(func() error { 298 inputC <- &cas.UploadInput{ 299 Path: path, 300 } 301 close(inputC) 302 return nil 303 }) 304 305 eg.Go(func() error { 306 _, err := casC.Upload(ctx, cas.UploadOptions{}, inputC) 307 return errors.WithStack(err) 308 }) 309 310 return errors.WithStack(eg.Wait()) 311 } 312 313 // DownloadDirectory downloads a an input root from the remote cache into the specified path. 314 func (c *Client) DownloadDirectory(ctx context.Context, rootDigest, path string) error { 315 log.Infof("Cleaning contents of %v.", path) 316 os.RemoveAll(path) 317 os.Mkdir(path, 0755) 318 319 dg, err := digest.NewFromString(rootDigest) 320 if err != nil { 321 return err 322 } 323 log.Infof("Downloading input root %v to %v.", dg, path) 324 _, _, err = c.GrpcClient.DownloadDirectory(ctx, dg, path, filemetadata.NewNoopCache()) 325 return err 326 } 327 328 // UploadStats contains various metadata of a directory upload. 329 type UploadStats struct { 330 rc.TreeStats 331 RootDigest digest.Digest 332 CountBlobs int64 333 CountCacheMisses int64 334 BytesTransferred int64 335 BytesCacheMisses int64 336 Error string 337 } 338 339 // UploadDirectory uploads a directory from the specified path as a Merkle-tree to the remote cache. 340 func (c *Client) UploadDirectory(ctx context.Context, path string) (*UploadStats, error) { 341 log.Infof("Computing Merkle tree rooted at %s", path) 342 root, blobs, stats, err := c.GrpcClient.ComputeMerkleTree(ctx, path, "", "", &command.InputSpec{Inputs: []string{"."}}, filemetadata.NewNoopCache()) 343 if err != nil { 344 return &UploadStats{Error: err.Error()}, err 345 } 346 us := &UploadStats{ 347 TreeStats: *stats, 348 RootDigest: root, 349 CountBlobs: int64(len(blobs)), 350 } 351 log.Infof("Directory root digest: %v", root) 352 log.Infof("Directory stats: %d files, %d directories, %d symlinks, %d total bytes", stats.InputFiles, stats.InputDirectories, stats.InputSymlinks, stats.TotalInputBytes) 353 log.Infof("Uploading directory %v rooted at %s to CAS.", root, path) 354 missing, n, err := c.GrpcClient.UploadIfMissing(ctx, blobs...) 355 if err != nil { 356 us.Error = err.Error() 357 return us, err 358 } 359 var sumMissingBytes int64 360 for _, d := range missing { 361 sumMissingBytes += d.Size 362 } 363 us.CountCacheMisses = int64(len(missing)) 364 us.BytesTransferred = n 365 us.BytesCacheMisses = sumMissingBytes 366 return us, nil 367 } 368 369 func (c *Client) writeProto(m proto.Message, baseName string) error { 370 f, err := os.Create(baseName) 371 if err != nil { 372 return err 373 } 374 defer f.Close() 375 f.WriteString(prototext.Format(m)) 376 return nil 377 } 378 379 // DownloadAction parses and downloads an action to the given directory. 380 // The output directory will have the following: 381 // 1. ac.textproto: the action proto file in text format. 382 // 2. cmd.textproto: the command proto file in text format. 383 // 3. input/: the input tree root directory with all files under it. 384 // 4. input_node_properties.txtproto: all the NodeProperties defined on the 385 // input tree, as an InputSpec proto file in text format. Will be omitted 386 // if no NodeProperties are defined. 387 func (c *Client) DownloadAction(ctx context.Context, actionDigest, outputPath string, overwrite bool) error { 388 resPb, err := c.getActionResult(ctx, actionDigest) 389 if err != nil { 390 return err 391 } 392 acDg, err := digest.NewFromString(actionDigest) 393 if err != nil { 394 return err 395 } 396 actionProto := &repb.Action{} 397 log.Infof("Reading action..") 398 if _, err := c.GrpcClient.ReadProto(ctx, acDg, actionProto); err != nil { 399 return err 400 } 401 402 // Directory already exists, ask the user for confirmation before overwrite it. 403 if _, err := os.Stat(outputPath); !os.IsNotExist(err) { 404 fmt.Printf("Directory '%s' already exists. Do you want to overwrite it? (yes/no): ", outputPath) 405 if !overwrite { 406 reader := bufio.NewReader(os.Stdin) 407 input, err := reader.ReadString('\n') 408 if err != nil { 409 return fmt.Errorf("error reading user input: %v", err) 410 } 411 input = strings.TrimSpace(input) 412 input = strings.ToLower(input) 413 414 if !(input == "yes" || input == "y") { 415 return errors.Errorf("operation aborted.") 416 } 417 } 418 // If the user confirms, remove the existing directory and create a new one 419 err = os.RemoveAll(outputPath) 420 if err != nil { 421 return fmt.Errorf("error removing existing directory: %v", err) 422 } 423 } 424 // Directory doesn't exist, create it. 425 err = os.MkdirAll(outputPath, os.ModePerm) 426 if err != nil { 427 return fmt.Errorf("error creating the directory: %v", err) 428 } 429 log.Infof("Directory created: %v", outputPath) 430 431 if err := c.writeProto(actionProto, filepath.Join(outputPath, "ac.textproto")); err != nil { 432 return err 433 } 434 435 cmdDg, err := digest.NewFromProto(actionProto.GetCommandDigest()) 436 if err != nil { 437 return err 438 } 439 log.Infof("Reading command from action..") 440 commandProto := &repb.Command{} 441 if _, err := c.GrpcClient.ReadProto(ctx, cmdDg, commandProto); err != nil { 442 return err 443 } 444 if err := c.writeProto(commandProto, filepath.Join(outputPath, "cmd.textproto")); err != nil { 445 return err 446 } 447 448 if err := c.writeExecScript(ctx, commandProto, filepath.Join(outputPath, "run_locally.sh")); err != nil { 449 return err 450 } 451 452 log.Infof("Fetching input tree from input root digest.. %v", actionProto.GetInputRootDigest()) 453 rootPath := filepath.Join(outputPath, "input") 454 os.RemoveAll(rootPath) 455 os.Mkdir(rootPath, 0755) 456 rDg, err := digest.NewFromProto(actionProto.GetInputRootDigest()) 457 if err != nil { 458 return err 459 } 460 ts, _, err := c.GrpcClient.DownloadDirectory(ctx, rDg, rootPath, filemetadata.NewNoopCache()) 461 if err != nil { 462 return fmt.Errorf("error fetching input tree: %v", err) 463 } 464 is := &cpb.InputSpec{InputNodeProperties: make(map[string]*cpb.NodeProperties)} 465 for path, t := range ts { 466 if t.NodeProperties != nil { 467 is.InputNodeProperties[path] = command.NodePropertiesFromAPI(t.NodeProperties) 468 } 469 } 470 if len(is.InputNodeProperties) != 0 { 471 err = c.writeProto(is, filepath.Join(outputPath, "input_node_properties.textproto")) 472 if err != nil { 473 return err 474 } 475 } 476 res, err := c.formatAction(ctx, actionProto, resPb, commandProto, cmdDg) 477 if err != nil { 478 return fmt.Errorf("error formatting action %v", err) 479 } 480 err = os.WriteFile(filepath.Join(outputPath, "action.txt"), []byte(res), 0644) 481 if err != nil { 482 return fmt.Errorf("error dumping to action.txt] %v: %v", outputPath, err) 483 } 484 return nil 485 } 486 487 // shellSprintf is intended to add args sanitization before using them. 488 func shellSprintf(format string, args ...any) string { 489 // TODO: check args for flag injection 490 return fmt.Sprintf(format, args...) 491 } 492 493 func (c *Client) writeExecScript(ctx context.Context, cmd *repb.Command, filename string) error { 494 if cmd == nil { 495 return fmt.Errorf("invalid comment (nil)") 496 } 497 498 var runActionScript bytes.Buffer 499 runActionFilename := filepath.Join(filepath.Dir(filename), "run_command.sh") 500 wd := cmd.WorkingDirectory 501 cmdArgs := make([]string, len(cmd.GetArguments())) 502 for i, arg := range cmd.GetArguments() { 503 if strings.Contains(arg, " ") { 504 cmdArgs[i] = fmt.Sprintf("'%s'", arg) 505 continue 506 } 507 cmdArgs[i] = arg 508 } 509 execCmd := strings.Join(cmdArgs, " ") 510 runActionScript.WriteString(shellSprintf("#!/bin/bash\n\n")) 511 runActionScript.WriteString(shellSprintf("# This script is meant to be called by %v.\n", filename)) 512 if wd != "" { 513 runActionScript.WriteString(shellSprintf("cd %v\n", wd)) 514 } 515 for _, od := range cmd.GetOutputDirectories() { 516 runActionScript.WriteString(shellSprintf("mkdir -p %v\n", od)) 517 } 518 for _, of := range cmd.GetOutputFiles() { 519 runActionScript.WriteString(shellSprintf("mkdir -p %v\n", filepath.Dir(of))) 520 } 521 for _, e := range cmd.GetEnvironmentVariables() { 522 runActionScript.WriteString(shellSprintf("export %v=%v\n", e.GetName(), e.GetValue())) 523 } 524 runActionScript.WriteString(execCmd) 525 runActionScript.WriteRune('\n') 526 runActionScript.WriteString(shellSprintf("bash\n")) 527 if err := os.WriteFile(runActionFilename, runActionScript.Bytes(), 0755); err != nil { 528 return err 529 } 530 531 var container string 532 var dockerParams string 533 for _, property := range cmd.Platform.GetProperties() { 534 if property.Name == "container-image" { 535 container = strings.TrimPrefix(property.Value, "docker://") 536 continue 537 } 538 if property.Name == "dockerPrivileged" { 539 dockerParams = "--privileged" 540 } 541 } 542 if container == "" { 543 return fmt.Errorf("container-image platform property missing from command proto: %v", cmd) 544 } 545 var execScript bytes.Buffer 546 dockerCmd := shellSprintf("docker run -i -t -w /b/f/w -v `pwd`/input:/b/f/w -v `pwd`/run_command.sh:/b/f/w/run_command.sh %s %s ./run_command.sh\n", dockerParams, container) 547 execScript.WriteString(shellSprintf("#!/bin/bash\n\n")) 548 execScript.WriteString(shellSprintf("# This script can be used to run the action locally on\n")) 549 execScript.WriteString(shellSprintf("# this machine.\n")) 550 execScript.WriteString(shellSprintf("echo \"WARNING: The results from executing the action through this script may differ from results from RBE.\"\n")) 551 execScript.WriteString(shellSprintf("set -x\n")) 552 execScript.WriteString(dockerCmd) 553 return os.WriteFile(filename, execScript.Bytes(), 0755) 554 } 555 556 func (c *Client) prepProtos(ctx context.Context, actionRoot string) (string, error) { 557 cmdTxt, err := os.ReadFile(filepath.Join(actionRoot, "cmd.textproto")) 558 if err != nil { 559 return "", err 560 } 561 cmdPb := &repb.Command{} 562 if err := prototext.Unmarshal(cmdTxt, cmdPb); err != nil { 563 return "", err 564 } 565 ue, err := uploadinfo.EntryFromProto(cmdPb) 566 if err != nil { 567 return "", err 568 } 569 if _, _, err := c.GrpcClient.UploadIfMissing(ctx, ue); err != nil { 570 return "", err 571 } 572 ac, err := os.ReadFile(filepath.Join(actionRoot, "ac.textproto")) 573 if err != nil { 574 return "", err 575 } 576 acPb := &repb.Action{} 577 if err := prototext.Unmarshal(ac, acPb); err != nil { 578 return "", err 579 } 580 dg, err := digest.NewFromMessage(cmdPb) 581 if err != nil { 582 return "", err 583 } 584 acPb.CommandDigest = dg.ToProto() 585 ue, err = uploadinfo.EntryFromProto(acPb) 586 if err != nil { 587 return "", err 588 } 589 if _, _, err := c.GrpcClient.UploadIfMissing(ctx, ue); err != nil { 590 return "", err 591 } 592 dg, err = digest.NewFromMessage(acPb) 593 if err != nil { 594 return "", err 595 } 596 return dg.String(), nil 597 } 598 599 // ExecuteAction executes an action in a canonical structure remotely. 600 // The structure is the same as that produced by DownloadAction. 601 // top level > 602 // 603 // > ac.textproto (Action text proto) 604 // > cmd.textproto (Command text proto) 605 // > input_node_properties.textproto (InputSpec text proto, optional) 606 // > input (Input root) 607 // > inputs... 608 func (c *Client) ExecuteAction(ctx context.Context, actionDigest, actionRoot, outDir string, oe outerr.OutErr) (*command.Metadata, error) { 609 fmc := filemetadata.NewNoopCache() 610 client := &rexec.Client{ 611 FileMetadataCache: fmc, 612 GrpcClient: c.GrpcClient, 613 } 614 if actionRoot != "" { 615 var err error 616 if actionDigest, err = c.prepProtos(ctx, actionRoot); err != nil { 617 return nil, err 618 } 619 } 620 cmd, err := c.prepCommand(ctx, client, actionDigest, actionRoot) 621 if err != nil { 622 return nil, err 623 } 624 opt := &command.ExecutionOptions{AcceptCached: false, DownloadOutputs: false, DownloadOutErr: true} 625 ec, err := client.NewContext(ctx, cmd, opt, oe) 626 if err != nil { 627 return nil, err 628 } 629 ec.ExecuteRemotely() 630 fmt.Printf("Action complete\n") 631 fmt.Printf("---------------\n") 632 fmt.Printf("Action digest: %v\n", ec.Metadata.ActionDigest.String()) 633 fmt.Printf("Command digest: %v\n", ec.Metadata.CommandDigest.String()) 634 fmt.Printf("Stdout digest: %v\n", ec.Metadata.StdoutDigest.String()) 635 fmt.Printf("Stderr digest: %v\n", ec.Metadata.StderrDigest.String()) 636 fmt.Printf("Number of Input Files: %v\n", ec.Metadata.InputFiles) 637 fmt.Printf("Number of Input Dirs: %v\n", ec.Metadata.InputDirectories) 638 if len(cmd.InputSpec.InputNodeProperties) != 0 { 639 fmt.Printf("Number of Input Node Properties: %d\n", len(cmd.InputSpec.InputNodeProperties)) 640 } 641 fmt.Printf("Number of Output Files: %v\n", ec.Metadata.OutputFiles) 642 fmt.Printf("Number of Output Directories: %v\n", ec.Metadata.OutputDirectories) 643 switch ec.Result.Status { 644 case command.NonZeroExitResultStatus: 645 oe.WriteErr([]byte(fmt.Sprintf("Remote action FAILED with exit code %d.\n", ec.Result.ExitCode))) 646 case command.TimeoutResultStatus: 647 oe.WriteErr([]byte(fmt.Sprintf("Remote action TIMED OUT after %0f seconds.\n", cmd.Timeout.Seconds()))) 648 case command.InterruptedResultStatus: 649 oe.WriteErr([]byte(fmt.Sprintf("Remote execution was interrupted.\n"))) 650 case command.RemoteErrorResultStatus: 651 oe.WriteErr([]byte(fmt.Sprintf("Remote execution error: %v.\n", ec.Result.Err))) 652 case command.LocalErrorResultStatus: 653 oe.WriteErr([]byte(fmt.Sprintf("Local error: %v.\n", ec.Result.Err))) 654 } 655 if ec.Result.Err == nil && outDir != "" { 656 ec.DownloadOutputs(outDir) 657 fmt.Printf("Output written to %v\n", outDir) 658 } 659 return ec.Metadata, ec.Result.Err 660 } 661 662 // ShowAction parses and displays an action with its corresponding command. 663 func (c *Client) ShowAction(ctx context.Context, actionDigest string) (string, error) { 664 resPb, err := c.getActionResult(ctx, actionDigest) 665 if err != nil { 666 return "", err 667 } 668 669 acDg, err := digest.NewFromString(actionDigest) 670 if err != nil { 671 return "", err 672 } 673 actionProto := &repb.Action{} 674 if _, err := c.GrpcClient.ReadProto(ctx, acDg, actionProto); err != nil { 675 return "", err 676 } 677 commandProto := &repb.Command{} 678 cmdDg, err := digest.NewFromProto(actionProto.GetCommandDigest()) 679 if err != nil { 680 return "", err 681 } 682 log.Infof("Reading command from action digest..") 683 if _, err := c.GrpcClient.ReadProto(ctx, cmdDg, commandProto); err != nil { 684 return "", err 685 } 686 return c.formatAction(ctx, actionProto, resPb, commandProto, cmdDg) 687 } 688 689 func (c *Client) formatAction(ctx context.Context, actionProto *repb.Action, resPb *repb.ActionResult, commandProto *repb.Command, cmdDg digest.Digest) (string, error) { 690 var showActionRes bytes.Buffer 691 if actionProto.Timeout != nil { 692 timeout := actionProto.Timeout.AsDuration() 693 showActionRes.WriteString(fmt.Sprintf("Timeout: %s\n", timeout.String())) 694 } 695 showActionRes.WriteString("Command\n=======\n") 696 showActionRes.WriteString(fmt.Sprintf("Command Digest: %v\n", cmdDg)) 697 for _, ev := range commandProto.GetEnvironmentVariables() { 698 showActionRes.WriteString(fmt.Sprintf("\t%s=%s\n", ev.Name, ev.Value)) 699 } 700 cmdStr := strings.Join(commandProto.GetArguments(), " ") 701 showActionRes.WriteString(fmt.Sprintf("\t%v\n", cmdStr)) 702 showActionRes.WriteString("\nPlatform\n========\n") 703 for _, property := range commandProto.GetPlatform().GetProperties() { 704 showActionRes.WriteString(fmt.Sprintf("\t%s=%s\n", property.Name, property.Value)) 705 } 706 showActionRes.WriteString("\nInputs\n======\n") 707 log.Infof("Fetching input tree from input root digest..") 708 inpTree, _, err := c.getInputTree(ctx, actionProto.GetInputRootDigest()) 709 if err != nil { 710 showActionRes.WriteString("Failed to fetch input tree:\n") 711 showActionRes.WriteString(err.Error()) 712 showActionRes.WriteString("\n") 713 } else { 714 showActionRes.WriteString(inpTree) 715 } 716 717 if resPb == nil { 718 showActionRes.WriteString("\nNo action result in cache.\n") 719 } else { 720 log.Infof("Fetching output tree from action result..") 721 outs, err := c.getOutputs(ctx, resPb) 722 if err != nil { 723 return "", err 724 } 725 showActionRes.WriteString("\n") 726 showActionRes.WriteString(outs) 727 } 728 return showActionRes.String(), nil 729 } 730 731 func (c *Client) getOutputs(ctx context.Context, actionRes *repb.ActionResult) (string, error) { 732 var res bytes.Buffer 733 734 res.WriteString("------------------------------------------------------------------------\n") 735 res.WriteString("Action Result\n\n") 736 res.WriteString(fmt.Sprintf("Exit code: %d\n", actionRes.ExitCode)) 737 738 if actionRes.StdoutDigest != nil { 739 dg, err := digest.NewFromProto(actionRes.StdoutDigest) 740 if err != nil { 741 return "", err 742 } 743 res.WriteString(fmt.Sprintf("stdout digest: %v\n", dg)) 744 } 745 746 if actionRes.StderrDigest != nil { 747 dg, err := digest.NewFromProto(actionRes.StderrDigest) 748 if err != nil { 749 return "", err 750 } 751 res.WriteString(fmt.Sprintf("stderr digest: %v\n", dg)) 752 } 753 754 res.WriteString("\nOutput Files\n============\n") 755 for _, of := range actionRes.GetOutputFiles() { 756 dg, err := digest.NewFromProto(of.GetDigest()) 757 if err != nil { 758 return "", err 759 } 760 res.WriteString(fmt.Sprintf("%v, digest: %v\n", of.GetPath(), dg)) 761 } 762 763 res.WriteString("\nOutput Files From Directories\n=============================\n") 764 for _, od := range actionRes.GetOutputDirectories() { 765 treeDigest := od.GetTreeDigest() 766 dg, err := digest.NewFromProto(treeDigest) 767 if err != nil { 768 return "", err 769 } 770 outDirTree := &repb.Tree{} 771 if _, err := c.GrpcClient.ReadProto(ctx, dg, outDirTree); err != nil { 772 return "", err 773 } 774 775 outputs, _, err := c.flattenTree(ctx, outDirTree) 776 if err != nil { 777 return "", err 778 } 779 res.WriteString("\n") 780 res.WriteString(outputs) 781 } 782 783 return res.String(), nil 784 } 785 786 func (c *Client) getInputTree(ctx context.Context, root *repb.Digest) (string, []string, error) { 787 var res bytes.Buffer 788 789 dg, err := digest.NewFromProto(root) 790 if err != nil { 791 return "", nil, err 792 } 793 res.WriteString(fmt.Sprintf("[Root directory digest: %v]", dg)) 794 795 dirs, err := c.GrpcClient.GetDirectoryTree(ctx, root) 796 if err != nil { 797 return "", nil, err 798 } 799 if len(dirs) == 0 { 800 return "", nil, fmt.Errorf("Empty directories returned by GetTree for %v", dg) 801 } 802 t := &repb.Tree{ 803 Root: dirs[0], 804 Children: dirs, 805 } 806 inputs, paths, err := c.flattenTree(ctx, t) 807 if err != nil { 808 return "", nil, err 809 } 810 res.WriteString("\n") 811 res.WriteString(inputs) 812 813 return res.String(), paths, nil 814 } 815 816 func (c *Client) flattenTree(ctx context.Context, t *repb.Tree) (string, []string, error) { 817 var res bytes.Buffer 818 outputs, err := c.GrpcClient.FlattenTree(t, "") 819 if err != nil { 820 return "", nil, err 821 } 822 // Sort the values by path. 823 paths := make([]string, 0, len(outputs)) 824 for path := range outputs { 825 if path == "" { 826 path = "." 827 outputs[path] = outputs[""] 828 } 829 paths = append(paths, path) 830 } 831 sort.Strings(paths) 832 for _, path := range paths { 833 output := outputs[path] 834 var np string 835 if output.NodeProperties != nil { 836 np = fmt.Sprintf(" [Node properties: %v]", prototext.MarshalOptions{Multiline: false}.Format(output.NodeProperties)) 837 } 838 if output.IsEmptyDirectory { 839 res.WriteString(fmt.Sprintf("%v: [Directory digest: %v]%s\n", path, output.Digest, np)) 840 } else if output.SymlinkTarget != "" { 841 res.WriteString(fmt.Sprintf("%v: [Symlink digest: %v, Symlink Target: %v]%s\n", path, output.Digest, output.SymlinkTarget, np)) 842 } else { 843 res.WriteString(fmt.Sprintf("%v: [File digest: %v]%s\n", path, output.Digest, np)) 844 } 845 } 846 return res.String(), paths, nil 847 } 848 849 func (c *Client) getActionResult(ctx context.Context, actionDigest string) (*repb.ActionResult, error) { 850 acDg, err := digest.NewFromString(actionDigest) 851 if err != nil { 852 return nil, err 853 } 854 d := &repb.Digest{ 855 Hash: acDg.Hash, 856 SizeBytes: acDg.Size, 857 } 858 resPb, err := c.GrpcClient.CheckActionCache(ctx, d) 859 if err != nil { 860 return nil, err 861 } 862 return resPb, nil 863 }