mirror of
https://github.com/obsproject/obs-studio.git
synced 2025-12-31 02:18:13 -05:00
Putting the crash sentinel in the main "obs-studio" directory carries the risk of unwanted deletion of files in the same directory, which usually contains user configuration files like "user.ini" and "global.ini". Even though the code will ignore any file that does not start with the crash sentinel prefix string, the code itself would allow changing the prefix to "global.ini" and thus inadvertently delete the global configuration file as well. To further reduce the risk, this change will put the sentinels in a separate sub-directory that should only ever contain sentinel files, all of which can possibly deleted without any data loss for the user.
393 lines
11 KiB
C++
393 lines
11 KiB
C++
/******************************************************************************
|
|
Copyright (C) 2025 by Patrick Heyer <PatTheMav@users.noreply.github.com>
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
******************************************************************************/
|
|
|
|
#include "CrashHandler.hpp"
|
|
#include <OBSApp.hpp>
|
|
#include <qt-wrappers.hpp>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <string_view>
|
|
|
|
#include "moc_CrashHandler.cpp"
|
|
|
|
using json = nlohmann::json;
|
|
using CrashLogUpdateResult = OBS::CrashHandler::CrashLogUpdateResult;
|
|
|
|
namespace {
|
|
|
|
constexpr std::string_view crashSentinelPath = "obs-studio/.sentinel";
|
|
constexpr std::string_view crashSentinelPrefix = "run_";
|
|
constexpr std::string_view crashUploadURL = "https://obsproject.com/logs/upload";
|
|
|
|
#ifndef NDEBUG
|
|
constexpr bool isSentinelEnabled = false;
|
|
#else
|
|
constexpr bool isSentinelEnabled = true;
|
|
#endif
|
|
|
|
std::string getCrashLogFileContent(std::filesystem::path crashLogFile)
|
|
{
|
|
std::string crashLogFileContent;
|
|
|
|
if (!std::filesystem::exists(crashLogFile)) {
|
|
return crashLogFileContent;
|
|
}
|
|
|
|
std::fstream filestream;
|
|
std::streampos crashLogFileSize = 0;
|
|
filestream.open(crashLogFile, std::ios::in | std::ios::binary);
|
|
|
|
if (filestream.is_open()) {
|
|
crashLogFileSize = filestream.tellg();
|
|
filestream.seekg(0, std::ios::end);
|
|
crashLogFileSize = filestream.tellg() - crashLogFileSize;
|
|
filestream.seekg(0);
|
|
|
|
crashLogFileContent.resize(crashLogFileSize);
|
|
crashLogFileContent.assign((std::istreambuf_iterator<char>(filestream)),
|
|
(std::istreambuf_iterator<char>()));
|
|
}
|
|
|
|
return crashLogFileContent;
|
|
}
|
|
|
|
std::pair<OBS::TimePoint, std::string> buildCrashLogUploadContent(OBS::PlatformType platformType,
|
|
std::string crashLogFileContent)
|
|
{
|
|
OBS::TimePoint uploadTimePoint = OBS::Clock::now();
|
|
std::time_t uploadTimePoint_c = OBS::Clock::to_time_t(uploadTimePoint);
|
|
std::tm uploadTimeLocal = *std::localtime(&uploadTimePoint_c);
|
|
|
|
std::stringstream uploadLogMessage;
|
|
|
|
switch (platformType) {
|
|
case OBS::PlatformType::Windows:
|
|
uploadLogMessage << "OBS " << App()->GetVersionString(false) << " crash file uploaded at "
|
|
<< std::put_time(&uploadTimeLocal, "%Y-%m-%d, %X") << "\n\n"
|
|
<< crashLogFileContent;
|
|
break;
|
|
case OBS::PlatformType::macOS:
|
|
uploadLogMessage << crashLogFileContent;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return std::pair<OBS::TimePoint, std::string>(uploadTimePoint, uploadLogMessage.str());
|
|
}
|
|
} // namespace
|
|
|
|
namespace OBS {
|
|
|
|
static_assert(!crashSentinelPath.empty(), "Crash sentinel path name cannot be empty");
|
|
static_assert(!crashSentinelPrefix.empty(), "Crash sentinel filename prefix cannot be empty");
|
|
static_assert(!crashUploadURL.empty(), "Crash sentinel upload URL cannot be empty");
|
|
|
|
CrashHandler::CrashHandler(QUuid appLaunchUUID) : appLaunchUUID_(appLaunchUUID)
|
|
{
|
|
if (!isSentinelEnabled) {
|
|
return;
|
|
}
|
|
|
|
setupSentinel();
|
|
checkCrashState();
|
|
}
|
|
|
|
CrashHandler::~CrashHandler()
|
|
{
|
|
if (isActiveCrashHandler_) {
|
|
applicationShutdownHandler();
|
|
}
|
|
}
|
|
|
|
CrashHandler::CrashHandler(CrashHandler &&other) noexcept
|
|
{
|
|
crashLogUploadThread_ = std::exchange(other.crashLogUploadThread_, nullptr);
|
|
isActiveCrashHandler_ = true;
|
|
other.isActiveCrashHandler_ = false;
|
|
}
|
|
|
|
CrashHandler &CrashHandler::operator=(CrashHandler &&other) noexcept
|
|
{
|
|
std::swap(crashLogUploadThread_, other.crashLogUploadThread_);
|
|
isActiveCrashHandler_ = true;
|
|
other.isActiveCrashHandler_ = false;
|
|
return *this;
|
|
}
|
|
|
|
bool CrashHandler::hasUncleanShutdown()
|
|
{
|
|
return isSentinelEnabled && isSentinelPresent_;
|
|
}
|
|
|
|
bool CrashHandler::hasNewCrashLog()
|
|
{
|
|
CrashLogUpdateResult result = updateLocalCrashLogState();
|
|
|
|
if (result == CrashLogUpdateResult::NotAvailable) {
|
|
return false;
|
|
}
|
|
|
|
bool hasNewCrashLog = (result == CrashLogUpdateResult::Updated);
|
|
bool hasNoLogUrl = lastCrashLogURL_.empty();
|
|
|
|
return (hasNewCrashLog || hasNoLogUrl);
|
|
}
|
|
|
|
CrashLogUpdateResult CrashHandler::updateLocalCrashLogState()
|
|
{
|
|
updateCrashLogFromConfig();
|
|
|
|
std::filesystem::path lastLocalCrashLogFile = findLastCrashLog();
|
|
|
|
if (lastLocalCrashLogFile.empty() && lastCrashLogFile_.empty()) {
|
|
return CrashLogUpdateResult::NotAvailable;
|
|
}
|
|
|
|
if (lastLocalCrashLogFile != lastCrashLogFile_) {
|
|
lastCrashLogFile_ = std::move(lastLocalCrashLogFile);
|
|
lastCrashLogFileName_ = lastCrashLogFile_.filename().u8string();
|
|
lastCrashLogURL_.clear();
|
|
|
|
return CrashLogUpdateResult::Updated;
|
|
} else {
|
|
return CrashLogUpdateResult::NotUpdated;
|
|
}
|
|
}
|
|
|
|
void CrashHandler::uploadLastCrashLog()
|
|
{
|
|
bool newCrashDetected = hasNewCrashLog();
|
|
|
|
if (newCrashDetected) {
|
|
uploadCrashLogToServer();
|
|
} else {
|
|
handleExistingCrashLogUpload();
|
|
}
|
|
}
|
|
|
|
void CrashHandler::checkCrashState()
|
|
{
|
|
bool currentSentinelFound = false;
|
|
|
|
std::filesystem::path crashSentinelPath = crashSentinelFile_.parent_path();
|
|
|
|
if (!std::filesystem::exists(crashSentinelPath)) {
|
|
try {
|
|
std::filesystem::create_directory(crashSentinelPath);
|
|
} catch (const std::filesystem::filesystem_error &error) {
|
|
blog(LOG_ERROR,
|
|
"Crash sentinel location '%s' does not exist and unable to create directory:\n%s.",
|
|
crashSentinelPath.u8string().c_str(), error.what());
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (const auto &entry : std::filesystem::directory_iterator(crashSentinelPath)) {
|
|
if (entry.is_directory()) {
|
|
continue;
|
|
}
|
|
|
|
std::string entryFileName = entry.path().filename().u8string();
|
|
|
|
if (entryFileName.rfind(crashSentinelPrefix.data(), 0) != 0) {
|
|
continue;
|
|
}
|
|
|
|
if (entry.path().filename() == crashSentinelFile_.filename()) {
|
|
currentSentinelFound = true;
|
|
continue;
|
|
}
|
|
|
|
isSentinelPresent_ = true;
|
|
}
|
|
|
|
if (!currentSentinelFound) {
|
|
std::fstream filestream;
|
|
filestream.open(crashSentinelFile_, std::ios::out);
|
|
filestream.close();
|
|
}
|
|
}
|
|
|
|
void CrashHandler::setupSentinel()
|
|
{
|
|
BPtr crashSentinelPathString = GetAppConfigPathPtr(crashSentinelPath.data());
|
|
std::string appLaunchUUIDString = appLaunchUUID_.toString(QUuid::WithoutBraces).toStdString();
|
|
|
|
std::string crashSentinelFilePath = crashSentinelPathString.Get();
|
|
crashSentinelFilePath.reserve(crashSentinelFilePath.size() + crashSentinelPrefix.size() +
|
|
appLaunchUUIDString.size() + 1);
|
|
crashSentinelFilePath.append("/").append(crashSentinelPrefix).append(appLaunchUUIDString);
|
|
|
|
crashSentinelFile_ = std::filesystem::u8path(crashSentinelFilePath);
|
|
|
|
isActiveCrashHandler_ = true;
|
|
}
|
|
|
|
void CrashHandler::updateCrashLogFromConfig()
|
|
{
|
|
config_t *appConfig = App()->GetAppConfig();
|
|
|
|
if (!appConfig) {
|
|
return;
|
|
}
|
|
|
|
const char *last_crash_log_file = config_get_string(appConfig, "CrashHandler", "last_crash_log_file");
|
|
const char *last_crash_log_url = config_get_string(appConfig, "CrashHandler", "last_crash_log_url");
|
|
|
|
std::string lastCrashLogFilePath = last_crash_log_file ? last_crash_log_file : "";
|
|
int64_t lastCrashLogUploadTimestamp = config_get_int(appConfig, "CrashHandler", "last_crash_log_time");
|
|
std::string lastCrashLogUploadURL = last_crash_log_url ? last_crash_log_url : "";
|
|
|
|
OBS::Clock::duration durationSinceEpoch = std::chrono::seconds(lastCrashLogUploadTimestamp);
|
|
OBS::TimePoint lastCrashLogUploadTime = OBS::TimePoint(durationSinceEpoch);
|
|
|
|
lastCrashLogFile_ = std::filesystem::u8path(lastCrashLogFilePath);
|
|
lastCrashLogFileName_ = lastCrashLogFile_.filename().u8string();
|
|
lastCrashLogURL_ = lastCrashLogUploadURL;
|
|
lastCrashUploadTime_ = lastCrashLogUploadTime;
|
|
}
|
|
|
|
void CrashHandler::saveCrashLogToConfig()
|
|
{
|
|
config_t *appConfig = App()->GetAppConfig();
|
|
|
|
if (!appConfig) {
|
|
return;
|
|
}
|
|
|
|
std::time_t uploadTimePoint_c = OBS::Clock::to_time_t(lastCrashUploadTime_);
|
|
|
|
config_set_string(appConfig, "CrashHandler", "last_crash_log_file", lastCrashLogFile_.u8string().c_str());
|
|
config_set_int(appConfig, "CrashHandler", "last_crash_log_time", uploadTimePoint_c);
|
|
config_set_string(appConfig, "CrashHandler", "last_crash_log_url", lastCrashLogURL_.c_str());
|
|
config_save_safe(appConfig, "tmp", nullptr);
|
|
}
|
|
|
|
void CrashHandler::uploadCrashLogToServer()
|
|
{
|
|
std::string crashLogFileContent = getCrashLogFileContent(lastCrashLogFile_);
|
|
|
|
if (crashLogFileContent.empty()) {
|
|
blog(LOG_WARNING, "Most recent crash log file was empty or unavailable for reading");
|
|
return;
|
|
}
|
|
|
|
emit crashLogUploadStarted();
|
|
|
|
PlatformType platformType = getPlatformType();
|
|
|
|
auto crashLogUploadContent = buildCrashLogUploadContent(platformType, crashLogFileContent);
|
|
|
|
if (crashLogUploadThread_) {
|
|
crashLogUploadThread_->wait();
|
|
}
|
|
|
|
auto uploadThread =
|
|
std::make_unique<RemoteTextThread>(crashUploadURL.data(), "text/plain", crashLogUploadContent.second);
|
|
|
|
std::swap(crashLogUploadThread_, uploadThread);
|
|
|
|
connect(crashLogUploadThread_.get(), &RemoteTextThread::Result, this,
|
|
&CrashHandler::crashLogUploadResultHandler);
|
|
|
|
lastCrashUploadTime_ = crashLogUploadContent.first;
|
|
|
|
crashLogUploadThread_->start();
|
|
}
|
|
|
|
void CrashHandler::handleExistingCrashLogUpload()
|
|
{
|
|
if (!lastCrashLogURL_.empty()) {
|
|
const QString crashLogUrlString = QString::fromStdString(lastCrashLogURL_);
|
|
|
|
emit crashLogUploadFinished(crashLogUrlString);
|
|
} else {
|
|
uploadCrashLogToServer();
|
|
}
|
|
}
|
|
|
|
void CrashHandler::crashLogUploadResultHandler(const QString &uploadResult, const QString &error)
|
|
{
|
|
if (uploadResult.isEmpty()) {
|
|
emit crashLogUploadFailed(error);
|
|
return;
|
|
}
|
|
|
|
json uploadResultData = json::parse(uploadResult.toStdString());
|
|
|
|
try {
|
|
std::string crashLogUrl = uploadResultData["url"];
|
|
|
|
lastCrashLogURL_ = crashLogUrl;
|
|
|
|
saveCrashLogToConfig();
|
|
|
|
const QString crashLogUrlString = QString::fromStdString(crashLogUrl);
|
|
|
|
emit crashLogUploadFinished(crashLogUrlString);
|
|
} catch (const json::exception &error) {
|
|
blog(LOG_ERROR, "JSON error while parsing crash upload response:\n%s", error.what());
|
|
|
|
const QString jsonErrorMessage = QTStr("CrashHandling.Errors.UploadJSONError");
|
|
emit crashLogUploadFailed(jsonErrorMessage);
|
|
}
|
|
}
|
|
|
|
void CrashHandler::applicationShutdownHandler() noexcept
|
|
{
|
|
if (!isSentinelEnabled) {
|
|
return;
|
|
}
|
|
|
|
if (crashSentinelFile_.empty()) {
|
|
blog(LOG_ERROR, "No crash sentinel location set for crash handler");
|
|
return;
|
|
}
|
|
|
|
const std::filesystem::path crashSentinelPath = crashSentinelFile_.parent_path();
|
|
|
|
if (!std::filesystem::exists(crashSentinelPath)) {
|
|
blog(LOG_ERROR, "Crash sentinel location '%s' does not exist", crashSentinelPath.u8string().c_str());
|
|
return;
|
|
}
|
|
|
|
for (const auto &entry : std::filesystem::directory_iterator(crashSentinelPath)) {
|
|
if (entry.is_directory()) {
|
|
continue;
|
|
}
|
|
|
|
std::string entryFileName = entry.path().filename().u8string();
|
|
|
|
if (entryFileName.rfind(crashSentinelPrefix.data(), 0) != 0) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
std::filesystem::remove(entry.path());
|
|
} catch (const std::filesystem::filesystem_error &error) {
|
|
blog(LOG_ERROR, "Failed to delete crash sentinel file:\n%s", error.what());
|
|
}
|
|
}
|
|
|
|
isActiveCrashHandler_ = false;
|
|
}
|
|
} // namespace OBS
|