CrashHandler.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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. if (result == CrashLogUpdateResult::NotAvailable) {
  114. return false;
  115. }
  116. bool hasNewCrashLog = (result == CrashLogUpdateResult::Updated);
  117. bool hasNoLogUrl = lastCrashLogURL_.empty();
  118. return (hasNewCrashLog || hasNoLogUrl);
  119. }
  120. CrashLogUpdateResult CrashHandler::updateLocalCrashLogState()
  121. {
  122. updateCrashLogFromConfig();
  123. std::filesystem::path lastLocalCrashLogFile = findLastCrashLog();
  124. if (lastLocalCrashLogFile.empty() && lastCrashLogFile_.empty()) {
  125. return CrashLogUpdateResult::NotAvailable;
  126. }
  127. if (lastLocalCrashLogFile != lastCrashLogFile_) {
  128. lastCrashLogFile_ = std::move(lastLocalCrashLogFile);
  129. lastCrashLogFileName_ = lastCrashLogFile_.filename().u8string();
  130. lastCrashLogURL_.clear();
  131. return CrashLogUpdateResult::Updated;
  132. } else {
  133. return CrashLogUpdateResult::NotUpdated;
  134. }
  135. }
  136. void CrashHandler::uploadLastCrashLog()
  137. {
  138. bool newCrashDetected = hasNewCrashLog();
  139. if (newCrashDetected) {
  140. uploadCrashLogToServer();
  141. } else {
  142. handleExistingCrashLogUpload();
  143. }
  144. }
  145. void CrashHandler::checkCrashState()
  146. {
  147. bool currentSentinelFound = false;
  148. std::filesystem::path crashSentinelPath = crashSentinelFile_.parent_path();
  149. if (!std::filesystem::exists(crashSentinelPath)) {
  150. blog(LOG_ERROR, "Crash sentinel location '%s' does not exist", crashSentinelPath.u8string().c_str());
  151. return;
  152. }
  153. for (const auto &entry : std::filesystem::directory_iterator(crashSentinelPath)) {
  154. if (entry.is_directory()) {
  155. continue;
  156. }
  157. std::string entryFileName = entry.path().filename().u8string();
  158. if (entryFileName.rfind(crashSentinelPrefix.data(), 0) != 0) {
  159. continue;
  160. }
  161. if (entry.path().filename() == crashSentinelFile_.filename()) {
  162. currentSentinelFound = true;
  163. continue;
  164. }
  165. isSentinelPresent_ = true;
  166. }
  167. if (!currentSentinelFound) {
  168. std::fstream filestream;
  169. filestream.open(crashSentinelFile_, std::ios::out);
  170. filestream.close();
  171. }
  172. }
  173. void CrashHandler::setupSentinel()
  174. {
  175. BPtr crashSentinelPathString = GetAppConfigPathPtr(crashSentinelPath.data());
  176. std::string appLaunchUUIDString = appLaunchUUID_.toString(QUuid::WithoutBraces).toStdString();
  177. std::string crashSentinelFilePath = crashSentinelPathString.Get();
  178. crashSentinelFilePath.reserve(crashSentinelFilePath.size() + crashSentinelPrefix.size() +
  179. appLaunchUUIDString.size() + 1);
  180. crashSentinelFilePath.append("/").append(crashSentinelPrefix).append(appLaunchUUIDString);
  181. crashSentinelFile_ = std::filesystem::u8path(crashSentinelFilePath);
  182. isActiveCrashHandler_ = true;
  183. }
  184. void CrashHandler::updateCrashLogFromConfig()
  185. {
  186. config_t *appConfig = App()->GetAppConfig();
  187. if (!appConfig) {
  188. return;
  189. }
  190. const char *last_crash_log_file = config_get_string(appConfig, "CrashHandler", "last_crash_log_file");
  191. const char *last_crash_log_url = config_get_string(appConfig, "CrashHandler", "last_crash_log_url");
  192. std::string lastCrashLogFilePath = last_crash_log_file ? last_crash_log_file : "";
  193. int64_t lastCrashLogUploadTimestamp = config_get_int(appConfig, "CrashHandler", "last_crash_log_time");
  194. std::string lastCrashLogUploadURL = last_crash_log_url ? last_crash_log_url : "";
  195. OBS::Clock::duration durationSinceEpoch = std::chrono::seconds(lastCrashLogUploadTimestamp);
  196. OBS::TimePoint lastCrashLogUploadTime = OBS::TimePoint(durationSinceEpoch);
  197. lastCrashLogFile_ = std::filesystem::u8path(lastCrashLogFilePath);
  198. lastCrashLogFileName_ = lastCrashLogFile_.filename().u8string();
  199. lastCrashLogURL_ = lastCrashLogUploadURL;
  200. lastCrashUploadTime_ = lastCrashLogUploadTime;
  201. }
  202. void CrashHandler::saveCrashLogToConfig()
  203. {
  204. config_t *appConfig = App()->GetAppConfig();
  205. if (!appConfig) {
  206. return;
  207. }
  208. std::time_t uploadTimePoint_c = OBS::Clock::to_time_t(lastCrashUploadTime_);
  209. config_set_string(appConfig, "CrashHandler", "last_crash_log_file", lastCrashLogFile_.u8string().c_str());
  210. config_set_int(appConfig, "CrashHandler", "last_crash_log_time", uploadTimePoint_c);
  211. config_set_string(appConfig, "CrashHandler", "last_crash_log_url", lastCrashLogURL_.c_str());
  212. config_save_safe(appConfig, "tmp", nullptr);
  213. }
  214. void CrashHandler::uploadCrashLogToServer()
  215. {
  216. std::string crashLogFileContent = getCrashLogFileContent(lastCrashLogFile_);
  217. if (crashLogFileContent.empty()) {
  218. blog(LOG_WARNING, "Most recent crash log file was empty or unavailable for reading");
  219. return;
  220. }
  221. emit crashLogUploadStarted();
  222. PlatformType platformType = getPlatformType();
  223. auto crashLogUploadContent = buildCrashLogUploadContent(platformType, crashLogFileContent);
  224. if (crashLogUploadThread_) {
  225. crashLogUploadThread_->wait();
  226. }
  227. auto uploadThread =
  228. std::make_unique<RemoteTextThread>(crashUploadURL.data(), "text/plain", crashLogUploadContent.second);
  229. std::swap(crashLogUploadThread_, uploadThread);
  230. connect(crashLogUploadThread_.get(), &RemoteTextThread::Result, this,
  231. &CrashHandler::crashLogUploadResultHandler);
  232. lastCrashUploadTime_ = crashLogUploadContent.first;
  233. crashLogUploadThread_->start();
  234. }
  235. void CrashHandler::handleExistingCrashLogUpload()
  236. {
  237. if (!lastCrashLogURL_.empty()) {
  238. const QString crashLogUrlString = QString::fromStdString(lastCrashLogURL_);
  239. emit crashLogUploadFinished(crashLogUrlString);
  240. } else {
  241. uploadCrashLogToServer();
  242. }
  243. }
  244. void CrashHandler::crashLogUploadResultHandler(const QString &uploadResult, const QString &error)
  245. {
  246. if (uploadResult.isEmpty()) {
  247. emit crashLogUploadFailed(error);
  248. return;
  249. }
  250. json uploadResultData = json::parse(uploadResult.toStdString());
  251. try {
  252. std::string crashLogUrl = uploadResultData["url"];
  253. lastCrashLogURL_ = crashLogUrl;
  254. saveCrashLogToConfig();
  255. const QString crashLogUrlString = QString::fromStdString(crashLogUrl);
  256. emit crashLogUploadFinished(crashLogUrlString);
  257. } catch (const json::exception &error) {
  258. blog(LOG_ERROR, "JSON error while parsing crash upload response:\n%s", error.what());
  259. const QString jsonErrorMessage = QTStr("CrashHandling.Errors.UploadJSONError");
  260. emit crashLogUploadFailed(jsonErrorMessage);
  261. }
  262. }
  263. void CrashHandler::applicationShutdownHandler() noexcept
  264. {
  265. if (!isSentinelEnabled) {
  266. return;
  267. }
  268. if (crashSentinelFile_.empty()) {
  269. blog(LOG_ERROR, "No crash sentinel location set for crash handler");
  270. return;
  271. }
  272. const std::filesystem::path crashSentinelPath = crashSentinelFile_.parent_path();
  273. if (!std::filesystem::exists(crashSentinelPath)) {
  274. blog(LOG_ERROR, "Crash sentinel location '%s' does not exist", crashSentinelPath.u8string().c_str());
  275. return;
  276. }
  277. for (const auto &entry : std::filesystem::directory_iterator(crashSentinelPath)) {
  278. if (entry.is_directory()) {
  279. continue;
  280. }
  281. std::string entryFileName = entry.path().filename().u8string();
  282. if (entryFileName.rfind(crashSentinelPrefix.data(), 0) != 0) {
  283. continue;
  284. }
  285. try {
  286. std::filesystem::remove(entry.path());
  287. } catch (const std::filesystem::filesystem_error &error) {
  288. blog(LOG_ERROR, "Failed to delete crash sentinel file:\n%s", error.what());
  289. }
  290. }
  291. isActiveCrashHandler_ = false;
  292. }
  293. } // namespace OBS