blob: 251b5e9b465c78682ccd86de54270907348009b0 [file] [log] [blame]
// Copyright 2018 The Goma Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package remoteexec
import (
"bytes"
"context"
_ "embed"
"errors"
"fmt"
"math/rand"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"time"
rpb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
"go.opencensus.io/stats"
"go.opencensus.io/tag"
"go.opencensus.io/trace"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
tspb "google.golang.org/protobuf/types/known/timestamppb"
"go.chromium.org/goma/server/command/descriptor"
"go.chromium.org/goma/server/command/descriptor/posixpath"
"go.chromium.org/goma/server/command/descriptor/winpath"
"go.chromium.org/goma/server/exec"
"go.chromium.org/goma/server/log"
gomapb "go.chromium.org/goma/server/proto/api"
cmdpb "go.chromium.org/goma/server/proto/command"
"go.chromium.org/goma/server/remoteexec/cas"
"go.chromium.org/goma/server/remoteexec/digest"
"go.chromium.org/goma/server/remoteexec/merkletree"
"go.chromium.org/goma/server/rpc"
)
type request struct {
f *Adapter
userGroup string
gomaReq *gomapb.ExecReq
gomaResp *gomapb.ExecResp
client Client
cas *cas.CAS
cmdConfig *cmdpb.Config
cmdFiles []*cmdpb.FileSpec
digestStore *digest.Store
tree *merkletree.MerkleTree
input gomaInputInterface
filepath clientFilePath
cmdFilepath clientFilePath
args []string
envs []string
outputs []string
outputDirs []string
platform *rpb.Platform
action *rpb.Action
actionDigest *rpb.Digest
allowChroot bool
needChroot bool
crossTarget string
err error
}
func (r *request) Close() {
r.input.Close()
}
type clientFilePath interface {
IsAbs(path string) bool
Base(path string) string
Dir(path string) string
Join(elem ...string) string
Rel(basepath, targpath string) (string, error)
Clean(path string) string
SplitElem(path string) []string
PathSep() string
}
func doNotCache(req *gomapb.ExecReq) bool {
switch req.GetCachePolicy() {
case gomapb.ExecReq_LOOKUP_AND_STORE, gomapb.ExecReq_STORE_ONLY, gomapb.ExecReq_LOOKUP_AND_STORE_SUCCESS:
return false
default:
return true
}
}
func skipCacheLookup(req *gomapb.ExecReq) bool {
switch req.GetCachePolicy() {
case gomapb.ExecReq_STORE_ONLY:
return true
default:
return false
}
}
// Takes an input of environment flag defs, e.g. FLAG_NAME=value, and returns an array of
// rpb.Command_EnvironmentVariable with these flag names and values.
func createEnvVars(ctx context.Context, envs []string) []*rpb.Command_EnvironmentVariable {
envMap := make(map[string]string)
logger := log.FromContext(ctx)
for _, env := range envs {
e := strings.SplitN(env, "=", 2)
key, value := e[0], e[1]
storedValue, ok := envMap[key]
if ok {
logger.Infof("Duplicate env var: %s=%s => %s", key, storedValue, value)
}
envMap[key] = value
}
// EnvironmentVariables must be lexicographically sorted by name.
var envKeys []string
for k := range envMap {
envKeys = append(envKeys, k)
}
sort.Strings(envKeys)
var envVars []*rpb.Command_EnvironmentVariable
for _, k := range envKeys {
envVars = append(envVars, &rpb.Command_EnvironmentVariable{
Name: k,
Value: envMap[k],
})
}
return envVars
}
// ID returns compiler proxy id of the request.
func (r *request) ID() string {
if r == nil {
return "<unknown>"
}
return r.gomaReq.GetRequesterInfo().GetCompilerProxyId()
}
// Err returns error of the request.
func (r *request) Err() error {
switch status.Code(r.err) {
case codes.OK:
return nil
case codes.Canceled, codes.DeadlineExceeded, codes.Aborted:
// report cancel/deadline exceeded/aborted as is
return r.err
case codes.Unauthenticated:
// unauthenticated happens when oauth2 access token
// is expired during exec call.
// e.g.
// desc = Request had invalid authentication credentials.
// Expected OAuth 2 access token, login cookie or
// other valid authentication credential.
// See https://developers.google.com/identity/sign-in/web/devconsole-project.
// report it back to caller, so caller could retry it
// again with new refreshed oauth2 access token.
return r.err
default:
return status.Errorf(codes.Internal, "exec error: %v", r.err)
}
}
func (r *request) instanceName() string {
basename := r.cmdConfig.GetRemoteexecPlatform().GetRbeInstanceBasename()
if basename == "" {
return r.f.Instance()
}
return path.Join(r.f.InstancePrefix, basename)
}
// getInventoryData looks up Config and FileSpec from Inventory, and creates
// execution platform properties from Config.
// It returns non-nil ExecResp for:
// - compiler/subprogram not found
// - bad path_type in command config
func (r *request) getInventoryData(ctx context.Context) *gomapb.ExecResp {
if r.err != nil {
return nil
}
logger := log.FromContext(ctx)
cmdConfig, cmdFiles, err := r.f.Inventory.Pick(ctx, r.gomaReq, r.gomaResp)
if err != nil {
logger.Errorf("Inventory.Pick failed: %v", err)
return r.gomaResp
}
r.filepath, err = descriptor.FilePathOf(cmdConfig.GetCmdDescriptor().GetSetup().GetPathType())
if err != nil {
logger.Errorf("bad path type in setup %s: %v", cmdConfig.GetCmdDescriptor().GetSelector(), err)
r.gomaResp.Error = gomapb.ExecResp_BAD_REQUEST.Enum()
r.gomaResp.ErrorMessage = append(r.gomaResp.ErrorMessage, fmt.Sprintf("bad compiler config: %v", err))
return r.gomaResp
}
if cmdConfig.GetCmdDescriptor().GetCross().GetWindowsCross() {
r.filepath = winpath.FilePath{}
r.cmdFilepath = posixpath.FilePath{}
// drop .bat suffix
// http://b/185210502#comment12
cmdFiles[0].Path = strings.TrimSuffix(cmdFiles[0].Path, ".bat")
} else {
r.cmdFilepath = r.filepath
}
r.cmdConfig = cmdConfig
r.cmdFiles = cmdFiles
r.platform = &rpb.Platform{}
for _, prop := range cmdConfig.GetRemoteexecPlatform().GetProperties() {
r.addPlatformProperty(ctx, prop.Name, prop.Value)
}
if len(r.gomaReq.GetRequesterInfo().GetPlatformProperties()) > 0 {
for _, pp := range r.gomaReq.GetRequesterInfo().GetPlatformProperties() {
if !isSafePlatformProperty(pp.GetName(), pp.GetValue()) {
logger.Errorf("unsafe user platform property: %v", pp)
r.gomaResp.Error = gomapb.ExecResp_BAD_REQUEST.Enum()
r.gomaResp.ErrorMessage = append(r.gomaResp.ErrorMessage, fmt.Sprintf("unsafe platform property: %v", pp))
continue
}
logger.Infof("override by user platform property: %v", pp)
r.addPlatformProperty(ctx, pp.GetName(), pp.GetValue())
}
if len(r.gomaResp.ErrorMessage) > 0 {
return r.gomaResp
}
}
r.allowChroot = cmdConfig.GetRemoteexecPlatform().GetHasNsjail()
logger.Infof("platform: %s, allowChroot=%t path_tpye=%s windows_cross=%t", r.platform, r.allowChroot, cmdConfig.GetCmdDescriptor().GetSetup().GetPathType(), cmdConfig.GetCmdDescriptor().GetCross().GetWindowsCross())
return nil
}
func isSafePlatformProperty(name, value string) bool {
switch name {
case "container-image", "InputRootAbsolutePath", "cache-silo":
return true
case "dockerRuntime":
return value == "runsc"
}
return false
}
func (r *request) addPlatformProperty(ctx context.Context, name, value string) {
for _, p := range r.platform.Properties {
if p.Name == name {
p.Value = value
return
}
}
r.platform.Properties = append(r.platform.Properties, &rpb.Platform_Property{
Name: name,
Value: value,
})
}
type inputDigestData struct {
filename string
digest.Data
}
func (id inputDigestData) String() string {
return fmt.Sprintf("%s %s", id.Data.String(), id.filename)
}
func changeSymlinkAbsToRel(e merkletree.Entry) (merkletree.Entry, error) {
dir := filepath.Dir(e.Name)
if !filepath.IsAbs(dir) {
return merkletree.Entry{}, fmt.Errorf("absolute symlink path not allowed: %s -> %s", e.Name, e.Target)
}
target, err := filepath.Rel(dir, e.Target)
if err != nil {
return merkletree.Entry{}, fmt.Errorf("failed to make relative for absolute symlink path: %s in %s -> %s: %v", e.Name, dir, e.Target, err)
}
e.Target = target
return e, nil
}
type gomaInputInterface interface {
toDigest(context.Context, *gomapb.ExecReq_Input) (digest.Data, error)
upload(context.Context, []*gomapb.FileBlob) ([]string, error)
Close()
}
func uploadInputFiles(ctx context.Context, inputs []*gomapb.ExecReq_Input, gi gomaInputInterface) error {
ctx, span := trace.StartSpan(ctx, "go.chromium.org/goma/server/remoteexec.request.uploadInputFiles")
defer span.End()
span.AddAttributes(trace.Int64Attribute("uploads", int64(len(inputs))))
count := 0
size := 0
batchLimit := 500
sizeLimit := 10 * 1024 * 1024
beginOffset := 0
hashKeys := make([]string, len(inputs))
eg, ctx := errgroup.WithContext(ctx)
for i, input := range inputs {
count++
size += len(input.Content.Content)
// Upload a bunch of file blobs if one of the following:
// - inputs[uploadBegin:i] reached the upload blob count limit
// - inputs[uploadBegin:i] exceeds the upload blob size limit
// - we are on the last blob to be uploaded
if count < batchLimit && size < sizeLimit && i < len(inputs)-1 {
continue
}
inputs := inputs[beginOffset : i+1]
results := hashKeys[beginOffset : i+1]
eg.Go(func() error {
contents := make([]*gomapb.FileBlob, len(inputs))
for i, input := range inputs {
contents[i] = input.Content
}
var hks []string
var err error
err = rpc.Retry{}.Do(ctx, func() error {
hks, err = gi.upload(ctx, contents)
return err
})
if err != nil {
return fmt.Errorf("setup %s input error: %v", inputs[0].GetFilename(), err)
}
if len(hks) != len(contents) {
return fmt.Errorf("invalid number of hash keys: %d, want %d", len(hks), len(contents))
}
for i, hk := range hks {
input := inputs[i]
if input.GetHashKey() != hk {
return fmt.Errorf("hashkey missmatch: embedded input %s %s != %s", input.GetFilename(), input.GetHashKey(), hk)
}
results[i] = hk
}
return nil
})
beginOffset = i + 1
count = 0
size = 0
}
defer func() {
maxOutputSize := len(inputs)
if maxOutputSize > 10 {
maxOutputSize = 10
}
successfulUploadsMsg := make([]string, 0, maxOutputSize+1)
for i, input := range inputs {
if len(hashKeys[i]) == 0 {
continue
}
if i == maxOutputSize && i < len(inputs)-1 {
successfulUploadsMsg = append(successfulUploadsMsg, "...")
break
}
successfulUploadsMsg = append(successfulUploadsMsg, fmt.Sprintf("%s -> %s", input.GetFilename(), hashKeys[i]))
}
logger := log.FromContext(ctx)
logger.Infof("embedded inputs: %v", successfulUploadsMsg)
numSuccessfulUploads := 0
for _, hk := range hashKeys {
if len(hk) > 0 {
numSuccessfulUploads++
}
}
if numSuccessfulUploads < len(inputs) {
logger.Errorf("%d file blobs successfully uploaded, out of %d", numSuccessfulUploads, len(inputs))
}
}()
return eg.Wait()
}
func dedupInputs(filepath clientFilePath, cwd string, inputs []*gomapb.ExecReq_Input) []*gomapb.ExecReq_Input {
var deduped []*gomapb.ExecReq_Input
m := make(map[string]int) // key name -> index in deduped
for _, input := range inputs {
fname := input.GetFilename()
if !filepath.IsAbs(fname) {
fname = filepath.Join(cwd, fname)
}
k := strings.ToLower(fname)
i, found := m[k]
if !found {
m[k] = len(deduped)
deduped = append(deduped, input)
continue
}
// If there is already registered filename, compare and take shorter one.
if len(input.GetFilename()) < len(deduped[i].GetFilename()) {
deduped[i] = input
continue
}
// If length is same, take lexicographically smaller one.
if len(input.GetFilename()) == len(deduped[i].GetFilename()) && input.GetFilename() < deduped[i].GetFilename() {
deduped[i] = input
}
}
return deduped
}
type inputFileResult struct {
missingInput string
missingReason string
file merkletree.Entry
needUpload bool
err error
}
func inputFiles(ctx context.Context, inputs []*gomapb.ExecReq_Input, gi gomaInputInterface, rootRel func(string) (string, error), executableInputs map[string]bool) []inputFileResult {
logger := log.FromContext(ctx)
var wg sync.WaitGroup
ctx, span := trace.StartSpan(ctx, "go.chromium.org/goma/server/remoteexec.request.inputFiles")
defer span.End()
span.AddAttributes(trace.Int64Attribute("inputs", int64(len(inputs))))
results := make([]inputFileResult, len(inputs))
for i, input := range inputs {
wg.Add(1)
go func(input *gomapb.ExecReq_Input, result *inputFileResult) {
defer wg.Done()
fname, err := rootRel(input.GetFilename())
if err != nil {
if err == errOutOfRoot {
logger.Warnf("filename %s: %v", input.GetFilename(), err)
return
}
result.err = fmt.Errorf("input file: %s %v", input.GetFilename(), err)
return
}
data, err := gi.toDigest(ctx, input)
if err != nil {
result.missingInput = input.GetFilename()
result.missingReason = fmt.Sprintf("input: %v", err)
return
}
file := merkletree.Entry{
Name: fname,
Data: inputDigestData{
filename: input.GetFilename(),
Data: data,
},
IsExecutable: executableInputs[input.GetFilename()],
}
result.file = file
if input.Content == nil {
return
}
result.needUpload = true
}(input, &results[i])
}
wg.Wait()
return results
}
// newInputTree constructs input tree from req.
// it returns non-nil ExecResp for:
// - missing inputs
// - input root detection failed
// - non-relative and non C: drive on windows.
func (r *request) newInputTree(ctx context.Context) *gomapb.ExecResp {
if r.err != nil {
return nil
}
ctx, span := trace.StartSpan(ctx, "go.chromium.org/goma/server/remoteexec.request.newInputTree")
defer span.End()
logger := log.FromContext(ctx)
execPaths, err := execPaths(r.filepath, r.gomaReq, r.cmdFiles[0].Path)
if err != nil {
logger.Errorf("bad input: %v", err)
r.gomaResp.Error = gomapb.ExecResp_BAD_REQUEST.Enum()
r.gomaResp.ErrorMessage = append(r.gomaResp.ErrorMessage, fmt.Sprintf("bad input: %v", err))
return r.gomaResp
}
execRootDir := r.gomaReq.GetRequesterInfo().GetExecRoot()
rootDir, needChroot, err := deriveExecRoot(r.filepath, execPaths, r.allowChroot, execRootDir)
if err != nil {
logger.Errorf("exec root detection failed: %v", err)
logFileList(logger, "exec paths", execPaths)
r.gomaResp.Error = gomapb.ExecResp_BAD_REQUEST.Enum()
r.gomaResp.ErrorMessage = append(r.gomaResp.ErrorMessage, fmt.Sprintf("exec root detection failed: %v", err))
return r.gomaResp
}
r.tree = merkletree.New(r.filepath, rootDir, r.digestStore)
r.needChroot = needChroot
logger.Infof("new input tree cwd:%s root:%s execRoot:%s %s", r.gomaReq.GetCwd(), r.tree.RootDir(), execRootDir, r.filepath)
// If toolchain_included is true, r.gomaReq.Input and cmdFiles will contain the same files.
// To avoid dup, if it's added in r.gomaReq.Input, we don't add it as cmdFiles.
// While processing r.gomaReq.Input, we handle missing input, so the main routine is in
// r.gomaReq.Input.
// path from cwd -> is_executable. Don't confuse "path from cwd" and "path from input root".
// Everything (except symlink) in ToolchainSpec should be in r.gomaReq.Input.
// If not and it's necessary to execute, a runtime error (while compile) can happen.
// e.g. *.so is missing etc.
toolchainInputs := make(map[string]bool)
executableInputs := make(map[string]bool)
if r.gomaReq.GetToolchainIncluded() {
for _, ts := range r.gomaReq.ToolchainSpecs {
if ts.GetSymlinkPath() != "" {
// If toolchain is a symlink, it is not included in r.gomaReq.Input.
// So, toolchainInputs should not contain it.
continue
}
toolchainInputs[ts.GetPath()] = true
if ts.GetIsExecutable() {
executableInputs[ts.GetPath()] = true
}
}
}
cleanCWD := r.filepath.Clean(r.gomaReq.GetCwd())
cleanRootDir := r.filepath.Clean(r.tree.RootDir())
start := time.Now()
reqInputs := r.gomaReq.Input
if _, ok := r.filepath.(winpath.FilePath); ok && !r.cmdConfig.GetCmdDescriptor().GetCross().GetWindowsCross() {
// need to dedup filename for windows,
// except windows cross case.
reqInputs = dedupInputs(r.filepath, cleanCWD, r.gomaReq.Input)
if len(reqInputs) != len(r.gomaReq.Input) {
logger.Infof("input dedup %d -> %d", len(r.gomaReq.Input), len(reqInputs))
}
}
results := inputFiles(ctx, reqInputs, r.input, func(filename string) (string, error) {
return rootRel(r.filepath, filename, cleanCWD, cleanRootDir)
}, executableInputs)
uploads := make([]*gomapb.ExecReq_Input, 0, len(reqInputs))
for i, input := range reqInputs {
result := &results[i]
if r.err == nil && result.err != nil {
r.err = result.err
}
if result.needUpload {
uploads = append(uploads, input)
}
}
if r.err != nil {
logger.Warnf("inputFiles=%d uploads=%d in %s err:%v", len(reqInputs), len(uploads), time.Since(start), r.err)
return nil
}
logger.Infof("inputFiles=%d uploads=%d in %s", len(reqInputs), len(uploads), time.Since(start))
var files []merkletree.Entry
var missingInputs []string
var missingReason []string
for _, in := range results {
if in.missingInput != "" {
missingInputs = append(missingInputs, in.missingInput)
missingReason = append(missingReason, in.missingReason)
continue
}
if in.file.Name == "" {
// ignore out of root files.
continue
}
files = append(files, in.file)
}
if len(missingInputs) > 0 {
logger.Infof("missing %d inputs out of %d. need to uploads=%d", len(missingInputs), len(reqInputs), len(uploads))
r.gomaResp.MissingInput = missingInputs
r.gomaResp.MissingReason = missingReason
thinOutMissing(r.gomaResp, r.f.MissingInputLimit)
sortMissing(r.gomaReq.Input, r.gomaResp)
logFileList(logger, "missing inputs", r.gomaResp.MissingInput)
return r.gomaResp
}
// create wrapper scripts
err = r.newWrapperScript(ctx, r.cmdConfig, r.cmdFiles[0].Path)
if err != nil {
var badReqErr badRequestError
if errors.As(err, &badReqErr) {
r.gomaResp.Error = gomapb.ExecResp_BAD_REQUEST.Enum()
r.gomaResp.ErrorMessage = append(r.gomaResp.ErrorMessage, badReqErr.Error())
return r.gomaResp
}
// otherwise, internal error.
r.err = fmt.Errorf("wrapper script: %v", err)
return nil
}
symAbsOk := r.f.capabilities.GetCacheCapabilities().GetSymlinkAbsolutePathStrategy() == rpb.SymlinkAbsolutePathStrategy_ALLOWED
cmdCleanCWD := cleanCWD
cmdCleanRootDir := cleanRootDir
if (r.filepath == winpath.FilePath{}) && (r.cmdFilepath == posixpath.FilePath{}) {
cmdCleanCWD = winpath.ToPosix(cleanCWD)
cmdCleanRootDir = winpath.ToPosix(cleanRootDir)
}
for _, f := range r.cmdFiles {
if _, found := toolchainInputs[f.Path]; found {
// Must be processed in r.gomaReq.Input. So, skip this.
// TODO: cmdFiles should be empty instead if toolchain_included = true case?
continue
}
e, err := fileSpecToEntry(ctx, f, r.f.CmdStorage)
if err != nil {
r.err = fmt.Errorf("fileSpecToEntry: %v", err)
return nil
}
if !symAbsOk && e.Target != "" && filepath.IsAbs(e.Target) {
e, err = changeSymlinkAbsToRel(e)
if err != nil {
r.err = err
return nil
}
}
fname, err := rootRel(r.cmdFilepath, e.Name, cmdCleanCWD, cmdCleanRootDir)
if err != nil {
if err == errOutOfRoot {
logger.Warnf("cmd files: out of root: %s", e.Name)
continue
}
r.err = fmt.Errorf("command file: %v", err)
return nil
}
e.Name = fname
files = append(files, e)
}
addDirs := func(name string, dirs []string) {
if r.err != nil {
return
}
for _, d := range dirs {
rel, err := rootRel(r.filepath, d, cleanCWD, cleanRootDir)
if err != nil {
if err == errOutOfRoot {
logger.Warnf("%s %s: %v", name, d, err)
continue
}
r.err = fmt.Errorf("%s %s: %v", name, d, err)
return
}
files = append(files, merkletree.Entry{
// directory
Name: rel,
})
}
}
// Set up system include and framework paths (b/119072207)
// -isystem etc can be set for a compile, and non-existence of a directory specified by -isystem may cause compile error even if no file inside the directory is used.
addDirs("cxx system include path", r.gomaReq.GetCommandSpec().GetCxxSystemIncludePath())
addDirs("system include path", r.gomaReq.GetCommandSpec().GetSystemIncludePath())
addDirs("system framework path", r.gomaReq.GetCommandSpec().GetSystemFrameworkPath())
// prepare output dirs.
r.outputs = outputs(ctx, r.cmdConfig, r.gomaReq)
var outDirs []string
for _, d := range r.outputs {
outDirs = append(outDirs, r.filepath.Dir(d))
}
addDirs("output file", outDirs)
r.outputDirs = outputDirs(ctx, r.cmdConfig, r.gomaReq)
addDirs("output dir", r.outputDirs)
if r.err != nil {
return nil
}
for _, f := range files {
err = r.tree.Set(f)
if err != nil {
r.err = fmt.Errorf("input file: %v: %v", f, err)
return nil
}
}
root, err := r.tree.Build(ctx)
if err != nil {
r.err = err
return nil
}
logger.Infof("input root digest: %v", root)
r.action.InputRootDigest = root
// uploads embedded contents to file-server
// for the case the file was not yet uploaded to RBE CAS.
// even if client sends input with embedded content,
// the content may be already uploaded to RBE CAS,
// and uploaded content may not be needed,
// so we could ignore error of these uploads.
start = time.Now()
err = uploadInputFiles(ctx, uploads, r.input)
logger.Infof("upload %d inputs out of %d in %s: %v", len(uploads), len(r.gomaReq.Input), time.Since(start), err)
return nil
}
type wrapperType int
const (
wrapperRelocatable wrapperType = iota
wrapperInputRootAbsolutePath
wrapperNsjailChroot
wrapperWin
wrapperWinInputRootAbsolutePath
)
func (w wrapperType) String() string {
switch w {
case wrapperRelocatable:
return "wrapper-relocatable"
case wrapperInputRootAbsolutePath:
return "wrapper-input-root-absolute-path"
case wrapperNsjailChroot:
return "wrapper-nsjail-chroot"
case wrapperWin:
return "wrapper-win"
case wrapperWinInputRootAbsolutePath:
return "wrapper-win-input-root-absolute-path"
default:
return fmt.Sprintf("wrapper-unknown-%d", int(w))
}
}
var (
// TODO: use working_directory in action.
// need to fix output path to be relative to working_directory.
// http://b/113370588
//go:embed run.sh
wrapperScript []byte
)
type badRequestError struct {
err error
}
func (b badRequestError) Error() string {
return b.err.Error()
}
// TODO: put wrapper script in platform container?
func (r *request) newWrapperScript(ctx context.Context, cmdConfig *cmdpb.Config, argv0 string) error {
logger := log.FromContext(ctx)
cwd := r.gomaReq.GetCwd()
cleanCWD := r.filepath.Clean(cwd)
cleanRootDir := r.filepath.Clean(r.tree.RootDir())
wd, err := rootRel(r.filepath, cwd, cleanCWD, cleanRootDir)
if err != nil {
return badRequestError{err: fmt.Errorf("bad cwd=%s: %v", cwd, err)}
}
if wd == "" {
wd = "."
}
if cmdConfig.GetCmdDescriptor().GetCross().GetWindowsCross() {
wd = winpath.ToPosix(wd)
}
envs := []string{fmt.Sprintf("WORK_DIR=%s", wd)}
// The developer of this program can make multiple wrapper scripts
// to be used by adding fileDesc instances to `files`.
// However, only the first one is called in the command line.
// The other scripts should be called from the first wrapper script
// if needed.
var files []merkletree.Entry
args := buildArgs(ctx, cmdConfig, argv0, r.gomaReq)
// TODO: only allow specific envs.
r.crossTarget = targetFromArgs(args)
var relocatableErr error
wt := wrapperRelocatable
switch r.filepath.(type) {
case posixpath.FilePath:
if r.needChroot {
wt = wrapperNsjailChroot
} else {
relocatableErr = relocatableReq(ctx, cmdConfig, r.filepath, r.gomaReq.Arg, r.gomaReq.Env)
if relocatableErr != nil {
wt = wrapperInputRootAbsolutePath
logger.Infof("non relocatable: %v", relocatableErr)
}
}
case winpath.FilePath:
relocatableErr = relocatableReq(ctx, cmdConfig, r.filepath, r.gomaReq.Arg, r.gomaReq.Env)
if relocatableErr != nil {
wt = wrapperWinInputRootAbsolutePath
logger.Infof("non relocatable: %v", relocatableErr)
} else {
wt = wrapperWin
}
if cmdConfig.GetCmdDescriptor().GetCross().GetWindowsCross() {
switch wt {
case wrapperWinInputRootAbsolutePath:
// we expect most case is relocatable
// with -fdebug-compilation-dir=.
// but it would break if user uses unknown
// flags, which makes request unrelocatable.
// See rootDir fix in wrapperInputRootAbsolutePath below.
wt = wrapperInputRootAbsolutePath
case wrapperWin:
wt = wrapperRelocatable
}
}
default:
// internal error? maybe toolchain config is broken.
return fmt.Errorf("bad path type: %T", r.filepath)
}
const posixWrapperName = "run.sh"
switch wt {
case wrapperNsjailChroot:
logger.Infof("run with nsjail chroot")
// needed for bind mount.
r.addPlatformProperty(ctx, "dockerPrivileged", "true")
// needed for chroot command and mount command.
r.addPlatformProperty(ctx, "dockerRunAsRoot", "true")
nsjailCfg := nsjailChrootConfig(cwd, r.filepath, r.gomaReq.GetToolchainSpecs(), r.gomaReq.Env)
files = []merkletree.Entry{
{
Name: posixWrapperName,
Data: digest.Bytes("nsjail-chroot-run-wrapper-script", nsjailChrootRunWrapperScript),
IsExecutable: true,
},
{
Name: "nsjail.cfg",
Data: digest.Bytes("nsjail-config-file", []byte(nsjailCfg)),
},
}
case wrapperInputRootAbsolutePath:
wrapperData := digest.Bytes("wrapper-script", wrapperScript)
files, wrapperData = r.maybeApplyHardening(ctx, "InputRootAbsolutePath", files, wrapperData)
// https://cloud.google.com/remote-build-execution/docs/remote-execution-properties#container_properties
rootDir := r.tree.RootDir()
if cmdConfig.GetCmdDescriptor().GetCross().GetWindowsCross() {
// we can't use windows path as absolute path.
// drop first two letters (i.e. `C:`), and
// convert \ to /.
// instead of making relocatable check loose,
// better to omit drive letter and make the
// effective for the same drive letter.
rootDir = winpath.ToPosix(rootDir)
}
r.addPlatformProperty(ctx, "InputRootAbsolutePath", rootDir)
for _, e := range r.gomaReq.Env {
envs = append(envs, e)
}
files = append([]merkletree.Entry{
{
Name: posixWrapperName,
Data: wrapperData,
IsExecutable: true,
},
}, files...)
case wrapperRelocatable:
wrapperData := digest.Bytes("wrapper-script", wrapperScript)
files, wrapperData = r.maybeApplyHardening(ctx, "chdir: relocatble", files, wrapperData)
for _, e := range r.gomaReq.Env {
if strings.HasPrefix(e, "PWD=") {
// PWD is usually absolute path.
// if relocatable, then we should remove
// PWD environment variable.
continue
}
envs = append(envs, e)
}
files = append([]merkletree.Entry{
{
Name: posixWrapperName,
Data: wrapperData,
IsExecutable: true,
},
}, files...)
case wrapperWin:
logger.Infof("run on win")
wn, data, err := wrapperForWindows(ctx)
if err != nil {
// missing run.exe?
return err
}
// no need to set environment variables??
files = []merkletree.Entry{
{
Name: wn,
Data: data,
IsExecutable: true,
},
}
case wrapperWinInputRootAbsolutePath:
logger.Infof("run on win with InputRootAbsolutePath")
if relocatableErr != nil && !strings.HasPrefix(strings.ToUpper(r.tree.RootDir()), `C:\`) {
// TODO Docker Internal Errors
// see also http://b/161274896 Catch specific case where drive letter other than C: specified for input root on Windows
logger.Errorf("non relocatable on windows, but absolute path is not C: drive. %s", r.tree.RootDir())
return badRequestError{err: fmt.Errorf("non relocatable %v, but root dir is %q. make request relocatable, or use `C:`", relocatableErr, r.tree.RootDir())}
}
// https://cloud.google.com/remote-build-execution/docs/remote-execution-properties#container_properties
r.addPlatformProperty(ctx, "InputRootAbsolutePath", r.tree.RootDir())
wn, data, err := wrapperForWindows(ctx)
if err != nil {
// missing run.exe?
return err
}
// This is necessary for Win emscripten-releases LLVM build, which uses env vars to specify e.g. include
// dirs. See crbug.com/1040150.
// The build uses abs paths, which is why the env vars are stored here. Whether or not they should also
// be in stored in the case of `wrapperWin` is left for future consideration.
for _, e := range r.gomaReq.Env {
if strings.HasPrefix(e, "INCLUDE=") || strings.HasPrefix(e, "LIB=") {
envs = append(envs, e)
}
}
files = []merkletree.Entry{
{
Name: wn,
Data: data,
IsExecutable: true,
},
}
default:
// coding error?
return fmt.Errorf("bad wrapper type: %v", wt)
}
// Only the first one is called in the command line via storing
// `wrapperPath` in `r.args` later.
wrapperPath := ""
for i, w := range files {
w.Name, err = rootRel(r.filepath, w.Name, cleanCWD, cleanRootDir)
if err != nil {
// rootRel should not fail with any user input at this point?
return err
}
logger.Infof("file (%d) %s => %v", i, w.Name, w.Data.Digest())
r.tree.Set(w)
if wrapperPath == "" {
wrapperPath = w.Name
}
}
r.envs = envs
// if a wrapper exists in cwd, `wrapper` does not have a directory name.
// It cannot be callable on POSIX because POSIX do not contain "." in
// its PATH.
if wrapperPath == posixWrapperName {
wrapperPath = "./" + posixWrapperName
}
if cmdConfig.GetCmdDescriptor().GetCross().GetWindowsCross() {
wrapperPath = winpath.ToPosix(wrapperPath)
}
r.args = append([]string{wrapperPath}, args...)
err = stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(wrapperTypeKey, wt.String())}, wrapperCount.M(1))
if err != nil {
logger.Errorf("record wrapper-count %s: %v", wt, err)
}
return nil
}
func (r *request) maybeApplyHardening(ctx context.Context, wt string, files []merkletree.Entry, wrapperData digest.Data) ([]merkletree.Entry, digest.Data) {
logger := log.FromContext(ctx)
if f, disable := disableHardening(r.f.DisableHardenings, r.cmdFiles); disable {
logger.Infof("run with %s (disable hardening for %v)", wt, f)
} else if rand.Float64() < r.f.HardeningRatio {
if rand.Float64() < r.f.NsjailRatio {
logger.Infof("run with %s + nsjail", wt)
wrapperData = digest.Bytes("nsjail-hardening-wrapper-scrpt", nsjailHardeningWrapperScript)
// needed for nsjail
r.addPlatformProperty(ctx, "dockerPrivileged", "true")
files = append(files, merkletree.Entry{
Name: "nsjail.cfg",
Data: digest.Bytes("nsjail.cfg", nsjailHardeningConfig),
})
} else {
logger.Infof("run with %s + runsc", wt)
r.addPlatformProperty(ctx, "dockerRuntime", "runsc")
}
} else {
logger.Infof("run with %s", wt)
}
return files, wrapperData
}
func disableHardening(hashes []string, cmdFiles []*cmdpb.FileSpec) (*cmdpb.FileSpec, bool) {
for _, h := range hashes {
if h == "" {
continue
}
for _, f := range cmdFiles {
if f.GetSymlink() != "" {
continue
}
if f.GetHash() == h {
return f, true
}
}
}
return nil, false
}
// TODO: refactor with exec/clang.go, exec/clangcl.go?
// buildArgs builds args in RBE from arg0 and req, respecting cmdConfig.
func buildArgs(ctx context.Context, cmdConfig *cmdpb.Config, arg0 string, req *gomapb.ExecReq) []string {
// TODO: need compiler specific handling?
args := append([]string{arg0}, req.Arg[1:]...)
if cmdConfig.GetCmdDescriptor().GetCross().GetWindowsCross() {
args[0] = winpath.ToPosix(args[0])
pathFlag := false
argLoop:
for i := 1; i < len(args); i++ {
if pathFlag {
args[i] = winpath.ToPosix(args[i])
pathFlag = false
continue argLoop
}
// JoinedOrSeparate
for _, f := range []string{"/winsysroot", "-winsysroot", "-imsvc", "/imsvc", "-I", "/I"} {
if args[i] == f {
pathFlag = true
continue argLoop
}
if strings.HasPrefix(args[i], f) {
args[i] = f + winpath.ToPosix(strings.TrimPrefix(args[i], f))
continue argLoop
}
}
// Joined
// Fd is ignored, though
for _, f := range []string{"-resource-dir=", "/Fo", "-Fo", "/Fd", "-Fd"} {
if strings.HasPrefix(args[i], f) {
args[i] = f + winpath.ToPosix(strings.TrimPrefix(args[i], f))
continue argLoop
}
}
// TODO: need to handle other args?
if strings.HasPrefix(args[i], "-") || strings.HasPrefix(args[i], "/") {
continue argLoop
}
// input file, or arg of some flag?
// assume arg of some flag (e.g. -D) won't be windows
// absolute path.
if winpath.IsAbs(args[i]) {
args[i] = winpath.ToPosix(args[i])
continue argLoop
}
}
envs := req.Env
req.Env = nil
for _, e := range envs {
switch {
case strings.HasPrefix(e, "INCLUDE="):
includes := strings.Split(strings.TrimPrefix(e, "INCLUDE="), ";")
var imsvcArgs []string
for _, inc := range includes {
imsvcArgs = append(imsvcArgs, "-imsvc"+winpath.ToPosix(inc))
}
args = addFlagsToArgs(args, imsvcArgs...)
case strings.HasPrefix(e, "LIB="):
// unnecessary?
default:
req.Env = append(req.Env, e)
}
}
}
if cmdConfig.GetCmdDescriptor().GetCross().GetClangNeedTarget() {
args = addTargetIfNotExist(args, req.GetCommandSpec().GetTarget())
}
return args
}
func addFlagsToArgs(args []string, flagArgs ...string) []string {
for i := 0; i < len(args); i++ {
if args[i] == "--" {
// we should add flags before --
// append to empty slice to avoid overwriting after args[i:]
return append(append(append([]string{}, args[:i]...), flagArgs...), args[i:]...)
}
}
// -- is not used
return append(args, flagArgs...)
}
// add target option to args if args doesn't already have target option.
func addTargetIfNotExist(args []string, target string) []string {
// no need to add -target if arg already have it.
for _, arg := range args {
if arg == "-target" || strings.HasPrefix(arg, "--target=") {
return args
}
}
// https://clang.llvm.org/docs/CrossCompilation.html says
// `-target <triple>`, but clang --help shows
// --target=<value> Generate code for the given target
// add --target at front, not at end
// if it is after "--", it would fail "no such file or directory: `--target=xxx`"
// since we're setting default target, it's fine to set flag just after command name.
return append([]string{args[0], fmt.Sprintf("--target=%s", target)}, args[1:]...)
}
func targetFromArgs(args []string) string {
for i, arg := range args {
if arg == "-target" {
if i < len(args)-1 {
return args[i+1]
}
return ""
}
if strings.HasPrefix(arg, "--target=") {
return strings.TrimPrefix(arg, "--target=")
}
}
return ""
}
type unknownFlagError struct {
arg string
}
func (e unknownFlagError) Error() string {
return fmt.Sprintf("unknown flag: %s", e.arg)
}
// relocatableReq checks args, envs is relocatable, respecting cmdConfig.
func relocatableReq(ctx context.Context, cmdConfig *cmdpb.Config, filepath clientFilePath, args, envs []string) error {
name := cmdConfig.GetCmdDescriptor().GetSelector().GetName()
var err error
switch name {
case "gcc", "g++", "clang", "clang++":
err = gccRelocatableReq(filepath, args, envs)
case "clang-cl":
err = clangclRelocatableReq(filepath, args, envs)
case "javac":
// Currently, javac in Chromium is fully relocatable. Simpler just to
// support only the relocatable case and let it fail if the client passed
// in invalid absolute paths.
err = nil
default:
// "cl.exe", "clang-tidy"
err = fmt.Errorf("no relocatable check for %s", name)
}
if err != nil {
var uerr unknownFlagError
if errors.As(err, &uerr) {
if serr := stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(compilerNameKey, name)}, unknownFlagCount.M(1)); serr != nil {
logger := log.FromContext(ctx)
logger.Errorf("record unknown-flag %s: %v", name, serr)
}
}
}
return err
}
// outputs gets output filenames from gomaReq.
// If either expected_output_files or expected_output_dirs is specified,
// expected_output_files is used.
// Otherwise, it's calculated from args.
func outputs(ctx context.Context, cmdConfig *cmdpb.Config, gomaReq *gomapb.ExecReq) []string {
if len(gomaReq.ExpectedOutputFiles) > 0 || len(gomaReq.ExpectedOutputDirs) > 0 {
return gomaReq.GetExpectedOutputFiles()
}
args := gomaReq.Arg
switch name := cmdConfig.GetCmdDescriptor().GetSelector().GetName(); name {
case "gcc", "g++", "clang", "clang++":
return gccOutputs(args)
case "clang-cl":
return clangclOutputs(args)
default:
// "cl.exe", "javac", "clang-tidy"
return nil
}
}
// outputDirs gets output dirnames from gomaReq.
// If either expected_output_files or expected_output_dirs is specified,
// expected_output_dirs is used.
// Otherwise, it's calculated from args.
func outputDirs(ctx context.Context, cmdConfig *cmdpb.Config, gomaReq *gomapb.ExecReq) []string {
if len(gomaReq.ExpectedOutputFiles) > 0 || len(gomaReq.ExpectedOutputDirs) > 0 {
return gomaReq.GetExpectedOutputDirs()
}
args := gomaReq.Arg
switch cmdConfig.GetCmdDescriptor().GetSelector().GetName() {
case "javac":
return javacOutputDirs(args)
default:
return nil
}
}
func (r *request) setupNewAction(ctx context.Context) {
if r.err != nil {
return
}
command, err := r.newCommand(ctx)
if err != nil {
r.err = err
return
}
// we'll run wrapper script that chdir, so don't set chdir here.
// see newWrapperScript.
// TODO: set command.WorkingDirectory
data, err := digest.Proto(command)
if err != nil {
r.err = err
return
}
logger := log.FromContext(ctx)
logger.Infof("command digest: %v", data.Digest())
r.digestStore.Set(data)
r.action.CommandDigest = data.Digest()
data, err = digest.Proto(r.action)
if err != nil {
r.err = err
return
}
r.digestStore.Set(data)
logger.Infof("action digest: %v %s", data.Digest(), r.action)
r.actionDigest = data.Digest()
}
func (r *request) newCommand(ctx context.Context) (*rpb.Command, error) {
logger := log.FromContext(ctx)
envVars := createEnvVars(ctx, r.envs)
sort.Slice(r.platform.Properties, func(i, j int) bool {
return r.platform.Properties[i].Name < r.platform.Properties[j].Name
})
command := &rpb.Command{
Arguments: r.args,
EnvironmentVariables: envVars,
Platform: r.platform,
}
logger.Debugf("setup for outputs: %v", r.outputs)
cleanCWD := r.filepath.Clean(r.gomaReq.GetCwd())
cleanRootDir := r.filepath.Clean(r.tree.RootDir())
// set output files from command line flags.
for _, output := range r.outputs {
rel, err := rootRel(r.filepath, output, cleanCWD, cleanRootDir)
if err != nil {
return nil, fmt.Errorf("output %s: %v", output, err)
}
if r.cmdConfig.GetCmdDescriptor().GetCross().GetWindowsCross() {
rel = winpath.ToPosix(rel)
}
command.OutputFiles = append(command.OutputFiles, rel)
}
sort.Strings(command.OutputFiles)
logger.Debugf("setup for output dirs: %v", r.outputDirs)
// set output dirs from command line flags.
for _, output := range r.outputDirs {
rel, err := rootRel(r.filepath, output, cleanCWD, cleanRootDir)
if err != nil {
return nil, fmt.Errorf("output dir %s: %v", output, err)
}
if r.cmdConfig.GetCmdDescriptor().GetCross().GetWindowsCross() {
rel = winpath.ToPosix(rel)
}
command.OutputDirectories = append(command.OutputDirectories, rel)
}
sort.Strings(command.OutputDirectories)
return command, nil
}
func (r *request) checkCache(ctx context.Context) (*rpb.ActionResult, bool) {
if r.err != nil {
// no need to ask to execute.
return nil, true
}
logger := log.FromContext(ctx)
if skipCacheLookup(r.gomaReq) {
logger.Infof("store_only; skip cache lookup")
return nil, false
}
resp, err := r.client.Cache().GetActionResult(ctx, &rpb.GetActionResultRequest{
InstanceName: r.instanceName(),
ActionDigest: r.actionDigest,
})
if err != nil {
switch status.Code(err) {
case codes.NotFound:
logger.Infof("no cached action %v: %v", r.actionDigest, err)
case codes.Unavailable, codes.Canceled, codes.Aborted:
logger.Warnf("get action result %v: %v", r.actionDigest, err)
default:
logger.Errorf("get action result %v: %v", r.actionDigest, err)
}
return nil, false
}
return resp, true
}
func (r *request) missingBlobs(ctx context.Context) ([]*rpb.Digest, error) {
if r.err != nil {
return nil, r.err
}
var blobs []*rpb.Digest
err := rpc.Retry{}.Do(ctx, func() error {
var err error
blobs, err = r.cas.Missing(ctx, r.instanceName(), r.digestStore.List())
return fixRBEInternalError(err)
})
if err != nil {
r.err = err
return nil, err
}
return blobs, nil
}
func inputForDigest(ds *digest.Store, d *rpb.Digest) (string, error) {
src, ok := ds.GetSource(d)
if !ok {
return "", fmt.Errorf("not found for %s", d)
}
idd, ok := src.(inputDigestData)
if !ok {
return "", fmt.Errorf("not input file for %s", d)
}
return idd.filename, nil
}
type byInputFilenames struct {
order map[string]int
resp *gomapb.ExecResp
}
func (b byInputFilenames) Len() int { return len(b.resp.MissingInput) }
func (b byInputFilenames) Swap(i, j int) {
b.resp.MissingInput[i], b.resp.MissingInput[j] = b.resp.MissingInput[j], b.resp.MissingInput[i]
b.resp.MissingReason[i], b.resp.MissingReason[j] = b.resp.MissingReason[j], b.resp.MissingReason[i]
}
func (b byInputFilenames) Less(i, j int) bool {
io := b.order[b.resp.MissingInput[i]]
jo := b.order[b.resp.MissingInput[j]]
return io < jo
}
func sortMissing(inputs []*gomapb.ExecReq_Input, resp *gomapb.ExecResp) {
m := make(map[string]int)
for i, input := range inputs {
m[input.GetFilename()] = i
}
sort.Sort(byInputFilenames{
order: m,
resp: resp,
})
}
// thinOutMissing thins out missint inputs if it is more than limit.
// Note: sortMissing should be called after this to preserve the file name order.
func thinOutMissing(resp *gomapb.ExecResp, limit int) {
if limit == 0 || len(resp.MissingInput) < limit { // no need to thin out.
return
}
rand.Shuffle(len(resp.MissingInput), func(i, j int) {
resp.MissingInput[i], resp.MissingInput[j] = resp.MissingInput[j], resp.MissingInput[i]
})
resp.MissingInput = resp.MissingInput[:limit]
}
func logFileList(logger log.Logger, msg string, files []string) {
s := fmt.Sprintf("%q", files)
const logLineThreshold = 95 * 1024
if len(s) < logLineThreshold {
logger.Infof("%s %s", msg, s)
return
}
logger.Warnf("too many %s %d", msg, len(files))
var b strings.Builder
var i int
for len(files) > 0 {
if b.Len() > 0 {
fmt.Fprintf(&b, " ")
}
s, files = files[0], files[1:]
fmt.Fprintf(&b, "%q", s)
if b.Len() > logLineThreshold {
logger.Infof("%s %d: [%s]", msg, i, b.String())
i++
b.Reset()
}
}
if b.Len() > 0 {
logger.Infof("%s %d: [%s]", msg, i, b)
}
}
func (r *request) uploadBlobs(ctx context.Context, blobs []*rpb.Digest) (*gomapb.ExecResp, error) {
if r.err != nil {
return nil, r.err
}
err := r.cas.Upload(ctx, r.instanceName(), r.f.CASBlobLookupSema, blobs...)
if err != nil {
if missing, ok := err.(cas.MissingError); ok {
logger := log.FromContext(ctx)
logger.Infof("failed to upload blobs %s", missing.Blobs)
var missingInputs []string
var missingReason []string
for _, b := range missing.Blobs {
fname, err := inputForDigest(r.digestStore, b.Digest)
if err != nil {
logger.Warnf("unknown input for %s: %v", b.Digest, err)
continue
}
missingInputs = append(missingInputs, fname)
missingReason = append(missingReason, b.Err.Error())
}
if len(missingInputs) > 0 {
r.gomaResp.MissingInput = missingInputs
r.gomaResp.MissingReason = missingReason
thinOutMissing(r.gomaResp, r.f.MissingInputLimit)
sortMissing(r.gomaReq.Input, r.gomaResp)
logFileList(logger, "missing inputs", r.gomaResp.MissingInput)
return r.gomaResp, nil
}
// failed to upload non-input, so no need to report
// missing input to users.
// handle it as grpc error.
}
r.err = err
}
return nil, err
}
func (r *request) executeAction(ctx context.Context) (*rpb.ExecuteResponse, error) {
if r.err != nil {
return nil, r.Err()
}
_, resp, err := ExecuteAndWait(ctx, r.client, &rpb.ExecuteRequest{
InstanceName: r.instanceName(),
SkipCacheLookup: skipCacheLookup(r.gomaReq),
ActionDigest: r.actionDigest,
// ExecutionPolicy
// ResultsCachePolicy
})
if err != nil {
r.err = err
return nil, r.Err()
}
return resp, nil
}
func timestampSub(ctx context.Context, t1, t2 *tspb.Timestamp) time.Duration {
time1 := t1.AsTime()
time2 := t2.AsTime()
return time1.Sub(time2)
}
func (r *request) newResp(ctx context.Context, eresp *rpb.ExecuteResponse, cached bool) (*gomapb.ExecResp, error) {
logger := log.FromContext(ctx)
if r.err != nil {
return nil, r.Err()
}
logger.Debugf("response %v cached=%t", eresp, cached)
r.gomaResp.CacheKey = proto.String(r.actionDigest.String())
switch {
case eresp.CachedResult:
r.gomaResp.CacheHit = gomapb.ExecResp_STORAGE_CACHE.Enum()
case cached:
r.gomaResp.CacheHit = gomapb.ExecResp_MEM_CACHE.Enum()
default:
r.gomaResp.CacheHit = gomapb.ExecResp_NO_CACHE.Enum()
}
if st := eresp.GetStatus(); st.GetCode() != 0 {
logger.Errorf("execute status error: %v", st)
s := status.FromProto(st)
r.gomaResp.ErrorMessage = append(r.gomaResp.ErrorMessage, fmt.Sprintf("Execute error: %s", s.Code()))
logger.Errorf("resp %v", r.gomaResp)
return r.gomaResp, nil
}
if eresp.Result == nil {
r.gomaResp.ErrorMessage = append(r.gomaResp.ErrorMessage, "unexpected response message")
logger.Errorf("resp %v", r.gomaResp)
return r.gomaResp, nil
}
md := eresp.Result.GetExecutionMetadata()
queueTime := timestampSub(ctx, md.GetWorkerStartTimestamp(), md.GetQueuedTimestamp())
workerTime := timestampSub(ctx, md.GetWorkerCompletedTimestamp(), md.GetWorkerStartTimestamp())
inputTime := timestampSub(ctx, md.GetInputFetchCompletedTimestamp(), md.GetInputFetchStartTimestamp())
execTime := timestampSub(ctx, md.GetExecutionCompletedTimestamp(), md.GetExecutionStartTimestamp())
outputTime := timestampSub(ctx, md.GetOutputUploadCompletedTimestamp(), md.GetOutputUploadStartTimestamp())
osFamily := platformOSFamily(r.platform)
dockerRuntime := platformDockerRuntime(r.platform)
crossCompileType := crossCompileType(r.cmdConfig.GetCmdDescriptor().GetCross())
logger.Infof("exit=%d cache=%s : exec on %q[%s, %s, cross:%s, target=%s] queue=%s worker=%s input=%s exec=%s output=%s",
eresp.Result.GetExitCode(),
r.gomaResp.GetCacheHit(),
md.GetWorker(),
osFamily,
dockerRuntime,
crossCompileType,
r.crossTarget,
queueTime,
workerTime,
inputTime,
execTime,
outputTime)
tags := []tag.Mutator{
// exit_code=159 is seccomp violation.
tag.Upsert(rbeExitKey, fmt.Sprintf("%d", eresp.Result.GetExitCode())),
tag.Upsert(rbeCacheKey, r.gomaResp.GetCacheHit().String()),
tag.Upsert(rbePlatformOSFamilyKey, osFamily),
tag.Upsert(rbePlatformDockerRuntimeKey, dockerRuntime),
tag.Upsert(rbeCrossKey, crossCompileType),
}
stats.RecordWithTags(ctx, tags, rbeQueueTime.M(float64(queueTime.Nanoseconds())/1e6))
stats.RecordWithTags(ctx, tags, rbeWorkerTime.M(float64(workerTime.Nanoseconds())/1e6))
stats.RecordWithTags(ctx, tags, rbeInputTime.M(float64(inputTime.Nanoseconds())/1e6))
stats.RecordWithTags(ctx, tags, rbeExecTime.M(float64(execTime.Nanoseconds())/1e6))
stats.RecordWithTags(ctx, tags, rbeOutputTime.M(float64(outputTime.Nanoseconds())/1e6))
r.gomaResp.ExecutionStats = &gomapb.ExecutionStats{
ExecutionStartTimestamp: md.GetExecutionStartTimestamp(),
ExecutionCompletedTimestamp: md.GetExecutionCompletedTimestamp(),
}
gout := gomaOutput{
gomaResp: r.gomaResp,
bs: r.client.ByteStream(),
instance: r.instanceName(),
gomaFile: r.f.GomaFile,
}
// gomaOutput should return err for codes.Unauthenticated,
// instead of setting ErrorMessage in r.gomaResp,
// so it returns to caller (i.e. frontend), and retry with new
// refreshed oauth2 access token.
for _, f := range []func(context.Context, *rpb.ExecuteResponse) error{
gout.stdoutData,
gout.stderrData,
} {
err := f(ctx, eresp)
if status.Code(err) == codes.Unauthenticated && r.err == nil {
r.err = err
return r.gomaResp, r.Err()
}
}
if len(r.gomaResp.Result.StdoutBuffer) > 0 {
// docker failure would be error of goma server, not users.
// so make it internal error, rather than command execution error.
// http://b/80272874
const dockerErrorResponse = "docker: Error response from daemon: oci runtime error:"
if eresp.Result.ExitCode == 127 &&
bytes.Contains(r.gomaResp.Result.StdoutBuffer, []byte(dockerErrorResponse)) {
logger.Errorf("docker error response %s", shortLogMsg(r.gomaResp.Result.StdoutBuffer))
return r.gomaResp, status.Errorf(codes.Internal, "docker error: %s", string(r.gomaResp.Result.StdoutBuffer))
}
if eresp.Result.ExitCode != 0 {
logLLVMError(logger, "stdout", r.gomaResp.Result.StdoutBuffer)
}
logger.Infof("stdout %s", shortLogMsg(r.gomaResp.Result.StdoutBuffer))
}
if len(r.gomaResp.Result.StderrBuffer) > 0 {
if eresp.Result.ExitCode != 0 {
logLLVMError(logger, "stderr", r.gomaResp.Result.StderrBuffer)
}
logger.Infof("stderr %s", shortLogMsg(r.gomaResp.Result.StderrBuffer))
}
for _, output := range eresp.Result.OutputFiles {
if r.err != nil {
break
}
// output.Path should not be absolute, but relative to root dir.
// convert it to fname, which is cwd relative.
fname, err := r.filepath.Rel(r.gomaReq.GetCwd(), r.filepath.Join(r.tree.RootDir(), output.Path))
if err != nil {
r.gomaResp.ErrorMessage = append(r.gomaResp.ErrorMessage, fmt.Sprintf("output path %s: %v", output.Path, err))
continue
}
err = gout.outputFile(ctx, fname, output)
if err != nil && r.err == nil {
r.err = err
return r.gomaResp, r.Err()
}
}
for _, output := range eresp.Result.OutputDirectories {
if r.err != nil {
break
}
// output.Path should not be absolute, but relative to root dir.
// convert it to fname, which is cwd relative.
fname, err := r.filepath.Rel(r.gomaReq.GetCwd(), r.filepath.Join(r.tree.RootDir(), output.Path))
if err != nil {
r.gomaResp.ErrorMessage = append(r.gomaResp.ErrorMessage, fmt.Sprintf("output path %s: %v", output.Path, err))
continue
}
err = gout.outputDirectory(ctx, r.filepath, fname, output, r.f.OutputFileSema)
if err != nil && r.err == nil {
r.err = err
return r.gomaResp, r.Err()
}
}
if len(r.gomaResp.ErrorMessage) == 0 {
r.gomaResp.Result.ExitStatus = proto.Int32(eresp.Result.ExitCode)
}
sizeLimit := exec.DefaultMaxRespMsgSize
respSize := proto.Size(r.gomaResp)
if respSize > sizeLimit {
logger.Infof("gomaResp size=%d, limit=%d, using FileService for larger blobs.", respSize, sizeLimit)
if err := gout.reduceRespSize(ctx, sizeLimit, r.f.OutputFileSema); err != nil {
// Don't need to append any error messages to `r.gomaResp` because it won't be sent.
return nil, fmt.Errorf("failed to reduce resp size below limit=%d, %d -> %d: %v", sizeLimit, respSize, proto.Size(gout.gomaResp), err)
}
logger.Infof("gomaResp size reduced %d -> %d", respSize, proto.Size(gout.gomaResp))
}
return r.gomaResp, r.Err()
}
func platformOSFamily(p *rpb.Platform) string {
for _, p := range p.Properties {
if p.Name == "OSFamily" {
return p.Value
}
}
return "unspecified"
}
func platformDockerRuntime(p *rpb.Platform) string {
priv := false
runAsRoot := false
for _, p := range p.Properties {
switch p.Name {
case "dockerRuntime":
return p.Value
case "dockerPrivileged":
priv = p.Value == "true"
case "dockerRunAsRoot":
runAsRoot = p.Value == "true"
}
}
switch {
case priv && runAsRoot:
return "nsjail-chroot"
case priv:
return "nsjail"
}
return "default"
}
func crossCompileType(cross *cmdpb.CmdDescriptor_Cross) string {
switch {
case cross.GetWindowsCross():
return "win"
case cross.GetClangNeedTarget():
return "need-target"
}
return "no"
}
func shortLogMsg(msg []byte) string {
if len(msg) <= 1024 {
return string(msg)
}
var b strings.Builder
b.Write(msg[:512])
fmt.Fprint(&b, "...")
b.Write(msg[len(msg)-512:])
return b.String()
}
// logLLVMError records LLVM ERROR.
// http://b/145177862
func logLLVMError(logger log.Logger, id string, msg []byte) {
llvmErrorMsg, ok := extractLLVMError(msg)
if !ok {
return
}
logger.Errorf("%s: %s", id, llvmErrorMsg)
}
func extractLLVMError(msg []byte) ([]byte, bool) {
const llvmError = "LLVM ERROR:"
i := bytes.Index(msg, []byte(llvmError))
if i < 0 {
return nil, false
}
llvmErrorMsg := msg[i:]
i = bytes.IndexAny(llvmErrorMsg, "\r\n")
if i >= 0 {
llvmErrorMsg = llvmErrorMsg[:i]
}
return llvmErrorMsg, true
}