carb/crashreporter/CrashReporterUtils.h

File members: carb/crashreporter/CrashReporterUtils.h

// Copyright (c) 2019-2024, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//
#pragma once

#include "../Framework.h"
#include "../InterfaceUtils.h"
#include "../logging/Log.h"
#include "../settings/ISettings.h"
#include "ICrashReporter.h"

#if CARB_PLATFORM_WINDOWS && !defined(_DLL)
#    include "../CarbWindows.h"
#    include "../extras/Library.h"
#elif CARB_PLATFORM_LINUX
#    include <argz.h>
#endif

#include <signal.h>
#include <string.h>
#include <future>
#include <string>
#include <fstream>
#include <ios>

CARB_WEAKLINK carb::crashreporter::ICrashReporter* g_carbCrashReporter;

#ifdef DOXYGEN_BUILD
#    define CARB_CRASH_REPORTER_GLOBALS()
#else
// only install the signal handler for modules that have been statically linked to the
// Windows CRT (OVCC-1379).  This is done because plugins that statically link to the
// CRT have their own copies of the signal handlers table and the crash reporter is
// unable to directly manipulate those.  By setting the signal handler here in the
// context of the statically linked plugin, we provide a way to relay that abort
// signal to the crash reporter.
#    if CARB_PLATFORM_WINDOWS && !defined(_DLL)
#        if !CARB_COMPILER_MSC
static_assert(false, "Unsupported compiler!");
#        endif
#        define CARB_CRASH_REPORTER_GLOBALS()                                                                          \
            bool g_carbSignalHandlerInstalled = carb::crashreporter::detail::installSignalHandler();

namespace carb
{
namespace crashreporter
{
namespace detail
{

inline bool installSignalHandler()
{
    using SignalHandlerFn = void (*)(int);
    static bool disableHandler = []() {
        WCHAR envVarValue[32] = { 0 };

        return GetEnvironmentVariableW(L"CARB_DISABLE_ABORT_HANDLER", envVarValue, CARB_COUNTOF32(envVarValue)) != 0 &&
               envVarValue[0] == '1' && envVarValue[1] == '\0';
    }();

    if (disableHandler)
        return false;

    static SignalHandlerFn handler = []() -> SignalHandlerFn {
        SignalHandlerFn fn;
        carb::extras::LibraryHandle handle = carb::extras::loadLibrary(
            "carb", carb::extras::fLibFlagMakeFullLibName | carb::extras::fLibFlagLoadExisting);

        if (handle == carb::extras::kInvalidLibraryHandle)
            return nullptr;

        fn = carb::extras::getLibrarySymbol<SignalHandlerFn>(handle, "carbSignalHandler");

        if (fn == nullptr)
        {
            carb::extras::unloadLibrary(handle);
            return nullptr;
        }

        return fn;
    }();

    if (handler == nullptr)
        return false;

    // install the signal handler for this thread in this module.  Since signals on Windows
    // are a bonus feature and rarely used, we don't care about preserving the previous
    // signal handler.
    signal(SIGABRT, handler);
    return true;
}

} // namespace detail
} // namespace crashreporter
} // namespace carb

#    else
#        define CARB_CRASH_REPORTER_GLOBALS()
#    endif
#endif

namespace carb
{
namespace crashreporter
{

constexpr uintptr_t kBaseMagicSignature = 0xc7a547e907137700ull;

constexpr uintptr_t kMagicSignature = kBaseMagicSignature;

#if CARB_PLATFORM_LINUX || defined(DOXYGEN_BUILD)
static int kExternalTerminationSignal = SIGRTMAX - 1;
#elif CARB_PLATFORM_MACOS
constexpr int kExternalTerminationSignal = SIGUSR2;
#endif

inline void registerCrashReporterForClient()
{
    g_carbCrashReporter = getFramework()->tryAcquireInterface<ICrashReporter>();
}

inline void deregisterCrashReporterForClient()
{
    if (g_carbCrashReporter)
    {
        getFramework()->releaseInterface(g_carbCrashReporter);
        g_carbCrashReporter = nullptr;
    }
}

inline std::future<void> sendAndRemoveLeftOverDumpsAsync()
{
    std::unique_ptr<std::promise<void>> sentPromise(new std::promise<void>());

    std::future<void> sentFuture(sentPromise->get_future());

    if (g_carbCrashReporter)
    {
        const auto finishCallback = [](void* promisePtr) {
            auto sentPromise = reinterpret_cast<std::promise<void>*>(promisePtr);
            sentPromise->set_value();
            delete sentPromise;
        };
        g_carbCrashReporter->sendAndRemoveLeftOverDumpsAsync(finishCallback, sentPromise.release());
    }
    else
    {
        CARB_LOG_WARN("No crash reporter present, dumps uploading isn't available.");
        sentPromise->set_value();
    }
    return sentFuture;
}

namespace detail
{
inline std::string sanitizeExtraCrashFileKey(const char* keyName)
{
    std::string key = keyName;

    // sanitize the key name so that it contains only database friendly characters.
    for (auto& c : key)
    {
        if (c <= ' ' || c >= 127 || strchr("\"'\\/,#$%^&*()!~`[]{}|<>?;:=+.\t\b\n\r ", c) != nullptr)
        {
            c = '_';
        }
    }

    return key;
}
} // namespace detail

template <typename T>
inline bool addCrashMetadata(const char* keyName, T value)
{
    return addCrashMetadata(keyName, std::to_string(value).c_str());
}

template <>
inline bool addCrashMetadata(const char* keyName, const char* value)
{
    carb::settings::ISettings* settings = carb::getCachedInterface<carb::settings::ISettings>();
    std::string key;

    if (settings == nullptr)
        return false;

    key = detail::sanitizeExtraCrashFileKey(keyName);
    settings->setString((std::string("/crashreporter/data/") + key).c_str(), value);

    return true;
}

inline const char* getCrashMetadataValue(const char* keyName)
{
    carb::settings::ISettings* settings = carb::getCachedInterface<carb::settings::ISettings>();
    std::string key;

    if (settings == nullptr)
        return nullptr;

    key = "/crashreporter/data/" + detail::sanitizeExtraCrashFileKey(keyName);
    return settings->getStringBuffer(key.c_str());
}

inline bool addExtraCrashFile(const char* keyName, const char* filename)
{
    carb::settings::ISettings* settings = carb::getCachedInterface<carb::settings::ISettings>();
    std::string key;

    if (settings == nullptr)
        return false;

    // sanitize the key name so that it contains only database friendly characters.
    key = detail::sanitizeExtraCrashFileKey(keyName);

    settings->setString(("/crashreporter/files/" + key).c_str(), filename);

    return true;
}

inline bool isExtraCrashFileKeyUsed(const char* keyName)
{
    carb::settings::ISettings* settings = carb::getCachedInterface<carb::settings::ISettings>();
    std::string key;

    if (settings == nullptr)
        return false;

    // sanitize the key name so that it contains only database friendly characters.
    key = detail::sanitizeExtraCrashFileKey(keyName);

    return settings->isAccessibleAs(carb::dictionary::ItemType::eString, ("/crashreporter/files/" + key).c_str());
}

inline std::string getCommandLineArgsAsString()
{
    std::string out;

#if CARB_PLATFORM_WINDOWS
    out = carb::extras::convertWideToUtf8(GetCommandLineW());

#elif CARB_POSIX
    auto escapeString = [](const char* str) -> std::string {
        std::string out;
        const char* last = str;

        for (size_t i = 0; str[i] != '\0'; i++)
        {
            if (str[i] == ' ' || str[i] == '\"' || str[i] == '\\')
            {
                out += std::string(last, &str[i] - last) + "\\";
                last = &str[i];
            }
        }

        // add any remaining chunk of the string.
        out += last;

        return out;
    };
    auto getCmdLineString = [&](size_t argc, char* argv[]) -> std::string {
        std::string out;

        for (size_t i = 0; i < argc; i++)
        {
            out += escapeString(argv[i]) + std::string(" ");
        }

        out.pop_back();
        return out;
    };

#    if CARB_PLATFORM_LINUX
    std::string cmdLine;
    std::getline(std::fstream("/proc/self/cmdline", std::ios::in), cmdLine);

    size_t argc = argz_count(cmdLine.c_str(), cmdLine.size());
    auto argv = std::make_unique<char*[]>(argc + 1);
    argz_extract(cmdLine.c_str(), cmdLine.size(), argv.get());

    out = getCmdLineString(argc, argv.get());
#    elif CARB_PLATFORM_MACOS
    int argc = *_NSGetArgc();
    char** argv = *_NSGetArgv();

    out = getCmdLineString(argc, argv);
#    endif
#else
    CARB_UNSUPPORTED_PLATFORM();
#endif
    return out;
}

} // namespace crashreporter
} // namespace carb