/*
 * Copyright 2018 Bloomberg Finance LP
 *
 * 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 <buildboxcommon_grpcerror.h>
#include <buildboxcommon_runner.h>

#include <buildboxcommon_connectionoptions.h>
#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_executionstatsutils.h>
#include <buildboxcommon_fallbackstageddirectory.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_localcasstageddirectory.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_protos.h>
#include <buildboxcommon_remoteexecutionclient.h>
#include <buildboxcommon_stringutils.h>
#include <buildboxcommon_systemutils.h>
#include <buildboxcommon_temporaryfile.h>
#include <buildboxcommon_timeutils.h>
#include <buildboxcommon_version.h>

#include <google/protobuf/util/time_util.h>

#include <algorithm>
#include <cerrno>
#include <chrono>
#include <climits>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <fcntl.h>
#include <filesystem>
#include <functional>
#include <iostream>
#include <span>
#include <string>
#include <sys/stat.h>
#include <system_error>
#include <unistd.h>

#ifdef __linux__
#include <sys/prctl.h>
#endif

namespace buildboxcommon {

static const int BUILDBOXCOMMON_RUNNER_USAGE_PAD_WIDTH = 32;

namespace {
void usage(const char *name)
{
    std::clog << "\nusage: " << name << " [OPTIONS]\n";
    std::clog
        << "    --help                      Print this message and exit\n";
    std::clog << "    --version                   Print version information "
                 "and exit\n";
    std::clog
        << "    --action=PATH               Path to read input Action from\n";
    std::clog << "    --action-result=PATH        Path to write output "
                 "ActionResult to\n";
    std::clog << "    --log-level=LEVEL           (default: info) Log "
                 "verbosity: "
              << buildboxcommon::logging::stringifyLogLevels() << "\n";
    std::clog << "    --verbose                   Set log level to debug\n";
    std::clog << "    --digest-function           Set digest function\n";

    std::clog << "    --log-directory=DIR         Write logs to this "
                 "directory with filenames:\n"
                 "                                "
              << name
              << ".<hostname>.<user name>.log.<severity "
                 "level>.<date>.<time>.<pid>\n";
    std::clog << "    --disable-localcas          Do not use LocalCAS "
                 "protocol methods\n";
    std::clog << "    --workspace-path=PATH       Location on disk which "
                 "runner will use as root when executing jobs\n";
    std::clog
        << "    --stdout-file=FILE          File to redirect the command's "
           "stdout to\n";
    std::clog
        << "    --stderr-file=FILE          File to redirect the command's "
           "stderr to\n";
    std::clog
        << "    --no-logs-capture           Do not capture and upload the "
           "contents written to stdout and stderr\n";
    std::clog
        << "    --collect-execution-stats   Gather "
           "`execution_stats.proto` metrics and "
           "attach the digest of the message\n"
           "                                "
           "in CAS to `ActionResult.execution_metadata.auxiliary_metadata`\n";
    std::clog << "    --validate-parameters       Only check whether all the "
                 "required parameters are being passed and that no\n"
                 "                                unknown options are given. "
                 "Exits with a status code containing "
                 "the result (0 if successful).\n";
    std::clog << "    --post-action-command=COMMAND       Command to run "
                 "after the action is done. It can take one parameter which "
                 "is the path to the staged directory. These commands are "
                 "blocking if --disable-localcas is set.\n";
    std::clog << "    --output-bypass-local-cache        Skip caching output "
                 "blobs in the LocalCAS storage when using the LocalCAS API. "
                 "This has no effect if `--disable-localcas` is set.\n";
    std::clog
        << "    --staging-mode=MODE        Specify how to stage "
           "directories and files when handling StageTreeRequest. "
           "Valid values are: [fuse, copy-or-link, default], where `default` "
           "would favor `fuse` if it is available.\n";
    std::clog
        << "    --graceful-shutdown-seconds=SECONDS  How long to wait after "
           "sending a SIGTERM to the executed command due to a timeout "
           "or runner shutdown. By default a SIGKILL is sent immediately\n";
    ConnectionOptions::printArgHelp(BUILDBOXCOMMON_RUNNER_USAGE_PAD_WIDTH);
    ConnectionOptions::printArgHelp(BUILDBOXCOMMON_RUNNER_USAGE_PAD_WIDTH,
                                    "Asset", "ra-");
    std::clog << "    --nested-instance=NAME      Instance to use for nested "
                 "remote execution\n";
    std::clog << "    --nested-ac-enable-update   Allow action cache updates "
                 "for nested remote execution\n";
}

class Runner_StandardOutputFile final {
  public:
    /* If the specified path is empty, generate a temporary file and keep it
     * until this object goes out of scope. Otherwise just keep track of the
     * path (without any side effects on the filesystem).
     */
    explicit Runner_StandardOutputFile(const std::string &path)
    {
        if (path.empty()) {
            d_tmpFile = std::make_unique<buildboxcommon::TemporaryFile>();
            d_tmpFile->close();

            d_path = d_tmpFile->strname();
        }
        else {
            d_path = path;
        }
    }

    ~Runner_StandardOutputFile() {}

    // Delete move constructor
    Runner_StandardOutputFile(Runner_StandardOutputFile &&other) = delete;

    // Delete move assignment operator
    Runner_StandardOutputFile &
    operator=(Runner_StandardOutputFile &&) = delete;

    // Delete copy constructor
    Runner_StandardOutputFile(const Runner_StandardOutputFile &other) = delete;

    // Delete copy assignment operator
    Runner_StandardOutputFile &
    operator=(const Runner_StandardOutputFile &) = delete;

    const std::string &name() const { return d_path; }

  private:
    std::string d_path;
    std::unique_ptr<buildboxcommon::TemporaryFile> d_tmpFile;
};
} // namespace

volatile sig_atomic_t Runner::d_signal_status = 0;
void Runner::handleSignal(int signal) { d_signal_status = signal; }
sig_atomic_t Runner::getSignalStatus() { return d_signal_status; }

void Runner::recursively_chmod_directories(const char *path, mode_t mode)
{
    {
        DirentWrapper root(path);

        bool encountered_permission_errors = false;

        FileUtils::DirectoryTraversalFnPtr chmod_func =
            [&mode, &encountered_permission_errors](
                const char *dir_path = nullptr, int fd = 0) {
                if (fchmod(fd, mode) == -1) {
                    const int chmod_error = errno;
                    if (chmod_error == EPERM) {
                        // Logging every instance of this error might prove too
                        // noisy when staging using chroots. We aggregate them
                        // into a single warning message.
                        encountered_permission_errors = true;
                    }
                    else {
                        BUILDBOX_LOG_WARNING("Unable to chmod dir: "
                                             << dir_path << " errno: "
                                             << strerror(chmod_error));
                    }
                };
            };

        FileUtils::fileDescriptorTraverseAndApply(&root, chmod_func, nullptr,
                                                  true);

        if (encountered_permission_errors) {
            BUILDBOX_LOG_WARNING("Failed to `chmod()` some directories in \""
                                 << path
                                 << "\" due to permission issues (`EPERM`).");
        }
    }
}

void Runner::registerSignals() const
{
    // Handle SIGINT, SIGTERM and SIGALRM
    struct sigaction sa{};
    sa.sa_handler = handleSignal;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGINT, &sa, nullptr) == -1) {
        BUILDBOX_RUNNER_LOG(ERROR,
                            "Unable to register signal handler for SIGINT");
        exit(1);
    }
    if (sigaction(SIGTERM, &sa, nullptr) == -1) {
        BUILDBOX_RUNNER_LOG(ERROR,
                            "Unable to register signal handler for SIGTERM");
        exit(1);
    }
    if (sigaction(SIGALRM, &sa, nullptr) == -1) {
        BUILDBOX_RUNNER_LOG(ERROR,
                            "Unable to register signal handler for SIGALRM");
        exit(1);
    }

    // Ignore SIGPIPE in case of using sockets + grpc without MSG_NOSIGNAL
    // support configured
    sa.sa_handler = SIG_IGN;
    sa.sa_flags = 0;
    if (sigaction(SIGPIPE, &sa, nullptr) == -1) {
        BUILDBOX_RUNNER_LOG(ERROR, "Unable to ignore SIGPIPE");
        exit(1);
    }
}

void Runner::maskSignals(int how) const
{
    sigset_t signalMask;
    sigemptyset(&signalMask);
    sigaddset(&signalMask, SIGINT);
    sigaddset(&signalMask, SIGTERM);
    sigaddset(&signalMask, SIGALRM);
    if (pthread_sigmask(how, &signalMask, NULL) != 0) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "pthread_sigmask() failed");
    }
}

void Runner::resetSignals() const
{
    // Reset SIGINT, SIGTERM and SIGALRM to the default
    struct sigaction sa{};
    sa.sa_handler = SIG_DFL;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGINT, &sa, nullptr) == -1) {
        BUILDBOX_RUNNER_LOG(ERROR,
                            "Unable to reset signal handler for SIGINT");
        exit(1);
    }
    if (sigaction(SIGTERM, &sa, nullptr) == -1) {
        BUILDBOX_RUNNER_LOG(ERROR,
                            "Unable to reset signal handler for SIGTERM");
        exit(1);
    }
    if (sigaction(SIGALRM, &sa, nullptr) == -1) {
        BUILDBOX_RUNNER_LOG(ERROR,
                            "Unable to reset signal handler for SIGALRM");
        exit(1);
    }

    // Clear handled signal
    d_signal_status = 0;
}

Action Runner::readAction(const std::string &path) const
{
    try {
        return ProtoUtils::readProtobufFromFile<Action>(path);
    }
    catch (const std::runtime_error &e) {
        std::ostringstream errorMessage;
        errorMessage << "Failed to read Action from [" << path
                     << "]: " << e.what();

        const auto errorMessageStr = errorMessage.str();
        BUILDBOX_RUNNER_LOG(ERROR, errorMessageStr);

        writeErrorStatusFile(grpc::StatusCode::INTERNAL, errorMessageStr);
    }

    exit(1);
}

void Runner::initializeCasClient(const std::string &name,
                                 const std::string &actionId,
                                 const std::string &toolInvocationId,
                                 const std::string &correlatedId)
{
    BUILDBOX_RUNNER_LOG(DEBUG, "Initializing CAS client to connect to: \""
                                   << this->d_casRemote.d_url << "\"");
    try {
        auto grpcClient = std::make_shared<GrpcClient>();
        grpcClient->init(this->d_casRemote);
        grpcClient->setToolDetails(name, buildboxcommon::VERSION);
        grpcClient->setRequestMetadata(actionId, toolInvocationId,
                                       correlatedId);
        this->d_casClient = std::make_shared<CASClient>(grpcClient);
        this->d_casClient->init();
    }
    catch (const std::runtime_error &e) {
        BUILDBOX_RUNNER_LOG(ERROR,
                            "Error initializing CAS client: " << e.what());
        exit(1);
    }
}

void Runner::initializeAssetClient(const std::string &name,
                                   const std::string &actionId,
                                   const std::string &toolInvocationId,
                                   const std::string &correlatedId)
{
    if (!d_assetRemote) {
        return;
    }

    BUILDBOX_RUNNER_LOG(DEBUG, "Initializing Asset client to connect to: \""
                                   << this->d_assetRemote->d_url << "\"");
    try {
        auto grpcClient = std::make_shared<GrpcClient>();
        grpcClient->init(*this->d_assetRemote);
        grpcClient->setToolDetails(name, buildboxcommon::VERSION);
        grpcClient->setRequestMetadata(actionId, toolInvocationId,
                                       correlatedId);
        this->d_assetClient = std::make_shared<AssetClient>(grpcClient);
        this->d_assetClient->init();
    }
    catch (const std::runtime_error &e) {
        BUILDBOX_RUNNER_LOG(ERROR,
                            "Error initializing Asset client: " << e.what());
        exit(1);
    }
}

void Runner::writeActionResult(const ActionResult &action_result,
                               const std::string &path) const
{
    try {
        ProtoUtils::writeProtobufToFile(action_result, path);
    }
    catch (const std::runtime_error &e) {
        BUILDBOX_RUNNER_LOG(ERROR,
                            "Could not save ActionResult: " << e.what());
        exit(1);
    }
}

void Runner::writeErrorStatusFile(const google::protobuf::int32 errorCode,
                                  const std::string &errorMessage) const
{
    if (!d_outputPath.empty()) {
        google::rpc::Status status;
        status.set_code(errorCode);
        status.set_message(errorMessage);

        writeStatusFile(status, errorStatusCodeFilePath(d_outputPath));
    }
}

std::string
Runner::errorStatusCodeFilePath(const std::string &actionResultPath)
{
    if (actionResultPath.empty()) {
        return "";
    }
    return actionResultPath + ".error-status";
}

void Runner::writeStatusFile(const google::rpc::Status &status,
                             const std::string &path) const
{
    try {
        ProtoUtils::writeProtobufToFile(status, path);
    }
    catch (const std::runtime_error &e) {
        BUILDBOX_RUNNER_LOG(ERROR,
                            "Could not save Status proto file: " << e.what());
    }
}

bool Runner::readStatusFile(const std::string &path,
                            google::rpc::Status *status)
{
    try {
        *status = buildboxcommon::ProtoUtils::readProtobufFromFile<
            google::rpc::Status>(path);
        return true;
    }
    catch (const std::runtime_error &) {
        // `readProtobufFromFile()` logged the error
        return false;
    }
}

Command Runner::fetchCommand(const Digest &command_digest) const
{
    google::rpc::Status errorStatus;
    try {
        return this->d_casClient->fetchMessage<Command>(command_digest);
    }
    catch (const GrpcError &e) {
        errorStatus.set_code(e.status.error_code());
        errorStatus.set_message(e.status.error_message());
    }
    catch (const std::runtime_error &e) {
        errorStatus.set_code(grpc::StatusCode::INTERNAL);
        errorStatus.set_message(e.what());
    }

    std::ostringstream errorMessage;
    errorMessage << "Error fetching Command with digest \"" << command_digest
                 << "\" from \"" << d_casRemote.d_url
                 << "\": " << errorStatus.message();

    const std::string errorMessageStr = errorMessage.str();
    BUILDBOX_RUNNER_LOG(ERROR, errorMessageStr);

    writeErrorStatusFile(errorStatus.code(), errorMessageStr);
    exit(1);
}

int Runner::main(int argc, char *argv[])
{
    try {
        auto args = std::span(argv, argc);
        std::string programName(args[0]);
        // Binary name without path components
        programName = programName.substr(
            std::max<size_t>(0, programName.find_last_of('/') + 1));
        if (!this->parseArguments(argc, argv)) {
            usage(programName.c_str());
            printSpecialUsage();
            return 1;
        }
        else if (this->d_validateParametersAndExit) {
            std::cerr << "Asked to only validate the CLI parameters "
                         "(--validate-parameters) and the check "
                         "succeeded: exiting 0. \n";
            return 0;
        }

        logging::Logger::getLoggerInstance().initialize(args[0]);
        // (`parseArguments()` already set the destination of logs.)

        // now set the logging verbosity level after the init
        BUILDBOX_LOG_SET_LEVEL(this->d_logLevel);

        // -- Worker started --
        const auto worker_start_time = TimeUtils::now();

        const Action input = readAction(this->d_inputPath);
        this->d_action_digest = DigestGenerator::hash(input);
        if (input.has_timeout()) {
            this->d_timeout = std::chrono::seconds(
                google::protobuf::util::TimeUtil::DurationToSeconds(
                    input.timeout()));
        }

        registerSignals();

        // Block signals for gRPC initialization and operations which may spawn
        // background threads.
        maskSignals(SIG_BLOCK);

        const std::string toolInvocationId = StringUtils::getUUIDString();
        const std::string correlatedInvocationId =
            StringUtils::getUUIDString();
        const std::string action_id = toString(this->d_action_digest);
        initializeCasClient(programName, action_id, toolInvocationId,
                            correlatedInvocationId);
        initializeAssetClient(programName, action_id, toolInvocationId,
                              correlatedInvocationId);

        BUILDBOX_RUNNER_LOG(DEBUG,
                            "Fetching Command " << input.command_digest());
        const Command command = fetchCommand(input.command_digest());

        // REAPI v2.2: Servers SHOULD prefer platform properties set in
        // `Action` message.
        const Platform platform = input.has_platform()
                                      ? input.platform()
                                      : commandPlatformDeprecated(command);

        maskSignals(SIG_UNBLOCK);
        auto signal_status = getSignalStatus();
        maskSignals(SIG_BLOCK);
        if (signal_status) {
            // If signal is set here, then no clean up necessary, write an
            // error status file and return.
            writeErrorStatusFile(grpc::StatusCode::UNAVAILABLE,
                                 "Runner execution interrupted by signal");
            const int SIGNAL_OFFSET = 128;
            return SIGNAL_OFFSET + signal_status;
        }

        for (const auto &property : platform.properties()) {
            if (property.name() == "remoteApisSocketPath") {
                d_remoteApisSocketPath = property.value();
            }
        }

        ActionResult result;
        try {
            BUILDBOX_RUNNER_LOG(DEBUG, "Executing command");
            result =
                this->execute(command, input.input_root_digest(), platform);
        }
        catch (const GrpcError &e) {
            BUILDBOX_RUNNER_LOG(ERROR,
                                "Error executing command: " << e.what());
            writeErrorStatusFile(e.status.error_code(),
                                 e.status.error_message());
            return EXIT_FAILURE;
        }
        catch (const std::exception &e) {
            BUILDBOX_RUNNER_LOG(ERROR,
                                "Error executing command: " << e.what());

            writeErrorStatusFile(grpc::StatusCode::INTERNAL,
                                 "execute() threw: " + std::string(e.what()));

            return EXIT_FAILURE;
        }
        //  -- Worker finished, set start/completed timestamps --
        auto *result_metadata = result.mutable_execution_metadata();
        result_metadata->mutable_worker_completed_timestamp()->CopyFrom(
            TimeUtils::now());
        result_metadata->mutable_worker_start_timestamp()->CopyFrom(
            worker_start_time);

        if (!this->d_outputPath.empty()) {
            writeActionResult(result, this->d_outputPath);
        }

        signal_status = getSignalStatus();
        if (signal_status) {
            // Error status file is written by `executeAndStore()`.
            const int SIGNAL_OFFSET = 128;
            return SIGNAL_OFFSET + signal_status;
        }

        google::rpc::Status status;
        if (!d_outputPath.empty() &&
            (readStatusFile(errorStatusCodeFilePath(d_outputPath), &status) &&
             status.code() != grpc::OK)) {
            return EXIT_FAILURE;
        }

        return 0;
    }
    catch (const std::exception &exception) {
        // This catch should not be relied on and is here as a backstop.
        BUILDBOX_LOG_ERROR("Caught exception in main " << exception.what());
        return 1;
    }
}

std::unique_ptr<StagedDirectory> Runner::stageDirectory(
    const Digest &digest,
    const ProcessCredentialsGetter &processCredentialsGetter)
{
    try {
        auto stagedDirectory = std::unique_ptr<StagedDirectory>();
        if (d_use_localcas_protocol) {
            std::optional<RemoteApisSocketConfig> remoteApisSocket;
            if (!d_remoteApisSocketPath.empty()) {
                remoteApisSocket = RemoteApisSocketConfig();
                remoteApisSocket->set_instance_name(
                    d_nestedInstanceName ? *d_nestedInstanceName
                                         : d_casRemote.d_instanceName);
                remoteApisSocket->set_path(d_remoteApisSocketPath);
                remoteApisSocket->set_action_cache_update_enabled(
                    d_nestedActionCacheUpdateEnabled);
            }
            stagedDirectory = std::make_unique<LocalCasStagedDirectory>(
                digest, d_stage_path, this->d_casClient, d_stagingMode,
                processCredentialsGetter, d_postActionCommands,
                d_outputBypassLocalCache, remoteApisSocket);
        }
        else {
            if (!d_remoteApisSocketPath.empty()) {
                GrpcError::throwGrpcError(
                    grpc::Status(grpc::UNIMPLEMENTED,
                                 "remoteApisSocketPath is not supported "
                                 "without the LocalCAS protocol"));
            }

            // TODO: support post action commands in FallBackStagedDirectory
            stagedDirectory = std::make_unique<FallbackStagedDirectory>(
                digest, d_stage_path, this->d_casClient, d_postActionCommands);
        }
        this->d_stage_path = stagedDirectory->getPath();
        return stagedDirectory;
    }
    catch (const std::exception &e) {
        const auto staging_mechanism = d_use_localcas_protocol
                                           ? "LocalCasStagedDirectory"
                                           : "FallbackStagedDirectory";
        BUILDBOX_RUNNER_LOG(DEBUG, "Could not stage directory with digest \""
                                       << digest << "\" using `"
                                       << staging_mechanism
                                       << "`: " << e.what());
        throw;
    }
}

void Runner::createOutputDirectories(const Command &command,
                                     const std::string &workingDir) const
{
    auto createDirectoryIfNeeded = [&](const std::string &output) {
        if (output.find('/') != std::string::npos) {
            std::string directory_location =
                workingDir + "/" + output.substr(0, output.rfind('/'));
            try {
                FileUtils::createDirectory(directory_location.c_str());
            }
            catch (const std::system_error &e) {
                BUILDBOX_RUNNER_LOG(ERROR, "Error while creating directory "
                                               << directory_location << " : "
                                               << e.what());
                throw;
            }
            BUILDBOX_RUNNER_LOG(DEBUG, "Created parent output directory: "
                                           << directory_location);
        }
    };

    // The runner is still required to create the directories leading up to
    // the output paths
    std::for_each(command.output_paths().cbegin(),
                  command.output_paths().cend(), createDirectoryIfNeeded);
}

Digest Runner::collectExecutionStatsAndUploadIfNeeded()
{
    const build::buildbox::ExecutionStatistics executionStats =
        ExecutionStatsUtils::getChildrenProcessRusage();

    // Packing the message inside an `Any` proto so that type information is
    // attached:
    google::protobuf::Any anyWrapper;
    anyWrapper.PackFrom(executionStats);

    if (d_casRemote.d_url.empty()) {
        // To allow unit testing, skip the upload step if a CAS remote is not
        // specified.
        return DigestGenerator::hash(anyWrapper);
    }

    try {
        return d_casClient->uploadMessage(anyWrapper);
    }
    catch (const std::exception &) {
        BUILDBOX_LOG_WARNING("Failed to upload `ExecutionStatistics` message");
        return {};
    }
}

void Runner::executeAndStore(
    const std::vector<std::string> &command,
    const UploadOutputsCallback &uploadOutputsFunction, ActionResult *result,
    const std::string &workdir)
{
    std::unique_ptr<Runner_StandardOutputFile> stdoutFile, stderrFile;
    if (!d_standardOutputsCaptureConfig.skip_capture) {
        stdoutFile = std::make_unique<Runner_StandardOutputFile>(
            d_standardOutputsCaptureConfig.stdout_file_path);
        stderrFile = std::make_unique<Runner_StandardOutputFile>(
            d_standardOutputsCaptureConfig.stderr_file_path);
    }
    else {
        BUILDBOX_RUNNER_LOG(
            TRACE,
            "Will skip the capturing and uploading of stdout and stderr.");
    }

    BUILDBOX_RUNNER_LOG(
        DEBUG, "Executing command: "
                   << buildboxcommon::logging::printableCommandLine(command));

    auto *result_metadata = result->mutable_execution_metadata();

    // -- Execution started --
    result_metadata->mutable_execution_start_timestamp()->CopyFrom(
        TimeUtils::now());
    updatePartialExecutionMetadataFile(result_metadata);

    if (this->d_timeout.count() > 0 && this->d_timeout.count() <= UINT_MAX) {
        // Set alarm for execution timeout
        alarm(static_cast<unsigned int>(this->d_timeout.count()));
    }

    // Unblock signals before fork() to prevent unusual initial signal mask
    // in child process.
    maskSignals(SIG_UNBLOCK);

    // Fork and exec
    const auto pid = fork();
    if (pid == -1) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category, "Error in fork()");
    }
    else if (pid == 0) {
#ifdef __linux__
        // Kill action command if runner dies unexpectedly.
        if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0) {
            BUILDBOX_LOG_WARNING("prctl(PR_SET_PDEATHSIG, SIGKILL) failed: "
                                 << strerror(errno));
        }
#endif

        try {
            if (chdir(workdir.c_str()) != 0) {
                BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                    std::system_error, errno, std::system_category,
                    "Unable to chdir into working directory " << workdir);
            }

            if (stdoutFile) {
                SystemUtils::redirectStandardOutputToFile(STDOUT_FILENO,
                                                          stdoutFile->name());
            }
            if (stderrFile) {
                SystemUtils::redirectStandardOutputToFile(STDERR_FILENO,
                                                          stderrFile->name());
            }

            const int rc = buildboxcommon::SystemUtils::executeCommand(
                command, true, commandEnvironment());
            // executeCommand only returns if the actual exec fails, so write a
            // status file and exit
            const std::string errorMsg =
                "Unable to execute [" + command[0] + "]: " + strerror(errno);
            const int EXECUTION_ERROR_CODE = 127;
            if (rc == EXECUTION_ERROR_CODE) {
                writeErrorStatusFile(grpc::StatusCode::NOT_FOUND, errorMsg);
            }
            else {
                writeErrorStatusFile(grpc::StatusCode::INVALID_ARGUMENT,
                                     errorMsg);
            }
            _Exit(rc);
        }
        catch (const std::exception &e) {
            // The child process must not execute code outside this else block.
            // POSIX defines exit code 127 for errors preventing execution
            // of the child process.
            BUILDBOX_LOG_ERROR("Failed to execute command: " << e.what());
            writeErrorStatusFile(grpc::StatusCode::INVALID_ARGUMENT, e.what());
            const int EXIT_FAILURE_CODE = 127;
            _Exit(EXIT_FAILURE_CODE);
        }
    }

    int exit_code = -1;
    bool runnerSignaled = false;
    while (exit_code < 0) {
        const auto signalStatus = getSignalStatus();
        if (signalStatus) {
            if (signalStatus == SIGALRM) {
                const std::string msg = "Action timeout reached";
                BUILDBOX_RUNNER_LOG(INFO, msg);
                writeErrorStatusFile(grpc::StatusCode::DEADLINE_EXCEEDED, msg);
            }
            else {
                // We've received SIGINT or SIGTERM before execution completed.
                BUILDBOX_RUNNER_LOG(INFO, "Caught signal");
                writeErrorStatusFile(grpc::StatusCode::UNAVAILABLE,
                                     "Runner execution interrupted by signal");
                runnerSignaled = true;
            }
            // Send SIGTERM to process and give it some time to gracefully exit
            if (d_graceful_shutdown_seconds.has_value()) {
                kill(pid, SIGTERM);
                bool timedOut = false;
                exit_code = SystemUtils::waitPidOrSignalWithTimeout(
                    pid, d_graceful_shutdown_seconds, &timedOut);
                // If the process didn't respond in time or we recieved another
                // signal, kill the process
                if (timedOut or exit_code == -EINTR) {
                    BUILDBOX_RUNNER_LOG(
                        INFO,
                        "Process did not exit after SIGTERM, sending SIGKILL");
                    kill(pid, SIGKILL);
                }
                else {
                    break;
                }
            }
            else {
                // No grace period: send SIGKILL immediately
                kill(pid, SIGKILL);
            }
        }
        exit_code = SystemUtils::waitPidOrSignal(pid);
    }

    // Block signals again before gRPC operations
    maskSignals(SIG_BLOCK);

    // -- Execution ended --
    result_metadata->mutable_execution_completed_timestamp()->CopyFrom(
        TimeUtils::now());
    updatePartialExecutionMetadataFile(result_metadata);
    if (this->d_timeout.count() > 0) {
        // Cancel timeout alarm if set
        alarm(0);
    }

    result->set_exit_code(exit_code);

    google::rpc::Status status;
    if (runnerSignaled ||
        (!d_outputPath.empty() &&
         readStatusFile(this->errorStatusCodeFilePath(d_outputPath),
                        &status) &&
         status.code() != grpc::DEADLINE_EXCEEDED)) {
        // Don't upload anything if execution was aborted for
        // a reason which wasn't a timeout. For timeouts it's useful
        // to upload stdout/stderr
        return;
    }

    if (!d_standardOutputsCaptureConfig.skip_capture) {
        // Uploading standard outputs:
        Digest stdoutDigest, stderrDigest;
        grpc::Status uploadStatus =
            uploadOutputsFunction(stdoutFile->name(), stderrFile->name(),
                                  &stdoutDigest, &stderrDigest);
        if (!uploadStatus.ok()) {
            if (!runnerSignaled) {
                writeErrorStatusFile(uploadStatus.error_code(),
                                     "Uploading stdout and stderr failed: " +
                                         uploadStatus.error_message());
            }
            BUILDBOX_RUNNER_LOG(ERROR,
                                "Uploading stdout and stderr failed with "
                                    << uploadStatus.error_code() << ": "
                                    << uploadStatus.error_message());
            return;
        }

        if (!stdoutDigest.hash().empty()) {
            result->mutable_stdout_digest()->CopyFrom(stdoutDigest);
        }
        if (!stderrDigest.hash().empty()) {
            result->mutable_stderr_digest()->CopyFrom(stderrDigest);
        }
    }

    const auto signalStatus = getSignalStatus();
    if (signalStatus) {
        // If we got a signal, we don't upload execution stats
        BUILDBOX_RUNNER_LOG(
            INFO, "Runner execution interrupted by signal: " << signalStatus);
        return;
    }

    if (this->d_collect_execution_stats) { // rusage metadata
        const Digest executionStatsDigest =
            collectExecutionStatsAndUploadIfNeeded();
        if (executionStatsDigest.size_bytes() > 0) {
            result_metadata->add_auxiliary_metadata()->PackFrom(
                executionStatsDigest);
        }
    }
}

void Runner::executeAndStore(const std::vector<std::string> &command,
                             ActionResult *result, const std::string &workdir)
{
    UploadOutputsCallback outputs_upload_function;
    if (!d_standardOutputsCaptureConfig.skip_capture) {
        // This callback will be used to upload the contents of stdout and
        // stderr.
        outputs_upload_function =
            std::bind(&Runner::uploadOutputs, this, std::placeholders::_1,
                      std::placeholders::_2, std::placeholders::_3,
                      std::placeholders::_4);
    }

    this->executeAndStore(command, outputs_upload_function, result, workdir);
}

bool Runner::parseArguments(int argc, char *argv[])
{
    // The logger instance is not yet initialized at this point, write messages
    // to std::cout/std::cerr.
    auto args = std::span(argv, argc);
    const char *command = args[0];

    ConnectionOptions assetConnectionOptions;

    DigestFunction_Value digestFunction =
        static_cast<buildboxcommon::DigestFunction_Value>(
            BUILDBOXCOMMON_DIGEST_FUNCTION_VALUE);

    for (std::string_view arg : args.subspan(1)) {
        if (this->parseArg(arg)) {
            // Argument was handled by a subclass's parseArg method.
            continue;
        }

        if (this->d_casRemote.parseArg(arg)) {
            // Argument was handled by ConnectionOptions.
            continue;
        }

        if (assetConnectionOptions.parseArg(arg, "ra-")) {
            // Argument was handled by ConnectionOptions.
            continue;
        }

        if (arg[0] == '-' && arg[1] == '-') {
            arg.remove_prefix(2);
            auto equalPos = arg.find('=');
            if (equalPos != std::string_view::npos) {
                const std::string key(arg.substr(0, equalPos));
                const std::string value(arg.substr(equalPos + 1));
                if (key == "action") {
                    this->d_inputPath = std::string(value);
                }
                else if (key == "action-result") {
                    this->d_outputPath = std::string(value);
                }
                else if (key == "workspace-path") {
                    this->d_stage_path = std::string(value);
                }
                else if (key == "log-level") {
                    const auto &validLogLevels(logging::stringToLogLevelMap());

                    std::string level(value);
                    std::transform(level.begin(), level.end(), level.begin(),
                                   ::tolower);
                    if (validLogLevels.count(level) == 0) {
                        std::cerr << "Invalid log level.\n";
                        return false;
                    }
                    // save the value, then set log level after logger is
                    // initialized
                    this->d_logLevel = validLogLevels.at(level);
                }
                else if (key == "log-file") {
                    std::cerr << "Option --log-file is no longer supported. "
                                 "To redirect logs to files, use "
                                 "--log-directory=DIR.\n";
                    return false;
                }
                else if (key == "log-directory") {
                    if (!FileUtils::isDirectory(value.c_str())) {
                        std::cerr << "--log-directory: directory ["
                                  << std::string(value)
                                  << "] does not exist\n";
                        return false;
                    }

                    auto &logger = logging::Logger::getLoggerInstance();
                    logger.setOutputDirectory(value.c_str());
                }
                else if (key == "stdout-file") {
                    this->d_standardOutputsCaptureConfig.stdout_file_path =
                        std::string(value);
                }
                else if (key == "stderr-file") {
                    this->d_standardOutputsCaptureConfig.stderr_file_path =
                        std::string(value);
                }
                else if (key == "digest-function") {
                    digestFunction = buildboxcommon::DigestGenerator::
                        stringToDigestFunction(value);
                }
                else if (key == "partial-execution-metadata-file") {
                    this->d_partialExecutionMetadataFilePath =
                        std::string(value);
                }
                else if (key == "post-action-command") {
                    std::error_code ec;
                    const auto commandAbsolutePath =
                        std::filesystem::absolute(value, ec);
                    if (ec.value() != 0) {
                        std::cerr << "Invalid --post-action-command option: "
                                  << value;
                        return false;
                    }
                    this->d_postActionCommands.emplace_back(
                        commandAbsolutePath);
                }
                else if (key == "staging-mode") {
                    if (value == "fuse") {
                        this->d_stagingMode =
                            StageTreeRequest_StagingMode_FUSE;
                    }
                    else if (value == "copy-or-link") {
                        this->d_stagingMode =
                            StageTreeRequest_StagingMode_COPY_OR_LINK;
                    }
                    else if (value == "default") {
                        this->d_stagingMode =
                            StageTreeRequest_StagingMode_DEFAULT;
                    }
                    else {
                        std::cerr << "Invalid staging mode: " << value
                                  << ", valid options are [fuse, "
                                     "copy-or-link, default]\n";
                        return false;
                    }
                }
                else if (key == "graceful-shutdown-seconds") {
                    try {
                        int seconds = std::stoi(value);
                        if (seconds < 0)
                            throw std::invalid_argument("negative");
                        this->d_graceful_shutdown_seconds =
                            std::chrono::seconds(seconds);
                    }
                    catch (const std::exception &e) {
                        std::cerr << "Invalid value for "
                                     "--graceful-shutdown-seconds: "
                                  << value << '\n';
                        return false;
                    }
                }
                else if (key == "nested-instance") {
                    d_nestedInstanceName = value;
                }
                else {
                    std::cerr << "Invalid option " << arg << '\n';
                    return false;
                }
            }
            else {
                if (arg == "help") {
                    usage(command);
                    printSpecialUsage();
                    exit(0);
                }
                if (arg == "version") {
                    std::cout << command << " " << buildboxcommon::VERSION
                              << '\n';
                    exit(0);
                }
                else if (arg == "disable-localcas") {
                    this->d_use_localcas_protocol = false;
                }
                else if (arg == "output-bypass-local-cache") {
                    this->d_outputBypassLocalCache = true;
                }
                else if (arg == "no-logs-capture") {
                    this->d_standardOutputsCaptureConfig.skip_capture = true;
                }
                else if (arg == "collect-execution-stats") {
                    this->d_collect_execution_stats = true;
                }
                else if (arg == "verbose") {
                    BUILDBOX_LOG_SET_LEVEL(LogLevel::DEBUG);
                }
                else if (arg == "capabilities") {
                    // Generic capabilities
                    std::cout << "no-logs-capture\n";

                    printSpecialCapabilities();
                    exit(0);
                }
                else if (arg == "validate-parameters") {
                    this->d_validateParametersAndExit = true;
                }
                else if (arg == "nested-ac-enable-update") {
                    d_nestedActionCacheUpdateEnabled = true;
                }
                else {
                    std::cerr << "Invalid option " << arg << '\n';
                    return false;
                }
            }
            continue;
        }

        std::cerr << "Unexpected argument " << arg << '\n';
        return false;
    }

    DigestGenerator::init(digestFunction);

    if (!assetConnectionOptions.d_url.empty()) {
        d_assetRemote =
            std::make_unique<ConnectionOptions>(assetConnectionOptions);
    }

    if (this->d_casRemote.d_url.empty()) {
        std::cerr << "CAS server URL is missing.\n";
        return false;
    }
    return true;
}

grpc::Status Runner::uploadOutputs(const std::string &stdout_file,
                                   const std::string &stderr_file,
                                   Digest *stdout_digest,
                                   Digest *stderr_digest) const
{
    *stdout_digest = DigestGenerator::hashFile(stdout_file);
    *stderr_digest = DigestGenerator::hashFile(stderr_file);

    const std::vector<CASClient::UploadRequest> upload_requests = {
        CASClient::UploadRequest::from_path(*stdout_digest, stdout_file),
        CASClient::UploadRequest::from_path(*stderr_digest, stderr_file),
    };

    try {
        const auto failed_blobs =
            this->d_casClient->uploadBlobs(upload_requests);
        if (!failed_blobs.empty()) {
            BUILDBOX_LOG_ERROR(
                "Failed to upload stdout or stderr contents. Received: "
                << failed_blobs.front().status.error_message());
            *stdout_digest = Digest();
            *stderr_digest = Digest();
            return failed_blobs.front().status;
        }
        return grpc::Status::OK;
    }
    catch (const std::exception &e) {
        BUILDBOX_LOG_ERROR("Failed to upload stdout and stderr: " << e.what());
        return grpc::Status(grpc::StatusCode::INTERNAL, e.what());
    }
}
} // namespace buildboxcommon
