CrashHandler.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. /******************************************************************************
  2. Copyright (C) 2025 by Patrick Heyer <[email protected]>
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 2 of the License, or
  6. (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. ******************************************************************************/
  14. #include "CrashHandler.hpp"
  15. #include <OBSApp.hpp>
  16. #include <qt-wrappers.hpp>
  17. #include <nlohmann/json.hpp>
  18. #include <fstream>
  19. #include <iostream>
  20. #include <sstream>
  21. #include <string_view>
  22. #include "moc_CrashHandler.cpp"
  23. using json = nlohmann::json;
  24. using CrashLogUpdateResult = OBS::CrashHandler::CrashLogUpdateResult;
  25. namespace {
  26. constexpr std::string_view crashSentinelPath = "obs-studio";
  27. constexpr std::string_view crashSentinelPrefix = "crash_sentinel_";
  28. constexpr std::string_view crashUploadURL = "https://obsproject.com/logs/upload";
  29. #ifndef NDEBUG
  30. constexpr bool isSentinelEnabled = false;
  31. #else
  32. constexpr bool isSentinelEnabled = true;
  33. #endif
  34. std::string getCrashLogFileContent(std::filesystem::path crashLogFile)
  35. {
  36. std::string crashLogFileContent;
  37. if (!std::filesystem::exists(crashLogFile)) {
  38. return crashLogFileContent;
  39. }
  40. std::fstream filestream;
  41. std::streampos crashLogFileSize = 0;
  42. filestream.open(crashLogFile, std::ios::in | std::ios::binary);
  43. if (filestream.is_open()) {
  44. crashLogFileSize = filestream.tellg();
  45. filestream.seekg(0, std::ios::end);
  46. crashLogFileSize = filestream.tellg() - crashLogFileSize;
  47. filestream.seekg(0);
  48. crashLogFileContent.resize(crashLogFileSize);
  49. crashLogFileContent.assign((std::istreambuf_iterator<char>(filestream)),
  50. (std::istreambuf_iterator<char>()));
  51. }
  52. return crashLogFileContent;
  53. }
  54. std::pair<OBS::TimePoint, std::string> buildCrashLogUploadContent(OBS::PlatformType platformType,
  55. std::string crashLogFileContent)
  56. {
  57. OBS::TimePoint uploadTimePoint = OBS::Clock::now();
  58. std::time_t uploadTimePoint_c = OBS::Clock::to_time_t(uploadTimePoint);
  59. std::tm uploadTimeLocal = *std::localtime(&uploadTimePoint_c);
  60. std::stringstream uploadLogMessage;
  61. switch (platformType) {
  62. case OBS::PlatformType::Windows:
  63. uploadLogMessage << "OBS " << App()->GetVersionString(false) << " crash file uploaded at "
  64. << std::put_time(&uploadTimeLocal, "%Y-%m-%d, %X") << "\n\n"
  65. << crashLogFileContent;
  66. break;
  67. case OBS::PlatformType::macOS:
  68. uploadLogMessage << crashLogFileContent;
  69. default:
  70. break;
  71. }
  72. return std::pair<OBS::TimePoint, std::string>(uploadTimePoint, uploadLogMessage.str());
  73. }
  74. } // namespace
  75. namespace OBS {
  76. static_assert(!crashSentinelPath.empty(), "Crash sentinel path name cannot be empty");
  77. static_assert(!crashSentinelPrefix.empty(), "Crash sentinel filename prefix cannot be empty");
  78. static_assert(!crashUploadURL.empty(), "Crash sentinel upload URL cannot be empty");
  79. CrashHandler::CrashHandler(QUuid appLaunchUUID) : appLaunchUUID_(appLaunchUUID)
  80. {
  81. if (!isSentinelEnabled) {
  82. return;
  83. }
  84. setupSentinel();
  85. checkCrashState();
  86. }
  87. CrashHandler::~CrashHandler()
  88. {
  89. if (isActiveCrashHandler_) {
  90. applicationShutdownHandler();
  91. }
  92. }
  93. CrashHandler::CrashHandler(CrashHandler &&other) noexcept
  94. {
  95. crashLogUploadThread_ = std::exchange(other.crashLogUploadThread_, nullptr);
  96. isActiveCrashHandler_ = true;
  97. other.isActiveCrashHandler_ = false;
  98. }
  99. CrashHandler &CrashHandler::operator=(CrashHandler &&other) noexcept
  100. {
  101. std::swap(crashLogUploadThread_, other.crashLogUploadThread_);
  102. isActiveCrashHandler_ = true;
  103. other.isActiveCrashHandler_ = false;
  104. return *this;
  105. }
  106. bool CrashHandler::hasUncleanShutdown()
  107. {
  108. return isSentinelEnabled && isSentinelPresent_;
  109. }
  110. bool CrashHandler::hasNewCrashLog()
  111. {
  112. CrashLogUpdateResult result = updateLocalCrashLogState();
  113. bool hasNewCrashLog = (result == CrashLogUpdateResult::Updated);
  114. bool hasNoLogUrl = lastCrashLogURL_.empty();
  115. return (hasNewCrashLog || hasNoLogUrl);
  116. }
  117. CrashLogUpdateResult CrashHandler::updateLocalCrashLogState()
  118. {
  119. updateCrashLogFromConfig();
  120. std::filesystem::path lastLocalCrashLogFile = findLastCrashLog();
  121. if (lastLocalCrashLogFile != lastCrashLogFile_) {
  122. lastCrashLogFile_ = std::move(lastLocalCrashLogFile);
  123. lastCrashLogFileName_ = lastCrashLogFile_.filename().u8string();
  124. lastCrashLogURL_.clear();
  125. return CrashLogUpdateResult::Updated;
  126. } else {
  127. return CrashLogUpdateResult::NotUpdated;
  128. }
  129. }
  130. void CrashHandler::uploadLastCrashLog()
  131. {
  132. bool newCrashDetected = hasNewCrashLog();
  133. if (newCrashDetected) {
  134. uploadCrashLogToServer();
  135. } else {
  136. handleExistingCrashLogUpload();
  137. }
  138. }
  139. void CrashHandler::checkCrashState()
  140. {
  141. bool currentSentinelFound = false;
  142. std::filesystem::path crashSentinelPath = crashSentinelFile_.parent_path();
  143. if (!std::filesystem::exists(crashSentinelPath)) {
  144. blog(LOG_ERROR, "Crash sentinel location '%s' does not exist", crashSentinelPath.u8string().c_str());
  145. return;
  146. }
  147. for (const auto &entry : std::filesystem::directory_iterator(crashSentinelPath)) {
  148. if (entry.is_directory()) {
  149. continue;
  150. }
  151. std::string entryFileName = entry.path().filename().u8string();
  152. if (entryFileName.rfind(crashSentinelPrefix.data(), 0) != 0) {
  153. continue;
  154. }
  155. if (entry.path().filename() == crashSentinelFile_.filename()) {
  156. currentSentinelFound = true;
  157. continue;
  158. }
  159. isSentinelPresent_ = true;
  160. }
  161. if (!currentSentinelFound) {
  162. std::fstream filestream;
  163. filestream.open(crashSentinelFile_, std::ios::out);
  164. filestream.close();
  165. }
  166. }
  167. void CrashHandler::setupSentinel()
  168. {
  169. BPtr crashSentinelPathString = GetAppConfigPathPtr(crashSentinelPath.data());
  170. std::string appLaunchUUIDString = appLaunchUUID_.toString(QUuid::WithoutBraces).toStdString();
  171. std::string crashSentinelFilePath = crashSentinelPathString.Get();
  172. crashSentinelFilePath.reserve(crashSentinelFilePath.size() + crashSentinelPrefix.size() +
  173. appLaunchUUIDString.size() + 1);
  174. crashSentinelFilePath.append("/").append(crashSentinelPrefix).append(appLaunchUUIDString);
  175. crashSentinelFile_ = std::filesystem::u8path(crashSentinelFilePath);
  176. isActiveCrashHandler_ = true;
  177. }
  178. void CrashHandler::updateCrashLogFromConfig()
  179. {
  180. config_t *appConfig = App()->GetAppConfig();
  181. if (!appConfig) {
  182. return;
  183. }
  184. const char *last_crash_log_file = config_get_string(appConfig, "CrashHandler", "last_crash_log_file");
  185. const char *last_crash_log_url = config_get_string(appConfig, "CrashHandler", "last_crash_log_url");
  186. std::string lastCrashLogFilePath = last_crash_log_file ? last_crash_log_file : "";
  187. int64_t lastCrashLogUploadTimestamp = config_get_int(appConfig, "CrashHandler", "last_crash_log_time");
  188. std::string lastCrashLogUploadURL = last_crash_log_url ? last_crash_log_url : "";
  189. OBS::Clock::duration durationSinceEpoch = std::chrono::seconds(lastCrashLogUploadTimestamp);
  190. OBS::TimePoint lastCrashLogUploadTime = OBS::TimePoint(durationSinceEpoch);
  191. lastCrashLogFile_ = std::filesystem::u8path(lastCrashLogFilePath);
  192. lastCrashLogFileName_ = lastCrashLogFile_.filename().u8string();
  193. lastCrashLogURL_ = lastCrashLogUploadURL;
  194. lastCrashUploadTime_ = lastCrashLogUploadTime;
  195. }
  196. void CrashHandler::saveCrashLogToConfig()
  197. {
  198. config_t *appConfig = App()->GetAppConfig();
  199. if (!appConfig) {
  200. return;
  201. }
  202. std::time_t uploadTimePoint_c = OBS::Clock::to_time_t(lastCrashUploadTime_);
  203. config_set_string(appConfig, "CrashHandler", "last_crash_log_file", lastCrashLogFile_.u8string().c_str());
  204. config_set_int(appConfig, "CrashHandler", "last_crash_log_time", uploadTimePoint_c);
  205. config_set_string(appConfig, "CrashHandler", "last_crash_log_url", lastCrashLogURL_.c_str());
  206. config_save_safe(appConfig, "tmp", nullptr);
  207. }
  208. void CrashHandler::uploadCrashLogToServer()
  209. {
  210. std::string crashLogFileContent = getCrashLogFileContent(lastCrashLogFile_);
  211. if (crashLogFileContent.empty()) {
  212. blog(LOG_WARNING, "Most recent crash log file was empty or unavailable for reading");
  213. return;
  214. }
  215. emit crashLogUploadStarted();
  216. PlatformType platformType = getPlatformType();
  217. auto crashLogUploadContent = buildCrashLogUploadContent(platformType, crashLogFileContent);
  218. if (crashLogUploadThread_) {
  219. crashLogUploadThread_->wait();
  220. }
  221. auto uploadThread =
  222. std::make_unique<RemoteTextThread>(crashUploadURL.data(), "text/plain", crashLogUploadContent.second);
  223. std::swap(crashLogUploadThread_, uploadThread);
  224. connect(crashLogUploadThread_.get(), &RemoteTextThread::Result, this,
  225. &CrashHandler::crashLogUploadResultHandler);
  226. lastCrashUploadTime_ = crashLogUploadContent.first;
  227. crashLogUploadThread_->start();
  228. }
  229. void CrashHandler::handleExistingCrashLogUpload()
  230. {
  231. if (!lastCrashLogURL_.empty()) {
  232. const QString crashLogUrlString = QString::fromStdString(lastCrashLogURL_);
  233. emit crashLogUploadFinished(crashLogUrlString);
  234. } else {
  235. uploadCrashLogToServer();
  236. }
  237. }
  238. void CrashHandler::crashLogUploadResultHandler(const QString &uploadResult, const QString &error)
  239. {
  240. if (uploadResult.isEmpty()) {
  241. emit crashLogUploadFailed(error);
  242. return;
  243. }
  244. json uploadResultData = json::parse(uploadResult.toStdString());
  245. try {
  246. std::string crashLogUrl = uploadResultData["url"];
  247. lastCrashLogURL_ = crashLogUrl;
  248. saveCrashLogToConfig();
  249. const QString crashLogUrlString = QString::fromStdString(crashLogUrl);
  250. emit crashLogUploadFinished(crashLogUrlString);
  251. } catch (const json::exception &error) {
  252. blog(LOG_ERROR, "JSON error while parsing crash upload response:\n%s", error.what());
  253. const QString jsonErrorMessage = QTStr("CrashHandling.Errors.UploadJSONError");
  254. emit crashLogUploadFailed(jsonErrorMessage);
  255. }
  256. }
  257. void CrashHandler::applicationShutdownHandler() noexcept
  258. {
  259. if (!isSentinelEnabled) {
  260. return;
  261. }
  262. if (crashSentinelFile_.empty()) {
  263. blog(LOG_ERROR, "No crash sentinel location set for crash handler");
  264. return;
  265. }
  266. const std::filesystem::path crashSentinelPath = crashSentinelFile_.parent_path();
  267. if (!std::filesystem::exists(crashSentinelPath)) {
  268. blog(LOG_ERROR, "Crash sentinel location '%s' does not exist", crashSentinelPath.u8string().c_str());
  269. return;
  270. }
  271. for (const auto &entry : std::filesystem::directory_iterator(crashSentinelPath)) {
  272. if (entry.is_directory()) {
  273. continue;
  274. }
  275. std::string entryFileName = entry.path().filename().u8string();
  276. if (entryFileName.rfind(crashSentinelPrefix.data(), 0) != 0) {
  277. continue;
  278. }
  279. try {
  280. std::filesystem::remove(entry.path());
  281. } catch (const std::filesystem::filesystem_error &error) {
  282. blog(LOG_ERROR, "Failed to delete crash sentinel file:\n%s", error.what());
  283. }
  284. }
  285. isActiveCrashHandler_ = false;
  286. }
  287. } // namespace OBS