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 }