assert_handler_observer.cpp 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. #include "assert_handler_observer.h"
  2. #include <Windows.h>
  3. #include <array>
  4. #include <cstdarg>
  5. #include <cstdint>
  6. #include <cstdio>
  7. #include <cstring>
  8. #include "../../../core/logging/log.h"
  9. #include "../../targets/game/assert_handler.h"
  10. namespace sunrise::client::hooks::assert_handler {
  11. namespace {
  12. /** The game formats assert text into a buffer of this size, so it bounds ours too. */
  13. constexpr std::size_t kTextCapacity = 1024;
  14. /** One log line carries the message plus its fixed prefix. */
  15. constexpr std::size_t kLineCapacity = 1152;
  16. /** Consecutive repeats of one message written in full before counting takes over. */
  17. constexpr std::uint32_t kRepeatHead = 8;
  18. /** One repeat in this many is written after that, so a stuck assert never goes silent. */
  19. constexpr std::uint32_t kRepeatStride = 512;
  20. /** Used when the game's own format string cannot be printed. */
  21. constexpr char kUnformattable[] = "<unformattable>";
  22. /**
  23. * Halt category of the graphics device-loss assert. A removed device never returns, so this one
  24. * must keep halting the process. Every other category stays non-fatal.
  25. */
  26. constexpr int kGraphicsHaltCategory = 6;
  27. /** The handler the game installed, called with the same printf-style arguments the sites use. */
  28. using NativeHandler = void(__cdecl*)(int, const char*, ...);
  29. SRWLOCK g_lock{SRWLOCK_INIT};
  30. /** Last message seen, so a message that repeats every frame is counted rather than written. */
  31. std::array<char, kTextCapacity> g_lastText{};
  32. std::uint32_t g_repeats{};
  33. std::uint32_t g_seen{};
  34. /** @param code Halt category from the assert site. @return True when the process must still die. */
  35. [[nodiscard]] bool unrecoverable(int code) noexcept {
  36. return code == kGraphicsHaltCategory;
  37. }
  38. /**
  39. * Counts one message and says whether this occurrence is written.
  40. * @param text Formatted assert message.
  41. * @param seen Receives the total assert count.
  42. * @param repeats Receives the length of the current run of this message.
  43. * @return True when the caller writes a log line.
  44. */
  45. [[nodiscard]] bool admit(const char* text, std::uint32_t& seen, std::uint32_t& repeats) noexcept {
  46. AcquireSRWLockExclusive(&g_lock);
  47. ++g_seen;
  48. if (std::strcmp(g_lastText.data(), text) == 0) {
  49. ++g_repeats;
  50. } else {
  51. const std::size_t length = std::strlen(text);
  52. const std::size_t copied = length < kTextCapacity ? length : kTextCapacity - 1;
  53. std::memcpy(g_lastText.data(), text, copied);
  54. g_lastText[copied] = '\0';
  55. g_repeats = 1;
  56. }
  57. seen = g_seen;
  58. repeats = g_repeats;
  59. ReleaseSRWLockExclusive(&g_lock);
  60. return repeats <= kRepeatHead || repeats % kRepeatStride == 0;
  61. }
  62. /**
  63. * Records one assert without letting logging failures reach the game.
  64. * @param code Halt category the assert site passes. Zero, one and six all occur.
  65. * @param text Already-formatted assert message.
  66. */
  67. void report(int code, const char* text) noexcept {
  68. std::uint32_t seen = 0;
  69. std::uint32_t repeats = 0;
  70. if (!admit(text, seen, repeats)) {
  71. return;
  72. }
  73. std::array<char, kLineCapacity> line{};
  74. const int written = std::snprintf(line.data(),
  75. line.size(),
  76. "ev=assert stage=hit n=%u repeat=%u arg0=%d text=%s",
  77. seen,
  78. repeats,
  79. code,
  80. text);
  81. if (written <= 0) {
  82. return;
  83. }
  84. const auto length = static_cast<std::size_t>(written) < line.size()
  85. ? static_cast<std::size_t>(written)
  86. : line.size() - 1;
  87. core::log::write(core::log::Channel::client, core::log::Level::error, {line.data(), length});
  88. }
  89. /**
  90. * Hands one assert back to the handler the game installed, which halts the process.
  91. * @param code Halt category from the assert site.
  92. * @param text Message this handler already formatted.
  93. */
  94. void chain(int code, const char* text) noexcept {
  95. if (!targets::game::assert_handler::is_resolved()) {
  96. return;
  97. }
  98. const targets::game::assert_handler::Targets& resolved = targets::game::assert_handler::get();
  99. if (resolved.original == nullptr) {
  100. return;
  101. }
  102. std::array<char, 64> line{};
  103. const int written =
  104. std::snprintf(line.data(), line.size(), "ev=assert stage=halt arg0=%d result=native", code);
  105. if (written > 0) {
  106. core::log::write(core::log::Channel::client,
  107. core::log::Level::error,
  108. {line.data(), static_cast<std::size_t>(written)});
  109. }
  110. // The original is printf-style too, so the text already built is passed as its one argument.
  111. reinterpret_cast<NativeHandler>(resolved.original)(code, "%s", text);
  112. }
  113. /**
  114. * Replacement assert handler. The sites call this slot as a printf-style callback. Returning
  115. * without calling the game's own handler is what makes the assert non-fatal. That handler builds
  116. * a crash ticket, shows a dialog and blocks.
  117. * @param code Halt category from the assert site.
  118. * @param format Native printf-style format string.
  119. */
  120. void __cdecl handler_body(int code, const char* format, ...) noexcept {
  121. std::array<char, kTextCapacity> text{};
  122. if (format == nullptr) {
  123. std::memcpy(text.data(), kUnformattable, sizeof kUnformattable);
  124. } else {
  125. va_list arguments;
  126. va_start(arguments, format);
  127. const int written = std::vsnprintf(text.data(), text.size(), format, arguments);
  128. va_end(arguments);
  129. if (written < 0) {
  130. std::memcpy(text.data(), kUnformattable, sizeof kUnformattable);
  131. }
  132. }
  133. text.back() = '\0';
  134. // An assert raised by our own logging must not re-enter this body.
  135. static thread_local bool inside = false;
  136. if (inside) {
  137. return;
  138. }
  139. inside = true;
  140. report(code, text.data());
  141. // Swallowing an unrecoverable halt leaves the process running with nothing to present. The
  142. // original handler runs so the process dies the way the game intends.
  143. if (unrecoverable(code)) {
  144. chain(code, text.data());
  145. }
  146. inside = false;
  147. }
  148. } // namespace
  149. /** @return Address of the internal assert handler body. */
  150. void* handler_entry_point() noexcept {
  151. return reinterpret_cast<void*>(&handler_body);
  152. }
  153. } // namespace sunrise::client::hooks::assert_handler