CrashHandler.cpp 11 KB

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