// Copyright 2018-2021 Bloomberg Finance L.P
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <actionbuilder.h>
#include <deps.h>
#include <env.h>
#include <executioncontext.h>
#include <fileutils.h>
#include <grpcchannels.h>
#include <metricsconfig.h>
#include <parsedcommandfactory.h>
#include <reccdefaults.h>
#include <regexutils.h>
#include <shellutils.h>
#include <subprocess.h>

#include <cstdio>
#include <cstring>
#include <grpc/grpc.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/security/credentials.h>
#include <iostream>
#include <random>
#include <sys/stat.h>
#include <unistd.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_executionclient.h>
#include <buildboxcommon_jobserver.h>
#include <buildboxcommon_localexecutionclient.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_remoteexecutionclient.h>
#include <buildboxcommon_version.h>
#include <buildboxcommonmetrics_countingmetricutil.h>
#include <buildboxcommonmetrics_durationmetrictimer.h>
#include <buildboxcommonmetrics_durationmetricvalue.h>
#include <buildboxcommonmetrics_metricsconfigutil.h>
#include <buildboxcommonmetrics_metricteeguard.h>
#include <buildboxcommonmetrics_publisherguard.h>
#include <buildboxcommonmetrics_statsdpublisher.h>
#include <buildboxcommonmetrics_totaldurationmetricvalue.h>

using namespace buildboxcommon;

static const std::string TIMER_NAME_EXECUTE_LOCAL_NO_ACTION_RESULT =
    "recc.execute_local_no_action_result";
static const std::string TIMER_NAME_EXECUTE_LOCAL_WITH_ACTION_RESULT =
    "recc.execute_local_with_action_result";
static const std::string TIMER_NAME_EXECUTE_REMOTE_WITH_ACTION_RESULT =
    "recc.execute_remote_with_action_result";

static const std::string TIMER_NAME_FIND_MISSING_BLOBS =
    "recc.find_missing_blobs";
static const std::string TIMER_NAME_QUERY_ACTION_CACHE =
    "recc.query_action_cache";
static const std::string TIMER_NAME_UPLOAD_MISSING_INPUT_BLOBS =
    "recc.upload_missing_input_blobs";
static const std::string TIMER_NAME_UPLOAD_MISSING_LOCAL_BUILD_OUTPUT_BLOBS =
    "recc.upload_missing_local_build_output_blobs";
static const std::string TIMER_NAME_DOWNLOAD_BLOBS = "recc.download_blobs";

static const std::string COUNTER_NAME_ACTION_CACHE_HIT =
    "recc.action_cache_hit";
static const std::string COUNTER_NAME_ACTION_CACHE_MISS =
    "recc.action_cache_miss";
static const std::string COUNTER_NAME_ACTION_CACHE_SKIP =
    "recc.action_cache_skip";
static const std::string COUNTER_NAME_LINK_ACTION_CACHE_HIT =
    "recc.link_action_cache_hit";
static const std::string COUNTER_NAME_LINK_ACTION_CACHE_MISS =
    "recc.link_action_cache_miss";
static const std::string COUNTER_NAME_UPLOAD_BLOBS_CACHE_HIT =
    "recc.upload_blobs_cache_hit";
static const std::string COUNTER_NAME_UPLOAD_BLOBS_CACHE_MISS =
    "recc.upload_blobs_cache_miss";
static const std::string COUNTER_NAME_INPUT_SIZE_BYTES =
    "recc.input_size_bytes";
static const std::string COUNTER_NAME_UNSUPPORTED_COMMAND =
    "recc.unsupported_command";
static const std::string COUNTER_NAME_INVALID_PATHS =
    "recc.invalid_paths_command";

namespace recc {

ExecutionContext::ExecutionContext()
{
    // Explicitly initialize gRPC core to allow explicit shutdown.
    grpc_init();
}

ExecutionContext::~ExecutionContext()
{
    // Explicitly shutdown gRPC core to ensure shutdown doesn't happen
    // asynchronously in a background thread, which may interfere with OpenSSL
    // cleanup in the main thread on exit.
    this->d_casClient.reset();
    // Use deprecated `grpc_shutdown_blocking()` instead of `grpc_shutdown()`
    // to work properly with gRPC versions lower than 1.33.
    grpc_shutdown_blocking();
}

int ExecutionContext::execLocally(int argc, char *argv[])
{
    buildboxcommonmetrics::MetricTeeGuard<
        buildboxcommonmetrics::DurationMetricTimer>
        mt(TIMER_NAME_EXECUTE_LOCAL_NO_ACTION_RESULT,
           d_addDurationMetricCallback);

    auto subprocessResult = Subprocess::execute(
        std::vector<std::string>(argv, argv + argc), false, false);
    return subprocessResult.d_exitCode;
}

proto::ActionResult ExecutionContext::execLocallyWithActionResult(
    int argc, char *argv[], buildboxcommon::digest_string_map *blobs,
    buildboxcommon::digest_string_map *digest_to_filepaths,
    const std::set<std::string> &products,
    const std::set<std::string> &makeDependencyProducts,
    const std::string &modifiedUploadsDirectory)
{
    buildboxcommonmetrics::MetricTeeGuard<
        buildboxcommonmetrics::DurationMetricTimer>
        mt(TIMER_NAME_EXECUTE_LOCAL_WITH_ACTION_RESULT,
           d_addDurationMetricCallback);

    proto::ActionResult actionResult;

    auto subprocessResult = Subprocess::execute(
        std::vector<std::string>(argv, argv + argc), true, true);
    std::cout << subprocessResult.d_stdOut;
    std::cerr << subprocessResult.d_stdErr;

    actionResult.set_exit_code(subprocessResult.d_exitCode);

    // Digest captured streams and mark them for upload
    const auto stdoutDigest = DigestGenerator::hash(subprocessResult.d_stdOut);
    const auto stderrDigest = DigestGenerator::hash(subprocessResult.d_stdErr);
    (*blobs)[stdoutDigest] = subprocessResult.d_stdOut;
    (*blobs)[stderrDigest] = subprocessResult.d_stdErr;
    actionResult.mutable_stdout_digest()->CopyFrom(stdoutDigest);
    actionResult.mutable_stderr_digest()->CopyFrom(stderrDigest);

    std::vector<std::string> contentsPaths;
    std::unordered_map<std::string, std::string> contentsPathToOutputPathMap;

    for (const std::string &outputPath : products) {
        // Only upload products produced by the compiler
        if (buildboxcommon::FileUtils::isRegularFile(outputPath.c_str())) {

            std::string contentsPath = outputPath;

            // If this file is a make dependency file and there is a prefix map
            // then the paths inside the file will need remapping.
            if (makeDependencyProducts.find(outputPath) !=
                makeDependencyProducts.end()) {
                contentsPath =
                    FileUtils::resolvePathsInsideDependencyFileForRemote(
                        outputPath, modifiedUploadsDirectory);
            }

            if (RECC_USE_LOCALCAS) {
                // buildbox-casd requires absolute paths
                contentsPath = buildboxcommon::FileUtils::makePathAbsolute(
                    contentsPath, SystemUtils::getCurrentWorkingDirectory());
            }

            contentsPaths.push_back(contentsPath);
            contentsPathToOutputPathMap[contentsPath] = outputPath;
        }
    }

    if (RECC_USE_LOCALCAS) {
        // Off-load hashing to buildbox-casd to improve efficiency
        // and populate the inode cache.
        auto responses = d_casClient->captureFiles(
            contentsPaths, {}, false /* bypass_local_cache */,
            true /* skip_upload */);
        populateHashesFromResponses(responses, actionResult,
                                    digest_to_filepaths,
                                    contentsPathToOutputPathMap);
    }
    else {
        for (const auto &[contentsPath, outputPath] :
             contentsPathToOutputPathMap) {
            // Add the output file to the action result with the original file
            // path and desired file contents.
            const auto file = File(contentsPath.c_str());
            (*digest_to_filepaths)[file.d_digest] = contentsPath;
            auto outputFile = actionResult.add_output_files();
            outputFile->set_path(outputPath);
            outputFile->mutable_digest()->CopyFrom(file.d_digest);
            outputFile->set_is_executable(file.d_executable);
        }
    }

    return actionResult;
}

/**
 * Populate the given ActionResult with output file digests from the given
 * responses
 */
template <typename Response>
void ExecutionContext::populateHashesFromResponses(
    Response &responses, proto::ActionResult &actionResult,
    buildboxcommon::digest_string_map *digest_to_filepaths,
    std::unordered_map<std::string, std::string> &contentsPathToOutputPathMap)
{
    for (auto &response : responses) {
        const auto &status = response.status();
        if (status.code() == grpc::StatusCode::PERMISSION_DENIED) {
            // Fall back to in-process hashing in case buildbox-casd
            // was unable to access the input file.
            BUILDBOX_LOG_DEBUG("LocalCAS CaptureFiles fallback for \""
                               << response.path() << "\"");
            const auto file = File(response.path().c_str());
            response.mutable_digest()->CopyFrom(file.d_digest);
            response.set_is_executable(file.d_executable);
            *response.mutable_node_properties() = file.d_nodeProperties;
        }
        else if (status.code() != grpc::StatusCode::OK) {
            GrpcError::throwGrpcError(
                grpc::Status(static_cast<grpc::StatusCode>(status.code()),
                             status.message()));
        }
        (*digest_to_filepaths)[response.digest()] = response.path();
        auto outputFile = actionResult.add_output_files();
        outputFile->set_path(contentsPathToOutputPathMap[response.path()]);
        outputFile->mutable_digest()->CopyFrom(response.digest());
        outputFile->set_is_executable(response.is_executable());
    }
}

/**
 * Upload the given resources to the CAS server. This first sends a
 * FindMissingBlobsRequest to determine which resources need to be
 * uploaded, then uses the ByteStream and BatchUpdateBlobs APIs to upload
 * them.
 */
void ExecutionContext::uploadResources(
    const digest_string_map &blobs,
    const digest_string_map &digest_to_filepaths,
    const std::string &uploadBlobsCounterMetricName)
{
    std::vector<proto::Digest> digestsToUpload;
    std::vector<proto::Digest> missingDigests;

    for (const auto &i : blobs) {
        digestsToUpload.push_back(i.first);
    }
    for (const auto &i : digest_to_filepaths) {
        digestsToUpload.push_back(i.first);
    }

    {
        // Timed block
        buildboxcommonmetrics::MetricTeeGuard<
            buildboxcommonmetrics::DurationMetricTimer>
            mt(TIMER_NAME_FIND_MISSING_BLOBS, d_addDurationMetricCallback);

        missingDigests = d_casClient->findMissingBlobs(digestsToUpload);
    }

    std::vector<CASClient::UploadRequest> upload_requests;
    upload_requests.reserve(missingDigests.size());
    for (const auto &digest : missingDigests) {
        // Finding the data in one of the source maps:
        if (blobs.count(digest)) {
            upload_requests.emplace_back(digest, blobs.at(digest));
        }
        else if (digest_to_filepaths.count(digest)) {
            const auto &path = digest_to_filepaths.at(digest);
            upload_requests.push_back(
                CASClient::UploadRequest::from_path(digest, path));
        }
        else {
            throw std::runtime_error(
                "FindMissingBlobs returned non-existent digest");
        }
    }

    {
        // Timed block
        buildboxcommonmetrics::MetricTeeGuard<
            buildboxcommonmetrics::DurationMetricTimer>
            mt(uploadBlobsCounterMetricName, d_addDurationMetricCallback);

        d_casClient->uploadBlobs(upload_requests);
    }

    const int64_t uploadCacheHits =
        static_cast<int64_t>(digestsToUpload.size() - missingDigests.size());
    const int64_t uploadCacheMisses =
        static_cast<int64_t>(missingDigests.size());
    recordCounterMetric(COUNTER_NAME_UPLOAD_BLOBS_CACHE_HIT, uploadCacheHits);
    recordCounterMetric(COUNTER_NAME_UPLOAD_BLOBS_CACHE_MISS,
                        uploadCacheMisses);
}

int64_t ExecutionContext::calculateTotalSize(
    const digest_string_map &blobs,
    const digest_string_map &digest_to_filepaths)
{
    int64_t totalSize = 0;

    for (const auto &i : blobs) {
        totalSize += i.first.size_bytes();
    }
    for (const auto &i : digest_to_filepaths) {
        totalSize += i.first.size_bytes();
    }

    return totalSize;
}

void ExecutionContext::setStopToken(const std::atomic_bool &stop_requested)
{
    this->d_stopRequested = &stop_requested;
}

std::string getRandomString()
{
    std::random_device randomDevice;
    std::uniform_int_distribution<uint32_t> randomDistribution;
    std::stringstream stream;
    const int RANDOM_STRING_LENGTH = 8;
    stream << std::hex << std::setw(RANDOM_STRING_LENGTH) << std::setfill('0')
           << randomDistribution(randomDevice);
    return stream.str();
}

ExecutionContext::ParseConfigOption ExecutionContext::getParseConfigOption()
{
    return parseConfigOption;
}

void ExecutionContext::disableConfigParsing()
{
    parseConfigOption = ExecutionContext::SKIP_PARSING;
}

void logActionResult(const proto::ActionResult &resultProto)
{
    BUILDBOX_LOG_DEBUG("Action result contains: [Files="
                       << resultProto.output_files_size() << "], [Directories="
                       << resultProto.output_directories_size() << "]");
    for (int i = 0; i < resultProto.output_files_size(); ++i) {
        const auto &fileProto = resultProto.output_files(i);
        BUILDBOX_LOG_DEBUG("File digest=["
                           << fileProto.digest().hash() << "/"
                           << fileProto.digest().size_bytes() << "] :"
                           << " path=[" << fileProto.path() << "]");
    }
    for (int i = 0; i < resultProto.output_directories_size(); ++i) {
        const auto &dirProto = resultProto.output_directories(i);
        BUILDBOX_LOG_DEBUG("Directory tree digest=["
                           << dirProto.tree_digest().hash() << "/"
                           << dirProto.tree_digest().size_bytes() << "] :"
                           << " path=[" << dirProto.path() << "]");
    }
}

int ExecutionContext::execute(int argc, char *argv[])
{
    if (parseConfigOption == PARSE_CONFIG) {
        Env::try_to_parse_recc_config();
        Env::run_config_sanity_checks();
    }
    return ExecutionContext::executeConfigured(argc, argv);
}

std::shared_ptr<proto::Action> ExecutionContext::tryCreateAction(
    buildboxcommon::digest_string_map *blobs,
    buildboxcommon::digest_string_map *digest_to_filepaths,
    std::set<std::string> *products, const ParsedCommand &command,
    const std::string &cwd)
{
    if (command.is_compiler_command() ||
        ((RECC_LINK || RECC_LINK_METRICS_ONLY) &&
         command.is_linker_command()) ||
        RECC_FORCE_REMOTE) {
        try {
            ActionBuilder actionBuilder(
                d_addDurationMetricCallback, d_recordCounterMetricCallback,
                RECC_USE_LOCALCAS ? d_casClient : nullptr);
            auto actionPtr = actionBuilder.BuildAction(
                command, cwd, blobs, digest_to_filepaths, products);

            // Calculate and record total size of input blobs
            const int64_t inputSize =
                calculateTotalSize(*blobs, *digest_to_filepaths);
            recordCounterMetric(COUNTER_NAME_INPUT_SIZE_BYTES, inputSize);

            return actionPtr;
        }
        catch (const std::exception &e) {
            BUILDBOX_LOG_ERROR("Failed to build Action: " << e.what());
            throw;
        }
    }
    else {
        recordCounterMetric(COUNTER_NAME_UNSUPPORTED_COMMAND, 1);
        return nullptr;
    }
}

void ExecutionContext::setupCASClient(
    std::shared_ptr<GrpcChannels> *returnChannels,
    std::shared_ptr<GrpcClient> *casGrpcClient)
{
    try {
        *returnChannels = std::make_shared<GrpcChannels>(
            GrpcChannels::get_channels_from_config());
    }
    catch (const std::exception &e) {
        BUILDBOX_LOG_ERROR("Invalid argument in channel config: " << e.what());
        throw;
    }

    const buildboxcommon::ConnectionOptions *casOptions =
        (*returnChannels)->cas();

    *casGrpcClient = std::make_shared<GrpcClient>();
    (*casGrpcClient)->init(*casOptions);

    (*casGrpcClient)->setToolDetails("recc", buildboxcommon::VERSION);
    (*casGrpcClient)
        ->setRequestMetadata(
            "", RECC_TOOL_INVOCATION_ID, RECC_CORRELATED_INVOCATIONS_ID,
            RECC_ACTION_MNEMONIC, RECC_TARGET_ID, RECC_CONFIGURATION_ID);

    d_casClient = std::make_shared<CASClient>(*casGrpcClient);
    d_casClient->init(RECC_CAS_GET_CAPABILITIES);

    constexpr std::string_view UNIX_SOCKET_PREFIX = "unix:";
    if (casOptions->d_url.starts_with(UNIX_SOCKET_PREFIX)) {
        struct stat st{};
        const auto socketPath =
            casOptions->d_url.substr(UNIX_SOCKET_PREFIX.size());
        if (stat(socketPath.c_str(), &st) < 0) {
            BUILDBOX_LOG_WARNING("Failed to stat UNIX socket '"
                                 << socketPath
                                 << "': " << std::strerror(errno));
        }
        else {
            // UNIX socket created by buildbox-casd is owned by the effective
            // user ID of recc. This is a strong indication that buildbox-casd
            // is running as the same user as recc and thus, has write access
            // to the same directories as recc.
            d_casdSameUserHint = st.st_uid == geteuid();
        }
    }
}

void ExecutionContext::setupExecClient(
    const GrpcChannels &returnChannels,
    std::shared_ptr<GrpcClient> *actionCacheGrpcClient,
    std::shared_ptr<ExecutionClient> *execClient,
    const proto::Digest &actionDigest, bool local_runner)
{
    *actionCacheGrpcClient = std::make_shared<GrpcClient>();
    (*actionCacheGrpcClient)->init(*returnChannels.action_cache());

    (*actionCacheGrpcClient)->setToolDetails("recc", buildboxcommon::VERSION);
    (*actionCacheGrpcClient)
        ->setRequestMetadata(
            proto::toString(actionDigest), RECC_TOOL_INVOCATION_ID,
            RECC_CORRELATED_INVOCATIONS_ID, RECC_ACTION_MNEMONIC,
            RECC_TARGET_ID, RECC_CONFIGURATION_ID);

    if (!local_runner) {
        auto executionGrpcClient = std::make_shared<GrpcClient>();
        executionGrpcClient->init(*returnChannels.server());
        executionGrpcClient->setToolDetails("recc", buildboxcommon::VERSION);
        executionGrpcClient->setRequestMetadata(
            proto::toString(actionDigest), RECC_TOOL_INVOCATION_ID,
            RECC_CORRELATED_INVOCATIONS_ID, RECC_ACTION_MNEMONIC,
            RECC_TARGET_ID, RECC_CONFIGURATION_ID);

        *execClient = std::make_shared<RemoteExecutionClient>(
            executionGrpcClient, *actionCacheGrpcClient);

        if (RECC_VERBOSE && RECC_LOG_PROGRESS) {
            // Downcasting here is safe because `execClient` is
            // guaranteed to be a `RemoteExecutionClient` in this remote
            // build code branch.
            std::dynamic_pointer_cast<buildboxcommon::RemoteExecutionClient>(
                *execClient)
                ->enableProgressLog();
        }
    }
    else {
        auto localExecClient = std::make_shared<LocalExecutionClient>(
            *returnChannels.cas(), *actionCacheGrpcClient);
        if (!RECC_CACHE_UPLOAD_LOCAL_BUILD) {
            localExecClient->disableActionCacheUpdates();
        }
        auto runnerCommand = ShellUtils::splitCommand(RECC_RUNNER_COMMAND);
        if (runnerCommand.size() < 1) {
            BUILDBOXCOMMON_THROW_EXCEPTION(std::invalid_argument,
                                           "Empty runner command \""
                                               << RECC_RUNNER_COMMAND << "\"");
        }
        const std::vector<std::string> runnerArgs(runnerCommand.begin() + 1,
                                                  runnerCommand.end());
        localExecClient->setRunner(runnerCommand[0], runnerArgs);
        *execClient = localExecClient;
    }
    (*execClient)->init();
}

void ExecutionContext::queryActionCache(
    bool *action_in_cache, ActionResult *result,
    const std::shared_ptr<ExecutionClient> &execClient,
    const Digest &actionDigest, const ParsedCommand &command)
{
    try {
        { // Timed block
            buildboxcommonmetrics::MetricTeeGuard<
                buildboxcommonmetrics::DurationMetricTimer>
                mt(TIMER_NAME_QUERY_ACTION_CACHE, d_addDurationMetricCallback);

            *action_in_cache = execClient->fetchFromActionCache(
                actionDigest, command.get_products(), result);
            if (*action_in_cache &&
                (!RECC_IGNORE_FAILURE_RESULT || result->exit_code() == 0)) {
                recordCounterMetric(command.is_linker_command()
                                        ? COUNTER_NAME_LINK_ACTION_CACHE_HIT
                                        : COUNTER_NAME_ACTION_CACHE_HIT,
                                    1);
                BUILDBOX_LOG_INFO("Action Cache hit for [" << actionDigest
                                                           << "]");
            }
            else {
                if (*action_in_cache && RECC_IGNORE_FAILURE_RESULT &&
                    result->exit_code() != 0) {
                    BUILDBOX_LOG_INFO("Ignored failure results in cache");
                }
                recordCounterMetric(command.is_linker_command()
                                        ? COUNTER_NAME_LINK_ACTION_CACHE_MISS
                                        : COUNTER_NAME_ACTION_CACHE_MISS,
                                    1);
            }
        }
    }
    catch (const std::exception &e) {
        BUILDBOX_LOG_ERROR("Error while querying action cache at \""
                           << RECC_ACTION_CACHE_SERVER << "\": " << e.what());
    }
}

int ExecutionContext::executeLocallyAndUploadActionResult(
    int argc, char *argv[], const std::shared_ptr<ExecutionClient> &execClient,
    const std::set<std::string> &products,
    const std::set<std::string> &makeDependencyFiles,
    buildboxcommon::digest_string_map *blobs,
    buildboxcommon::digest_string_map *digest_to_filepaths)
{
    // There is no need to upload input files in cache-only mode.
    digest_to_filepaths->clear();

    // Create a temporary directory to store modified files for
    // uploading to remote.
    auto modifiedUploads =
        buildboxcommon::TemporaryDirectory("modifiedUploads");
    const std::string modifiedUploadsDirectory = modifiedUploads.name();

    const auto actionResult = execLocallyWithActionResult(
        argc, argv, blobs, digest_to_filepaths, products, makeDependencyFiles,
        modifiedUploadsDirectory);
    const size_t number_of_outputs = actionResult.output_files_size();

    if (actionResult.exit_code() != 0 && !RECC_CACHE_UPLOAD_FAILED_BUILD) {
        BUILDBOX_LOG_WARNING("Not uploading actionResult due to exit_code = "
                             << actionResult.exit_code()
                             << ", RECC_CACHE_UPLOAD_FAILED_BUILD = "
                             << std::boolalpha
                             << RECC_CACHE_UPLOAD_FAILED_BUILD);
    }
    else if (number_of_outputs != products.size()) {
        BUILDBOX_LOG_WARNING(
            "Not uploading actionResult due to "
            << (products.size() - number_of_outputs)
            << " of the requested output files not being found");
    }
    else {
        BUILDBOX_LOG_DEBUG("Uploading local build...");
        try {
            uploadResources(
                *blobs, *digest_to_filepaths,
                TIMER_NAME_UPLOAD_MISSING_LOCAL_BUILD_OUTPUT_BLOBS);
        }
        catch (const std::exception &e) {
            BUILDBOX_LOG_WARNING(
                "Error while uploading local build to CAS at \""
                << RECC_CAS_SERVER << "\": " << e.what());
            // Skipping update of action cache
            return actionResult.exit_code();
        }

        try {
            execClient->updateActionCache(d_actionDigest, actionResult);
            BUILDBOX_LOG_INFO("Action cache updated for [" << d_actionDigest
                                                           << "]");
        }
        catch (const std::exception &e) {
            // Only log warning as local execution was still successful
            BUILDBOX_LOG_WARNING(
                "Error while calling `UpdateActionCache()` on \""
                << RECC_ACTION_CACHE_SERVER << "\": " << e.what());
        }
    }

    // Store action result for access by the caller of this method.
    this->d_actionResult = actionResult;

    return actionResult.exit_code();
}

void ExecutionContext::serveOutputFromActionResult(
    std::shared_ptr<ExecutionClient> &execClient, proto::ActionResult &result,
    const std::set<std::string> &products,
    const std::set<std::string> &makeDependencyFiles)
{
    const int exitCode = result.exit_code();
    if (exitCode == 0 && result.output_files_size() == 0 &&
        products.size() != 0) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Action produced none of the of the expected output_files");
    }
    try {
        if (RECC_DONT_SAVE_OUTPUT) {
            // We still call `writeFilesToDisk()` for stdout and stderr.
            // Clear output files and directories to skip download and write.
            result.clear_output_files();
            result.clear_output_symlinks();
            result.clear_output_directories();
        }

        const auto randomStr = getRandomString();

        // Add stdout and stderr as output files if they aren't embedded.
        // This allows download of stdout, stderr and output files in a
        // single batch.
        const std::string stdoutFilename = ".recc-stdout-" + randomStr;
        const std::string stderrFilename = ".recc-stderr-" + randomStr;
        const bool fetchStdout = result.has_stdout_digest() &&
                                 result.stdout_digest().size_bytes() > 0;
        const bool fetchStderr = result.has_stderr_digest() &&
                                 result.stderr_digest().size_bytes() > 0;
        if (fetchStdout) {
            proto::OutputFile outputFile;
            outputFile.mutable_digest()->CopyFrom(result.stdout_digest());
            outputFile.set_path(stdoutFilename);
            *result.add_output_files() = outputFile;
        }
        if (fetchStderr) {
            proto::OutputFile outputFile;
            outputFile.mutable_digest()->CopyFrom(result.stderr_digest());
            outputFile.set_path(stderrFilename);
            *result.add_output_files() = outputFile;
        }

        {
            // Timed block
            buildboxcommonmetrics::MetricTeeGuard<
                buildboxcommonmetrics::DurationMetricTimer>
                mt(TIMER_NAME_DOWNLOAD_BLOBS, d_addDurationMetricCallback);

            bool exportSuccessful = false;

            // First try to use the more efficient LocalCAS export support,
            // if enabled, which will also populate the inode cache to
            // eliminate redundant hashing when using recc also for linking.
            if (RECC_USE_LOCALCAS && d_casdSameUserHint &&
                result.output_directories().empty()) {
                std::vector<OutputFile> files(result.output_files().begin(),
                                              result.output_files().end());
                try {
                    auto exportResponse = d_casClient->exportFiles(
                        files, SystemUtils::getCurrentWorkingDirectory());
                    exportSuccessful = true;
                    for (const auto &resp : exportResponse.responses()) {
                        if (resp.status().code() != GRPC_STATUS_OK) {
                            exportSuccessful = false;
                        }
                    }
                }
                catch (GrpcError &e) {
                    if (e.status.error_code() ==
                        grpc::StatusCode::UNIMPLEMENTED) {
                        BUILDBOX_LOG_WARNING("ExportFiles() failed, falling "
                                             "back to CAS download: "
                                             << e.what());
                        exportSuccessful = false;
                    }
                    else {
                        throw;
                    }
                }
            }

            if (!exportSuccessful) {
                execClient->downloadOutputs(d_casClient.get(), result,
                                            AT_FDCWD, nullptr);
            }

            // If not built using the local hosttools and using
            // RECC_PREFIX_MAP the filepaths inside downloaded make
            // dependency files need to be resolved.
            if (!RECC_PREFIX_REPLACEMENT.empty() || !RECC_NO_PATH_REWRITE) {
                for (const auto &file : result.output_files()) {
                    if (makeDependencyFiles.find(file.path()) !=
                        makeDependencyFiles.end()) {
                        FileUtils::resolvePathsInsideDependencyFileForLocal(
                            file.path());
                    }
                }
            }
        }

        /* These don't use logging macros because they are compiler output
         */
        if (fetchStdout) {
            std::ifstream file(stdoutFilename);
            std::cout << file.rdbuf();
            unlink(stdoutFilename.c_str());
        }
        else {
            std::cout << result.stdout_raw();
        }
        if (fetchStderr) {
            std::ifstream file(stderrFilename);
            std::cerr << file.rdbuf();
            unlink(stderrFilename.c_str());
        }
        else {
            std::cerr << result.stderr_raw();
        }
    }
    catch (const std::exception &e) {
        BUILDBOX_LOG_ERROR(e.what());
        throw;
    }
}

int ExecutionContext::executeConfigured(int argc, char *argv[])
{
    BUILDBOX_LOG_DEBUG("RECC_REAPI_VERSION == '" << RECC_REAPI_VERSION << "'");

    std::shared_ptr<StatsDPublisherType> statsDPublisher;
    try {
        statsDPublisher = get_statsdpublisher_from_config(
            std::unordered_map<std::string, std::string>(
                RECC_METRICS_TAG.begin(), RECC_METRICS_TAG.end()));
    }
    catch (const std::runtime_error &e) {
        BUILDBOX_LOG_ERROR(
            "Could not initialize statsD publisher: " << e.what());
        throw;
    }

    buildboxcommonmetrics::PublisherGuard<StatsDPublisherType>
        statsDPublisherGuard(
            RECC_ENABLE_METRICS, *statsDPublisher,
            [](const std::exception &e) {
                BUILDBOX_LOG_WARNING(
                    "Exception thrown when publishing metrics: " << e.what());
            });

    d_addDurationMetricCallback =
        std::bind(&ExecutionContext::addDurationMetric, this,
                  std::placeholders::_1, std::placeholders::_2);
    d_recordCounterMetricCallback =
        std::bind(&ExecutionContext::recordCounterMetric, this,
                  std::placeholders::_1, std::placeholders::_2);

    const std::string cwd = FileUtils::getCurrentWorkingDirectory();
    const auto command =
        ParsedCommandFactory::createParsedCommand(argv, cwd.c_str());

    if (!RECC_INVALID_INPUT_PATHS_REGEX.empty()) {
        if (matchIgnorePaths(RECC_INVALID_INPUT_PATHS_REGEX,
                             command.get_input_files(), cwd)) {
            BUILDBOX_LOG_INFO(
                "Input path matches regex in RECC_INVALID_INPUT_PATHS_REGEX, "
                "executing locally");
            recordCounterMetric(COUNTER_NAME_INVALID_PATHS, 1);
            return execLocally(argc, argv);
        }
    }

    if (command.is_compiler_command()) {
        RECC_CACHE_ONLY |= RECC_COMPILE_CACHE_ONLY;
        // Merge RECC_REMOTE_PLATFORM and RECC_COMPILE_REMOTE_PLATFORM,
        // giving priority to RECC_COMPILE_REMOTE_PLATFORM in case of
        // conflicts.
        RECC_COMPILE_REMOTE_PLATFORM.merge(RECC_REMOTE_PLATFORM);
        RECC_REMOTE_PLATFORM.clear();
        RECC_REMOTE_PLATFORM.swap(RECC_COMPILE_REMOTE_PLATFORM);
    }
    else if (command.is_linker_command()) {
        RECC_CACHE_ONLY |= RECC_LINK_CACHE_ONLY;
        // Merge RECC_REMOTE_PLATFORM and RECC_LINK_REMOTE_PLATFORM,
        // giving priority to RECC_LINK_REMOTE_PLATFORM in case of conflicts.
        RECC_LINK_REMOTE_PLATFORM.merge(RECC_REMOTE_PLATFORM);
        RECC_REMOTE_PLATFORM.clear();
        RECC_REMOTE_PLATFORM.swap(RECC_LINK_REMOTE_PLATFORM);

        // Override instance names for linker commands if RECC_LINK_INSTANCE is
        // set
        if (RECC_LINK_INSTANCE.has_value()) {
            BUILDBOX_LOG_DEBUG("Using RECC_LINK_INSTANCE=\""
                               << RECC_LINK_INSTANCE.value()
                               << "\" for linker command");
            RECC_INSTANCE = RECC_LINK_INSTANCE.value();
            RECC_CAS_INSTANCE = RECC_LINK_INSTANCE;
            RECC_ACTION_CACHE_INSTANCE = RECC_LINK_INSTANCE;
        }
    }

    std::shared_ptr<GrpcChannels> returnChannels;
    std::shared_ptr<GrpcClient> casGrpcClient;
    setupCASClient(&returnChannels, &casGrpcClient);

    digest_string_map blobs;
    digest_string_map digest_to_filepaths;
    std::set<std::string> products;
    std::set<std::string> makeDependencyFiles =
        Deps::determineMakeDependencyFiles(command);

    std::shared_ptr<proto::Action> actionPtr =
        tryCreateAction(&blobs, &digest_to_filepaths, &products, command, cwd);

    // If we don't need to build an `Action` or if the process fails, we defer
    // to running the command locally (unless we are in no-build mode):
    if (!actionPtr) {
        if (RECC_NO_EXECUTE) {
            BUILDBOX_LOG_INFO("Command would have run locally but "
                              "RECC_NO_EXECUTE is enabled, exiting.");
            return 0;
        }
        BUILDBOX_LOG_INFO("Not a compiler command, so running locally. (Use "
                          "RECC_FORCE_REMOTE=1 to force remote execution)");
        return execLocally(argc, argv);
    }

    const proto::Action &action = *actionPtr;
    const proto::Digest actionDigest = DigestGenerator::hash(action);
    this->d_actionDigest = actionDigest;

    BUILDBOX_LOG_DEBUG("Action Digest: " << actionDigest
                                         << " Action Contents: "
                                         << action.ShortDebugString());
    if (RECC_NO_EXECUTE) {
        BUILDBOX_LOG_INFO("RECC_NO_EXECUTE is enabled, exiting.");
        return 0;
    }

    std::shared_ptr<GrpcClient> actionCacheGrpcClient;
    std::shared_ptr<ExecutionClient> execClient;

    bool local_runner = RECC_CACHE_ONLY && !RECC_RUNNER_COMMAND.empty();

    casGrpcClient->setActionId(proto::toString(actionDigest));
    setupExecClient(*returnChannels, &actionCacheGrpcClient, &execClient,
                    actionDigest, local_runner);

    bool action_in_cache = false;
    proto::ActionResult result;

    // If allowed, we look in the action cache first:
    if (!RECC_SKIP_CACHE) {
        queryActionCache(&action_in_cache, &result, execClient, actionDigest,
                         command);
    }
    else {
        recordCounterMetric(COUNTER_NAME_ACTION_CACHE_SKIP, 1);
    }

    // If the results for the action are not cached or set to ignore failure
    // results with non-zero exit code in cache, we upload the necessary
    // resources to CAS:
    if (!action_in_cache ||
        (RECC_LINK_METRICS_ONLY && command.is_linker_command()) ||
        (RECC_IGNORE_FAILURE_RESULT && result.exit_code() != 0)) {
        blobs[actionDigest] = action.SerializeAsString();

        if (RECC_CACHE_ONLY && !local_runner) {
            bool cache_upload_local_build = RECC_CACHE_UPLOAD_LOCAL_BUILD &&
                                            !RECC_ACTION_UNCACHEABLE &&
                                            !action_in_cache;
            BUILDBOX_LOG_INFO(
                "Action not cached and running in cache-only mode, "
                "executing locally");
            if (!cache_upload_local_build) {
                return execLocally(argc, argv);
            }
            else {
                return executeLocallyAndUploadActionResult(
                    argc, argv, execClient, products, makeDependencyFiles,
                    &blobs, &digest_to_filepaths);
            }
        }

        if (!local_runner) {
            BUILDBOX_LOG_INFO("Executing action remotely... [actionDigest="
                              << actionDigest << "]");
        }
        else {
            BUILDBOX_LOG_INFO(
                "Executing action in local runner... [actionDigest="
                << actionDigest << "]");
        }

        BUILDBOX_LOG_DEBUG("Uploading resources...");
        try {
            uploadResources(blobs, digest_to_filepaths,
                            TIMER_NAME_UPLOAD_MISSING_INPUT_BLOBS);
        }
        catch (const std::exception &e) {
            BUILDBOX_LOG_ERROR("Error while uploading resources to CAS at \""
                               << RECC_CAS_SERVER << "\": " << e.what());
            throw;
        }

        // And call `Execute()`:
        try {
            // Timed block
            buildboxcommonmetrics::MetricTeeGuard<
                buildboxcommonmetrics::DurationMetricTimer>
                mt(TIMER_NAME_EXECUTE_REMOTE_WITH_ACTION_RESULT,
                   d_addDurationMetricCallback);

            std::optional<JobServerClient> jobServerClient =
                RECC_USE_JOBSERVER ? JobServerClient::tryCreate()
                                   : std::nullopt;
            if (jobServerClient) {
                // Release the implicit job slot while recc is idle waiting
                // for remote execution to complete.
                // The `JobServerClient` destructor will automatically
                // re-acquire the released slot (at the end of the try block).
                jobServerClient->releaseSlot();
            }

            bool skip_cache_option =
                RECC_SKIP_CACHE ||
                (action_in_cache && RECC_IGNORE_FAILURE_RESULT &&
                 result.exit_code() != 0);
            result = execClient->executeAction(actionDigest, *d_stopRequested,
                                               skip_cache_option);
            BUILDBOX_LOG_INFO("Remote execution finished with exit code "
                              << result.exit_code());
            if (RECC_VERBOSE) {
                logActionResult(result);
            }
        }
        catch (const buildboxcommon::GrpcError &e) {
            if (e.status.error_code() == grpc::StatusCode::CANCELLED) {
                BUILDBOX_LOG_INFO("Execute() cancelled: " << e.what());
            }
            if (local_runner) {
                BUILDBOX_LOG_ERROR(
                    "Failed to execute action in local runner: " << e.what());
            }
            else {
                BUILDBOX_LOG_ERROR("Error while calling `Execute()` on \""
                                   << RECC_SERVER << "\": " << e.what());
            }
            throw;
        }
    }

    // Store action result for access by the caller of this method.
    this->d_actionResult = result;

    serveOutputFromActionResult(execClient, result, products,
                                makeDependencyFiles);

    return result.exit_code();
}

const std::map<std::string, buildboxcommonmetrics::DurationMetricValue> *
ExecutionContext::getDurationMetrics() const
{
    return &d_durationMetrics;
}

void ExecutionContext::addDurationMetric(
    const std::string &name, buildboxcommonmetrics::DurationMetricValue value)
{
    d_durationMetrics[name] = value;
}

const std::map<std::string, int64_t> *
ExecutionContext::getCounterMetrics() const
{
    return &d_counterMetrics;
}

void ExecutionContext::recordCounterMetric(const std::string &name,
                                           int64_t value)
{
    buildboxcommonmetrics::CountingMetricUtil::recordCounterMetric(name,
                                                                   value);
    d_counterMetrics[name] = value;
}

const Digest &ExecutionContext::getActionDigest() const
{
    return d_actionDigest;
}

const ActionResult &ExecutionContext::getActionResult() const
{
    return d_actionResult;
}

CASClient *ExecutionContext::getCasClient() const { return d_casClient.get(); }

} // namespace recc
