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  }