github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/rexec/rexec.go (about)

     1  // Package rexec provides a top-level client for executing remote commands.
     2  package rexec
     3  
     4  import (
     5  	"context"
     6  	"fmt"
     7  	"path/filepath"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/command"
    13  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/digest"
    14  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/filemetadata"
    15  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/outerr"
    16  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/symlinkopts"
    17  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/uploadinfo"
    18  	"google.golang.org/grpc/codes"
    19  	"google.golang.org/grpc/status"
    20  	"google.golang.org/protobuf/encoding/prototext"
    21  
    22  	rc "github.com/bazelbuild/remote-apis-sdks/go/pkg/client"
    23  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/contextmd"
    24  	repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
    25  	log "github.com/golang/glog"
    26  	dpb "google.golang.org/protobuf/types/known/durationpb"
    27  	tspb "google.golang.org/protobuf/types/known/timestamppb"
    28  )
    29  
    30  // Client is a remote execution client.
    31  type Client struct {
    32  	FileMetadataCache filemetadata.Cache
    33  	GrpcClient        *rc.Client
    34  }
    35  
    36  // Context allows more granular control over various stages of command execution.
    37  // At any point, any errors that occurred will be stored in the Result.
    38  type Context struct {
    39  	ctx         context.Context
    40  	cmd         *command.Command
    41  	opt         *command.ExecutionOptions
    42  	oe          outerr.OutErr
    43  	client      *Client
    44  	inputBlobs  []*uploadinfo.Entry
    45  	cmdUe, acUe *uploadinfo.Entry
    46  	resPb       *repb.ActionResult
    47  	// The metadata of the current execution.
    48  	Metadata *command.Metadata
    49  	// The result of the current execution, if available.
    50  	Result *command.Result
    51  }
    52  
    53  // NewContext starts a new Context for a given command.
    54  func (c *Client) NewContext(ctx context.Context, cmd *command.Command, opt *command.ExecutionOptions, oe outerr.OutErr) (*Context, error) {
    55  	cmd.FillDefaultFieldValues()
    56  	if err := cmd.Validate(); err != nil {
    57  		return nil, err
    58  	}
    59  	grpcCtx, err := contextmd.WithMetadata(ctx, &contextmd.Metadata{
    60  		ToolName:               cmd.Identifiers.ToolName,
    61  		ToolVersion:            cmd.Identifiers.ToolVersion,
    62  		ActionID:               cmd.Identifiers.CommandID,
    63  		InvocationID:           cmd.Identifiers.InvocationID,
    64  		CorrelatedInvocationID: cmd.Identifiers.CorrelatedInvocationID,
    65  	})
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	return &Context{
    70  		ctx:      grpcCtx,
    71  		cmd:      cmd,
    72  		opt:      opt,
    73  		oe:       oe,
    74  		client:   c,
    75  		Metadata: &command.Metadata{EventTimes: make(map[string]*command.TimeInterval)},
    76  	}, nil
    77  }
    78  
    79  // downloadStream reads the blob for the digest dgPb into memory and forwards the bytes to the write function.
    80  func (ec *Context) downloadStream(raw []byte, dgPb *repb.Digest, offset int64, write func([]byte)) error {
    81  	if raw != nil {
    82  		o := int(offset)
    83  		if int64(o) != offset || o > len(raw) {
    84  			return fmt.Errorf("offset %d is out of range for length %d", offset, len(raw))
    85  		}
    86  		write(raw[o:])
    87  	} else if dgPb != nil {
    88  		dg, err := digest.NewFromProto(dgPb)
    89  		if err != nil {
    90  			return err
    91  		}
    92  		bytes, stats, err := ec.client.GrpcClient.ReadBlobRange(ec.ctx, dg, offset, 0)
    93  		if err != nil {
    94  			return err
    95  		}
    96  		ec.Metadata.LogicalBytesDownloaded += stats.LogicalMoved
    97  		ec.Metadata.RealBytesDownloaded += stats.RealMoved
    98  		write(bytes)
    99  	}
   100  	return nil
   101  }
   102  
   103  func (ec *Context) setOutputMetadata() {
   104  	if ec.resPb == nil {
   105  		return
   106  	}
   107  	ec.Metadata.OutputFiles = len(ec.resPb.OutputFiles) + len(ec.resPb.OutputFileSymlinks)
   108  	ec.Metadata.OutputDirectories = len(ec.resPb.OutputDirectories) + len(ec.resPb.OutputDirectorySymlinks)
   109  	ec.Metadata.OutputFileDigests = make(map[string]digest.Digest)
   110  	ec.Metadata.OutputDirectoryDigests = make(map[string]digest.Digest)
   111  	ec.Metadata.OutputSymlinks = make(map[string]string)
   112  	ec.Metadata.TotalOutputBytes = 0
   113  	for _, file := range ec.resPb.OutputFiles {
   114  		dg := digest.NewFromProtoUnvalidated(file.Digest)
   115  		ec.Metadata.OutputFileDigests[file.Path] = dg
   116  		ec.Metadata.TotalOutputBytes += dg.Size
   117  	}
   118  	for _, dir := range ec.resPb.OutputDirectories {
   119  		dg := digest.NewFromProtoUnvalidated(dir.TreeDigest)
   120  		ec.Metadata.OutputDirectoryDigests[dir.Path] = dg
   121  		ec.Metadata.TotalOutputBytes += dg.Size
   122  	}
   123  	for _, sl := range ec.resPb.OutputFileSymlinks {
   124  		ec.Metadata.OutputSymlinks[sl.Path] = sl.Target
   125  	}
   126  	if ec.resPb.StdoutRaw != nil {
   127  		ec.Metadata.TotalOutputBytes += int64(len(ec.resPb.StdoutRaw))
   128  	} else if ec.resPb.StdoutDigest != nil {
   129  		ec.Metadata.TotalOutputBytes += ec.resPb.StdoutDigest.SizeBytes
   130  	}
   131  	if ec.resPb.StderrRaw != nil {
   132  		ec.Metadata.TotalOutputBytes += int64(len(ec.resPb.StderrRaw))
   133  	} else if ec.resPb.StderrDigest != nil {
   134  		ec.Metadata.TotalOutputBytes += ec.resPb.StderrDigest.SizeBytes
   135  	}
   136  	if ec.resPb.StdoutDigest != nil {
   137  		ec.Metadata.StdoutDigest = digest.NewFromProtoUnvalidated(ec.resPb.StdoutDigest)
   138  	}
   139  	if ec.resPb.StderrDigest != nil {
   140  		ec.Metadata.StderrDigest = digest.NewFromProtoUnvalidated(ec.resPb.StderrDigest)
   141  	}
   142  }
   143  
   144  func (ec *Context) downloadOutErr() *command.Result {
   145  	if err := ec.downloadStream(ec.resPb.StdoutRaw, ec.resPb.StdoutDigest, 0, ec.oe.WriteOut); err != nil {
   146  		return command.NewRemoteErrorResult(err)
   147  	}
   148  	if err := ec.downloadStream(ec.resPb.StderrRaw, ec.resPb.StderrDigest, 0, ec.oe.WriteErr); err != nil {
   149  		return command.NewRemoteErrorResult(err)
   150  	}
   151  	return command.NewResultFromExitCode((int)(ec.resPb.ExitCode))
   152  }
   153  
   154  func (ec *Context) downloadOutputs(outDir string) (*rc.MovedBytesMetadata, *command.Result) {
   155  	ec.Metadata.EventTimes[command.EventDownloadResults] = &command.TimeInterval{From: time.Now()}
   156  	defer func() { ec.Metadata.EventTimes[command.EventDownloadResults].To = time.Now() }()
   157  	if !ec.client.GrpcClient.LegacyExecRootRelativeOutputs {
   158  		outDir = filepath.Join(outDir, ec.cmd.WorkingDir)
   159  	}
   160  	stats, err := ec.client.GrpcClient.DownloadActionOutputs(ec.ctx, ec.resPb, outDir, ec.client.FileMetadataCache)
   161  	if err != nil {
   162  		return &rc.MovedBytesMetadata{}, command.NewRemoteErrorResult(err)
   163  	}
   164  	return stats, command.NewResultFromExitCode((int)(ec.resPb.ExitCode))
   165  }
   166  
   167  func (ec *Context) computeCmdDg() (*repb.Platform, error) {
   168  	cmdID, executionID := ec.cmd.Identifiers.ExecutionID, ec.cmd.Identifiers.CommandID
   169  	commandHasOutputPathsField := ec.client.GrpcClient.SupportsCommandOutputPaths()
   170  	cmdPb := ec.cmd.ToREProto(commandHasOutputPathsField)
   171  	log.V(2).Infof("%s %s> Command: \n%s\n", cmdID, executionID, prototext.Format(cmdPb))
   172  	var err error
   173  	if ec.cmdUe, err = uploadinfo.EntryFromProto(cmdPb); err != nil {
   174  		return nil, err
   175  	}
   176  	cmdDg := ec.cmdUe.Digest
   177  	ec.Metadata.CommandDigest = cmdDg
   178  	log.V(1).Infof("%s %s> Command digest: %s", cmdID, executionID, cmdDg)
   179  	return cmdPb.Platform, nil
   180  }
   181  
   182  func (ec *Context) computeActionDg(rootDg digest.Digest, platform *repb.Platform) error {
   183  	acPb := &repb.Action{
   184  		CommandDigest:   ec.cmdUe.Digest.ToProto(),
   185  		InputRootDigest: rootDg.ToProto(),
   186  		DoNotCache:      ec.opt.DoNotCache,
   187  	}
   188  	// If supported, we attach a copy of the platform properties list to the Action.
   189  	if ec.client.GrpcClient.SupportsActionPlatformProperties() {
   190  		acPb.Platform = platform
   191  	}
   192  
   193  	if ec.cmd.Timeout > 0 {
   194  		acPb.Timeout = dpb.New(ec.cmd.Timeout)
   195  	}
   196  	var err error
   197  	if ec.acUe, err = uploadinfo.EntryFromProto(acPb); err != nil {
   198  		return err
   199  	}
   200  	return nil
   201  }
   202  
   203  func (ec *Context) computeInputs() error {
   204  	cmdID, executionID := ec.cmd.Identifiers.ExecutionID, ec.cmd.Identifiers.CommandID
   205  	if ec.Metadata.ActionDigest.Size > 0 {
   206  		// Already computed inputs.
   207  		log.V(1).Infof("%s %s> Inputs already uploaded", cmdID, executionID)
   208  		return nil
   209  	}
   210  
   211  	ec.Metadata.EventTimes[command.EventComputeMerkleTree] = &command.TimeInterval{From: time.Now()}
   212  	defer func() { ec.Metadata.EventTimes[command.EventComputeMerkleTree].To = time.Now() }()
   213  	cmdPlatform, err := ec.computeCmdDg()
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	log.V(1).Infof("%s %s> Computing input Merkle tree...", cmdID, executionID)
   219  	execRoot, workingDir, remoteWorkingDir := ec.cmd.ExecRoot, ec.cmd.WorkingDir, ec.cmd.RemoteWorkingDir
   220  	root, blobs, stats, err := ec.client.GrpcClient.ComputeMerkleTree(ec.ctx, execRoot, workingDir, remoteWorkingDir, ec.cmd.InputSpec, ec.client.FileMetadataCache)
   221  	if err != nil {
   222  		return err
   223  	}
   224  	ec.inputBlobs = blobs
   225  	ec.Metadata.InputFiles = stats.InputFiles
   226  	ec.Metadata.InputDirectories = stats.InputDirectories
   227  	ec.Metadata.TotalInputBytes = stats.TotalInputBytes
   228  	err = ec.computeActionDg(root, cmdPlatform)
   229  	if err != nil {
   230  		return err
   231  	}
   232  	log.V(1).Infof("%s %s> Action digest: %s", cmdID, executionID, ec.acUe.Digest)
   233  	ec.inputBlobs = append(ec.inputBlobs, ec.cmdUe)
   234  	ec.inputBlobs = append(ec.inputBlobs, ec.acUe)
   235  	ec.Metadata.ActionDigest = ec.acUe.Digest
   236  	ec.Metadata.TotalInputBytes += ec.cmdUe.Digest.Size + ec.acUe.Digest.Size
   237  	return nil
   238  }
   239  
   240  func symlinkOpts(treeOpts *rc.TreeSymlinkOpts, cmdOpts command.SymlinkBehaviorType) symlinkopts.Options {
   241  	if treeOpts == nil {
   242  		treeOpts = rc.DefaultTreeSymlinkOpts()
   243  	}
   244  	slPreserve := treeOpts.Preserved
   245  	switch cmdOpts {
   246  	case command.ResolveSymlink:
   247  		slPreserve = false
   248  	case command.PreserveSymlink:
   249  		slPreserve = true
   250  	}
   251  
   252  	switch {
   253  	case slPreserve && treeOpts.FollowsTarget && treeOpts.MaterializeOutsideExecRoot:
   254  		return symlinkopts.ResolveExternalOnlyWithTarget()
   255  	case slPreserve && treeOpts.FollowsTarget:
   256  		return symlinkopts.PreserveWithTarget()
   257  	case slPreserve && treeOpts.MaterializeOutsideExecRoot:
   258  		return symlinkopts.ResolveExternalOnly()
   259  	case slPreserve:
   260  		return symlinkopts.PreserveNoDangling()
   261  	default:
   262  		return symlinkopts.ResolveAlways()
   263  	}
   264  }
   265  
   266  // GetCachedResult tries to get the command result from the cache. The Result will be nil on a
   267  // cache miss. The Context will be ready to execute the action, or, alternatively, to
   268  // update the remote cache with a local result. If the ExecutionOptions do not allow to accept
   269  // remotely cached results, the operation is a noop.
   270  func (ec *Context) GetCachedResult() {
   271  	if err := ec.computeInputs(); err != nil {
   272  		ec.Result = command.NewLocalErrorResult(err)
   273  		return
   274  	}
   275  	if ec.opt.AcceptCached && !ec.opt.DoNotCache {
   276  		ec.Metadata.EventTimes[command.EventCheckActionCache] = &command.TimeInterval{From: time.Now()}
   277  		resPb, err := ec.client.GrpcClient.CheckActionCache(ec.ctx, ec.Metadata.ActionDigest.ToProto())
   278  		ec.Metadata.EventTimes[command.EventCheckActionCache].To = time.Now()
   279  		if err != nil {
   280  			ec.Result = command.NewRemoteErrorResult(err)
   281  			return
   282  		}
   283  		ec.resPb = resPb
   284  	}
   285  	if ec.resPb != nil {
   286  		ec.Result = command.NewResultFromExitCode((int)(ec.resPb.ExitCode))
   287  		ec.setOutputMetadata()
   288  		cmdID, executionID := ec.cmd.Identifiers.ExecutionID, ec.cmd.Identifiers.CommandID
   289  		log.V(1).Infof("%s %s> Found cached result, downloading outputs...", cmdID, executionID)
   290  		if ec.opt.DownloadOutErr {
   291  			ec.Result = ec.downloadOutErr()
   292  		}
   293  		if ec.Result.Err == nil && ec.opt.DownloadOutputs {
   294  			stats, res := ec.downloadOutputs(ec.cmd.ExecRoot)
   295  			ec.Metadata.LogicalBytesDownloaded += stats.LogicalMoved
   296  			ec.Metadata.RealBytesDownloaded += stats.RealMoved
   297  			ec.Result = res
   298  		}
   299  		if ec.Result.Err == nil {
   300  			ec.Result.Status = command.CacheHitResultStatus
   301  		}
   302  		return
   303  	}
   304  	ec.Result = nil
   305  }
   306  
   307  // UpdateCachedResult tries to write local results of the execution to the remote cache.
   308  // TODO(olaola): optional arguments to override values of local outputs, and also stdout/err.
   309  func (ec *Context) UpdateCachedResult() {
   310  	cmdID, executionID := ec.cmd.Identifiers.ExecutionID, ec.cmd.Identifiers.CommandID
   311  	ec.Result = &command.Result{Status: command.SuccessResultStatus}
   312  	if ec.opt.DoNotCache {
   313  		log.V(1).Infof("%s %s> Command is marked do-not-cache, skipping remote caching.", cmdID, executionID)
   314  		return
   315  	}
   316  	if err := ec.computeInputs(); err != nil {
   317  		ec.Result = command.NewLocalErrorResult(err)
   318  		return
   319  	}
   320  	ec.Metadata.EventTimes[command.EventUpdateCachedResult] = &command.TimeInterval{From: time.Now()}
   321  	defer func() { ec.Metadata.EventTimes[command.EventUpdateCachedResult].To = time.Now() }()
   322  	outPaths := append(ec.cmd.OutputFiles, ec.cmd.OutputDirs...)
   323  	wd := ""
   324  	if !ec.client.GrpcClient.LegacyExecRootRelativeOutputs {
   325  		wd = ec.cmd.WorkingDir
   326  	}
   327  	blobs, resPb, err := ec.client.GrpcClient.ComputeOutputsToUpload(ec.cmd.ExecRoot, wd, outPaths, ec.client.FileMetadataCache, ec.cmd.InputSpec.SymlinkBehavior, ec.cmd.InputSpec.InputNodeProperties)
   328  	if err != nil {
   329  		ec.Result = command.NewLocalErrorResult(err)
   330  		return
   331  	}
   332  	ec.resPb = resPb
   333  	ec.setOutputMetadata()
   334  	toUpload := []*uploadinfo.Entry{ec.acUe, ec.cmdUe}
   335  	for _, ch := range blobs {
   336  		toUpload = append(toUpload, ch)
   337  	}
   338  	log.V(1).Infof("%s %s> Uploading local outputs...", cmdID, executionID)
   339  	missing, bytesMoved, err := ec.client.GrpcClient.UploadIfMissing(ec.ctx, toUpload...)
   340  	if err != nil {
   341  		ec.Result = command.NewRemoteErrorResult(err)
   342  		return
   343  	}
   344  
   345  	ec.Metadata.MissingDigests = missing
   346  	for _, d := range missing {
   347  		ec.Metadata.LogicalBytesUploaded += d.Size
   348  	}
   349  	ec.Metadata.RealBytesUploaded = bytesMoved
   350  	log.V(1).Infof("%s %s> Updating remote cache...", cmdID, executionID)
   351  	req := &repb.UpdateActionResultRequest{
   352  		InstanceName: ec.client.GrpcClient.InstanceName,
   353  		ActionDigest: ec.Metadata.ActionDigest.ToProto(),
   354  		ActionResult: resPb,
   355  	}
   356  	if _, err := ec.client.GrpcClient.UpdateActionResult(ec.ctx, req); err != nil {
   357  		ec.Result = command.NewRemoteErrorResult(err)
   358  		return
   359  	}
   360  }
   361  
   362  // ExecuteRemotely tries to execute the command remotely and download the results. It uploads any
   363  // missing inputs first.
   364  func (ec *Context) ExecuteRemotely() {
   365  	if err := ec.computeInputs(); err != nil {
   366  		ec.Result = command.NewLocalErrorResult(err)
   367  		return
   368  	}
   369  
   370  	cmdID, executionID := ec.cmd.Identifiers.ExecutionID, ec.cmd.Identifiers.CommandID
   371  	log.V(1).Infof("%s %s> Checking inputs to upload...", cmdID, executionID)
   372  	// TODO(olaola): compute input cache hit stats.
   373  	ec.Metadata.EventTimes[command.EventUploadInputs] = &command.TimeInterval{From: time.Now()}
   374  	missing, bytesMoved, err := ec.client.GrpcClient.UploadIfMissing(ec.ctx, ec.inputBlobs...)
   375  	ec.Metadata.EventTimes[command.EventUploadInputs].To = time.Now()
   376  	if err != nil {
   377  		ec.Result = command.NewRemoteErrorResult(err)
   378  		return
   379  	}
   380  	ec.Metadata.MissingDigests = missing
   381  	for _, d := range missing {
   382  		ec.Metadata.LogicalBytesUploaded += d.Size
   383  	}
   384  	ec.Metadata.RealBytesUploaded = bytesMoved
   385  
   386  	log.V(1).Infof("%s %s> Executing remotely...\n%s", cmdID, executionID, strings.Join(ec.cmd.Args, " "))
   387  	ec.Metadata.EventTimes[command.EventExecuteRemotely] = &command.TimeInterval{From: time.Now()}
   388  	// Initiate each streaming request once at most.
   389  	var streamOut, streamErr sync.Once
   390  	var streamWg sync.WaitGroup
   391  	// These variables are owned by the progress callback (which is async but not concurrent) until the execution returns.
   392  	var nOutStreamed, nErrStreamed int64
   393  	op, err := ec.client.GrpcClient.ExecuteAndWaitProgress(ec.ctx, &repb.ExecuteRequest{
   394  		InstanceName:    ec.client.GrpcClient.InstanceName,
   395  		SkipCacheLookup: !ec.opt.AcceptCached || ec.opt.DoNotCache,
   396  		ActionDigest:    ec.Metadata.ActionDigest.ToProto(),
   397  	}, func(md *repb.ExecuteOperationMetadata) {
   398  		if !ec.opt.StreamOutErr {
   399  			return
   400  		}
   401  		// The server may return either, both, or neither of the stream names, and not necessarily in the same or first call.
   402  		// The streaming request for each must be initiated once at most.
   403  		if name := md.GetStdoutStreamName(); name != "" {
   404  			streamOut.Do(func() {
   405  				streamWg.Add(1)
   406  				go func() {
   407  					defer streamWg.Done()
   408  					path, _ := ec.client.GrpcClient.ResourceName("logstreams", name)
   409  					log.V(1).Infof("%s %s> Streaming to stdout from %q", cmdID, executionID, path)
   410  					// Ignoring the error here since the net result is downloading the full stream after the fact.
   411  					n, err := ec.client.GrpcClient.ReadResourceTo(ec.ctx, path, outerr.NewOutWriter(ec.oe))
   412  					if err != nil {
   413  						log.Errorf("%s %s> error streaming stdout: %v", cmdID, executionID, err)
   414  					}
   415  					nOutStreamed += n
   416  				}()
   417  			})
   418  		}
   419  		if name := md.GetStderrStreamName(); name != "" {
   420  			streamErr.Do(func() {
   421  				streamWg.Add(1)
   422  				go func() {
   423  					defer streamWg.Done()
   424  					path, _ := ec.client.GrpcClient.ResourceName("logstreams", name)
   425  					log.V(1).Infof("%s %s> Streaming to stdout from %q", cmdID, executionID, path)
   426  					// Ignoring the error here since the net result is downloading the full stream after the fact.
   427  					n, err := ec.client.GrpcClient.ReadResourceTo(ec.ctx, path, outerr.NewErrWriter(ec.oe))
   428  					if err != nil {
   429  						log.Errorf("%s %s> error streaming stderr: %v", cmdID, executionID, err)
   430  					}
   431  					nErrStreamed += n
   432  				}()
   433  			})
   434  		}
   435  	})
   436  	ec.Metadata.EventTimes[command.EventExecuteRemotely].To = time.Now()
   437  	// This will always be called after both of the Add calls above if any, because the execution call above returns
   438  	// after all invokations of the progress callback.
   439  	// The server will terminate the streams when the execution finishes, regardless of its result, which will ensure the goroutines
   440  	// will have terminated at this point.
   441  	streamWg.Wait()
   442  	if err != nil {
   443  		ec.Result = command.NewRemoteErrorResult(err)
   444  		return
   445  	}
   446  
   447  	or := op.GetResponse()
   448  	if or == nil {
   449  		ec.Result = command.NewRemoteErrorResult(fmt.Errorf("unexpected operation result type: %v", or))
   450  		return
   451  	}
   452  
   453  	resp := &repb.ExecuteResponse{}
   454  	if err := or.UnmarshalTo(resp); err != nil {
   455  		ec.Result = command.NewRemoteErrorResult(err)
   456  		return
   457  	}
   458  	ec.resPb = resp.Result
   459  	setTimingMetadata(ec.Metadata, resp.Result.GetExecutionMetadata())
   460  	setAuxiliaryMetadata(ec.Metadata, resp.Result.GetExecutionMetadata())
   461  	st := status.FromProto(resp.Status)
   462  	message := resp.Message
   463  	if message != "" && (st.Code() != codes.OK || ec.resPb != nil && ec.resPb.ExitCode != 0) {
   464  		ec.oe.WriteErr([]byte(message + "\n"))
   465  	}
   466  
   467  	if ec.resPb != nil {
   468  		ec.setOutputMetadata()
   469  		ec.Result = command.NewResultFromExitCode((int)(ec.resPb.ExitCode))
   470  		if ec.opt.DownloadOutErr {
   471  			if nOutStreamed < int64(len(ec.resPb.StdoutRaw)) || nOutStreamed < ec.resPb.GetStdoutDigest().GetSizeBytes() {
   472  				if err := ec.downloadStream(ec.resPb.StdoutRaw, ec.resPb.StdoutDigest, nOutStreamed, ec.oe.WriteOut); err != nil {
   473  					ec.Result = command.NewRemoteErrorResult(err)
   474  				}
   475  			}
   476  			if nErrStreamed < int64(len(ec.resPb.StderrRaw)) || nErrStreamed < ec.resPb.GetStderrDigest().GetSizeBytes() {
   477  				if err := ec.downloadStream(ec.resPb.StderrRaw, ec.resPb.StderrDigest, nErrStreamed, ec.oe.WriteErr); err != nil {
   478  					ec.Result = command.NewRemoteErrorResult(err)
   479  				}
   480  			}
   481  		}
   482  		if ec.Result.Err == nil && ec.opt.DownloadOutputs {
   483  			log.V(1).Infof("%s %s> Downloading outputs...", cmdID, executionID)
   484  			stats, res := ec.downloadOutputs(ec.cmd.ExecRoot)
   485  			ec.Metadata.LogicalBytesDownloaded += stats.LogicalMoved
   486  			ec.Metadata.RealBytesDownloaded += stats.RealMoved
   487  			ec.Result = res
   488  		}
   489  		if resp.CachedResult && ec.Result.Err == nil {
   490  			ec.Result.Status = command.CacheHitResultStatus
   491  		}
   492  	}
   493  
   494  	if st.Code() == codes.DeadlineExceeded {
   495  		ec.Result = command.NewTimeoutResult()
   496  		return
   497  	}
   498  	if st.Code() != codes.OK {
   499  		ec.Result = command.NewRemoteErrorResult(rc.StatusDetailedError(st))
   500  		return
   501  	}
   502  	if ec.resPb == nil {
   503  		ec.Result = command.NewRemoteErrorResult(fmt.Errorf("execute did not return action result"))
   504  	}
   505  }
   506  
   507  // DownloadOutErr downloads the stdout and stderr of the command.
   508  func (ec *Context) DownloadOutErr() {
   509  	st := ec.Result.Status
   510  	ec.Result = ec.downloadOutErr()
   511  	if ec.Result.Err == nil {
   512  		ec.Result.Status = st
   513  	}
   514  }
   515  
   516  // DownloadOutputs downloads the outputs of the command in the context to the specified directory.
   517  func (ec *Context) DownloadOutputs(outputDir string) {
   518  	st := ec.Result.Status
   519  	stats, res := ec.downloadOutputs(outputDir)
   520  	ec.Metadata.LogicalBytesDownloaded += stats.LogicalMoved
   521  	ec.Metadata.RealBytesDownloaded += stats.RealMoved
   522  	ec.Result = res
   523  	if ec.Result.Err == nil {
   524  		ec.Result.Status = st
   525  	}
   526  }
   527  
   528  // DownloadSpecifiedOutputs downloads the specified outputs into the specified directory
   529  // This function is run when the option to preserve unchanged outputs is on
   530  func (ec *Context) DownloadSpecifiedOutputs(outs map[string]*rc.TreeOutput, outDir string) {
   531  	st := ec.Result.Status
   532  	ec.Metadata.EventTimes[command.EventDownloadResults] = &command.TimeInterval{From: time.Now()}
   533  	outDir = filepath.Join(outDir, ec.cmd.WorkingDir)
   534  	stats, err := ec.client.GrpcClient.DownloadOutputs(ec.ctx, outs, outDir, ec.client.FileMetadataCache)
   535  	if err != nil {
   536  		stats = &rc.MovedBytesMetadata{}
   537  		ec.Result = command.NewRemoteErrorResult(err)
   538  	} else {
   539  		ec.Result = command.NewResultFromExitCode((int)(ec.resPb.ExitCode))
   540  	}
   541  	ec.Metadata.EventTimes[command.EventDownloadResults].To = time.Now()
   542  	ec.Metadata.LogicalBytesDownloaded += stats.LogicalMoved
   543  	ec.Metadata.RealBytesDownloaded += stats.RealMoved
   544  	if ec.Result.Err == nil {
   545  		ec.Result.Status = st
   546  	}
   547  }
   548  
   549  // GetFlattenedOutputs flattens the outputs from the ActionResult of the context and returns
   550  // a map of output paths relative to the working directory and their corresponding TreeOutput
   551  func (ec *Context) GetFlattenedOutputs() (map[string]*rc.TreeOutput, error) {
   552  	out, err := ec.client.GrpcClient.FlattenActionOutputs(ec.ctx, ec.resPb)
   553  	if err != nil {
   554  		return nil, fmt.Errorf("Failed to flatten outputs: %v", err)
   555  	}
   556  	return out, nil
   557  }
   558  
   559  // GetOutputFileDigests returns a map of output file paths to digests.
   560  // This function is supposed to be run after a successful cache-hit / remote-execution
   561  // has been run with the given execution context. If called before the completion of
   562  // remote-execution, the function returns a nil result.
   563  func (ec *Context) GetOutputFileDigests(useAbsPath bool) (map[string]digest.Digest, error) {
   564  	if ec.resPb == nil {
   565  		return nil, nil
   566  	}
   567  
   568  	ft, err := ec.client.GrpcClient.FlattenActionOutputs(ec.ctx, ec.resPb)
   569  	if err != nil {
   570  		return nil, err
   571  	}
   572  	res := map[string]digest.Digest{}
   573  	for path, outTree := range ft {
   574  		if useAbsPath {
   575  			path = filepath.Join(ec.cmd.ExecRoot, path)
   576  		}
   577  		res[path] = outTree.Digest
   578  	}
   579  	return res, nil
   580  }
   581  
   582  func timeFromProto(tPb *tspb.Timestamp) time.Time {
   583  	if tPb == nil {
   584  		return time.Time{}
   585  	}
   586  	return tPb.AsTime()
   587  }
   588  
   589  func setEventTimes(cm *command.Metadata, event string, start, end *tspb.Timestamp) {
   590  	cm.EventTimes[event] = &command.TimeInterval{
   591  		From: timeFromProto(start),
   592  		To:   timeFromProto(end),
   593  	}
   594  }
   595  
   596  func setTimingMetadata(cm *command.Metadata, em *repb.ExecutedActionMetadata) {
   597  	if em == nil {
   598  		return
   599  	}
   600  	setEventTimes(cm, command.EventServerQueued, em.QueuedTimestamp, em.WorkerStartTimestamp)
   601  	setEventTimes(cm, command.EventServerWorker, em.WorkerStartTimestamp, em.WorkerCompletedTimestamp)
   602  	setEventTimes(cm, command.EventServerWorkerInputFetch, em.InputFetchStartTimestamp, em.InputFetchCompletedTimestamp)
   603  	setEventTimes(cm, command.EventServerWorkerExecution, em.ExecutionStartTimestamp, em.ExecutionCompletedTimestamp)
   604  	setEventTimes(cm, command.EventServerWorkerOutputUpload, em.OutputUploadStartTimestamp, em.OutputUploadCompletedTimestamp)
   605  }
   606  
   607  func setAuxiliaryMetadata(cm *command.Metadata, em *repb.ExecutedActionMetadata) {
   608  	if em == nil {
   609  		return
   610  	}
   611  	cm.AuxiliaryMetadata = em.GetAuxiliaryMetadata()
   612  }
   613  
   614  // Run executes a command remotely.
   615  func (c *Client) Run(ctx context.Context, cmd *command.Command, opt *command.ExecutionOptions, oe outerr.OutErr) (*command.Result, *command.Metadata) {
   616  	ec, err := c.NewContext(ctx, cmd, opt, oe)
   617  	if err != nil {
   618  		return command.NewLocalErrorResult(err), &command.Metadata{}
   619  	}
   620  	ec.GetCachedResult()
   621  	if ec.Result != nil {
   622  		return ec.Result, ec.Metadata
   623  	}
   624  	ec.ExecuteRemotely()
   625  	// TODO(olaola): implement the cache-miss-retry loop.
   626  	return ec.Result, ec.Metadata
   627  }
   628  
   629  func formatInputSpec(spec *command.InputSpec, indent string) string {
   630  	sb := strings.Builder{}
   631  	sb.WriteString(indent + "inputs:\n")
   632  	for _, p := range spec.Inputs {
   633  		sb.WriteString(fmt.Sprintf("%[1]s%[1]s%s\n", indent, p))
   634  	}
   635  	sb.WriteString(indent + "virtual_inputs:\n")
   636  	for _, v := range spec.VirtualInputs {
   637  		sb.WriteString(fmt.Sprintf("%[1]s%[1]s%s, bytes=%d, dir=%t, exe=%t\n", indent, v.Path, len(v.Contents), v.IsEmptyDirectory, v.IsExecutable))
   638  	}
   639  	sb.WriteString(indent + "exclusions:\n")
   640  	for _, e := range spec.InputExclusions {
   641  		sb.WriteString(fmt.Sprintf("%[1]s%[1]s%s\n", indent, e))
   642  	}
   643  	sb.WriteString(fmt.Sprintf("%ssymlink_behaviour: %s", indent, spec.SymlinkBehavior))
   644  	return sb.String()
   645  }