yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
crash_handler.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <chrono>
5#include <csignal>
6#include <cstdio>
7#include <cstring>
8#include <ctime>
9#include <fstream>
10#include <iomanip>
11#include <iterator>
12#include <sstream>
13#include <string_view>
14#include <system_error>
15#include <vector>
16
17#include "absl/debugging/failure_signal_handler.h"
18#include "absl/debugging/symbolize.h"
19#include "util/platform_paths.h"
20
21#ifdef _WIN32
22#include <fcntl.h>
23#include <io.h>
24#include <sys/stat.h>
25#define STDERR_FILENO _fileno(stderr)
26#define write _write
27#define close _close
28#else
29#include <fcntl.h>
30#include <sys/stat.h>
31#include <unistd.h>
32#endif
33
34namespace yaze {
35namespace util {
36
37namespace {
38
39using PathChar = std::filesystem::path::value_type;
40
41constexpr size_t kMaxCrashLogPathChars = 4096;
42constexpr size_t kMaxCrashLogHeaderBytes = 1024;
43
44constexpr std::string_view kHeaderBanner = "=== YAZE Crash Report ===\n";
45constexpr std::string_view kHeaderEnd = "========================\n\n";
46
47// Filled by PrepareCrashLog before the handler is installed and only read in
48// the signal path, which must not allocate.
52volatile sig_atomic_t g_crash_log_open_attempted = 0;
53
54} // namespace
55
56// Static member definitions
57std::string CrashHandler::version_;
58std::filesystem::path CrashHandler::crash_log_path_;
60
62 if (crash_log_fd_ >= 0 || g_crash_log_open_attempted ||
63 g_crash_log_path[0] == PathChar{0}) {
64 return;
65 }
66 g_crash_log_open_attempted = 1;
67#ifdef _WIN32
69 _wopen(g_crash_log_path, _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY,
70 _S_IREAD | _S_IWRITE);
71#else
73 open(g_crash_log_path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC,
74 S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
75#endif
76 if (crash_log_fd_ >= 0 && g_crash_log_header_len > 0) {
77 write(crash_log_fd_, g_crash_log_header, g_crash_log_header_len);
78 }
79}
80
81void CrashHandler::CrashLogWriter(const char* data) {
82 // Prevent re-entrancy - if we crash while handling a crash, just abort
83 static volatile sig_atomic_t in_crash_handler = 0;
84 if (in_crash_handler) {
85 // Already in crash handler - abort to prevent infinite loop
86 _Exit(1);
87 }
88 in_crash_handler = 1;
89
90 if (data == nullptr) {
91 in_crash_handler = 0;
92 return;
93 }
94
95 // Calculate length manually (strlen might crash)
96 size_t len = 0;
97 while (len < 4096 && data[len] != '\0') { // Cap at 4KB to prevent runaway
98 ++len;
99 }
100
102 if (crash_log_fd_ >= 0) {
103 // Write to crash log file (ignore errors - we're crashing anyway)
104 write(crash_log_fd_, data, len);
105 }
106
107 // Also write to stderr for immediate visibility
108 write(STDERR_FILENO, data, len);
109
110 in_crash_handler = 0;
111}
112
113void CrashHandler::PrepareCrashLog(const std::string& version,
114 const std::filesystem::path& directory) {
115 version_ = version;
116
117 auto now = std::chrono::system_clock::now();
118 auto time_t_now = std::chrono::system_clock::to_time_t(now);
119 std::tm* tm_now = std::localtime(&time_t_now);
120
121 std::ostringstream filename;
122 filename << "crash_" << std::put_time(tm_now, "%Y%m%d_%H%M%S") << ".log";
123 crash_log_path_ = directory / filename.str();
124
125 std::error_code ec;
126 std::filesystem::create_directories(directory, ec);
127
128 const auto& native_path = crash_log_path_.native();
129 if (native_path.size() < kMaxCrashLogPathChars) {
130 std::copy(native_path.begin(), native_path.end(), g_crash_log_path);
131 g_crash_log_path[native_path.size()] = PathChar{0};
132 } else {
133 // Too long for the static buffer: crashes still reach stderr.
134 g_crash_log_path[0] = PathChar{0};
135 }
136
137 std::ostringstream header;
138 header << kHeaderBanner;
139 header << "Version: " << version_ << "\n";
140 header << "Session start: " << std::put_time(tm_now, "%Y-%m-%d %H:%M:%S")
141 << "\n";
142 header << "Platform: ";
143#if defined(_WIN32)
144 header << "Windows";
145#elif defined(__APPLE__)
146 header << "macOS";
147#elif defined(__linux__)
148 header << "Linux";
149#else
150 header << "Unknown";
151#endif
152 header << "\n";
153 header << kHeaderEnd;
154
155 const std::string header_str = header.str();
156 g_crash_log_header_len =
157 std::min(header_str.size(), sizeof(g_crash_log_header));
158 std::memcpy(g_crash_log_header, header_str.data(), g_crash_log_header_len);
159 g_crash_log_open_attempted = 0;
160}
161
162void CrashHandler::Initialize(const std::string& version) {
164
165 // Configure absl failure signal handler
166 absl::FailureSignalHandlerOptions options;
167 options.symbolize_stacktrace = true;
168 options.use_alternate_stack = true;
169 options.call_previous_handler = true;
170
171 // Write to both the crash log (created on first write) and stderr
172 options.writerfn = CrashLogWriter;
173
174 absl::InstallFailureSignalHandler(options);
175}
176
178 const std::string& version, const std::filesystem::path& directory) {
180 PrepareCrashLog(version, directory);
181}
182
184 CrashLogWriter(data);
185}
186
188 return crash_log_path_;
189}
190
192 if (crash_log_fd_ >= 0) {
193 close(crash_log_fd_);
194 crash_log_fd_ = -1;
195 }
196 g_crash_log_path[0] = PathChar{0};
197 g_crash_log_header_len = 0;
198 g_crash_log_open_attempted = 0;
199 crash_log_path_.clear();
200}
201
202bool CrashHandler::IsHeaderOnlyCrashLog(const std::filesystem::path& path) {
203 std::error_code ec;
204 const auto size = std::filesystem::file_size(path, ec);
205 if (ec || size > kMaxCrashLogHeaderBytes * 2) {
206 return false;
207 }
208 std::ifstream in(path, std::ios::binary);
209 if (!in) {
210 return false;
211 }
212 std::string contents((std::istreambuf_iterator<char>(in)),
213 std::istreambuf_iterator<char>());
214 // Older Windows builds opened the log in text mode and wrote CRLF.
215 contents.erase(std::remove(contents.begin(), contents.end(), '\r'),
216 contents.end());
217 const std::string_view view(contents);
218 return view.starts_with(kHeaderBanner) && view.ends_with(kHeaderEnd);
219}
220
221std::filesystem::path CrashHandler::GetCrashLogDirectory() {
222 auto app_data_result = PlatformPaths::GetAppDataSubdirectory("crash_logs");
223 if (app_data_result.ok()) {
224 return *app_data_result;
225 }
226
227 // Fallback to temp directory
228 auto temp_result = PlatformPaths::GetTempDirectory();
229 if (temp_result.ok()) {
230 return *temp_result / "crash_logs";
231 }
232
233 // Last resort: current directory
234 return std::filesystem::current_path() / "crash_logs";
235}
236
237std::filesystem::path CrashHandler::GetMostRecentCrashLog() {
238 auto crash_dir = GetCrashLogDirectory();
239
240 if (!std::filesystem::exists(crash_dir)) {
241 return {};
242 }
243
244 std::vector<std::filesystem::path> logs;
245 for (const auto& entry : std::filesystem::directory_iterator(crash_dir)) {
246 if (entry.path().extension() == ".log" &&
247 entry.path().filename().string().starts_with("crash_") &&
248 !IsHeaderOnlyCrashLog(entry.path())) {
249 logs.push_back(entry.path());
250 }
251 }
252
253 if (logs.empty()) {
254 return {};
255 }
256
257 // Sort by modification time, newest first
258 std::sort(logs.begin(), logs.end(),
259 [](const std::filesystem::path& a, const std::filesystem::path& b) {
260 return std::filesystem::last_write_time(a) >
261 std::filesystem::last_write_time(b);
262 });
263
264 return logs.front();
265}
266
268 auto crash_dir = GetCrashLogDirectory();
269 auto ack_file = crash_dir / ".acknowledged";
270
271 auto most_recent = GetMostRecentCrashLog();
272 if (most_recent.empty()) {
273 return false;
274 }
275
276 // Check if there's an acknowledgment file newer than the crash log
277 if (std::filesystem::exists(ack_file)) {
278 auto ack_time = std::filesystem::last_write_time(ack_file);
279 auto crash_time = std::filesystem::last_write_time(most_recent);
280 return crash_time > ack_time;
281 }
282
283 return true;
284}
285
287 auto crash_dir = GetCrashLogDirectory();
288 auto ack_file = crash_dir / ".acknowledged";
289
290 // Create/update the acknowledgment file
291 std::ofstream ack(ack_file);
292 ack << "acknowledged";
293}
294
295void CrashHandler::CleanupOldLogs(int keep_count) {
296 auto crash_dir = GetCrashLogDirectory();
297
298 if (!std::filesystem::exists(crash_dir)) {
299 return;
300 }
301
302 std::vector<std::filesystem::path> logs;
303 std::vector<std::filesystem::path> header_only_logs;
304 for (const auto& entry : std::filesystem::directory_iterator(crash_dir)) {
305 if (entry.path().extension() == ".log" &&
306 entry.path().filename().string().starts_with("crash_")) {
307 if (IsHeaderOnlyCrashLog(entry.path())) {
308 header_only_logs.push_back(entry.path());
309 } else {
310 logs.push_back(entry.path());
311 }
312 }
313 }
314
315 for (const auto& path : header_only_logs) {
316 std::error_code ec;
317 std::filesystem::remove(path, ec);
318 }
319
320 if (static_cast<int>(logs.size()) <= keep_count) {
321 return;
322 }
323
324 // Sort by modification time, newest first
325 std::sort(logs.begin(), logs.end(),
326 [](const std::filesystem::path& a, const std::filesystem::path& b) {
327 return std::filesystem::last_write_time(a) >
328 std::filesystem::last_write_time(b);
329 });
330
331 // Remove older logs
332 for (size_t i = keep_count; i < logs.size(); ++i) {
333 std::error_code ec;
334 std::filesystem::remove(logs[i], ec);
335 }
336}
337
338} // namespace util
339} // namespace yaze
static void CleanupOldLogs(int keep_count=5)
Clean up old crash logs, keeping only the most recent N logs.
static bool IsHeaderOnlyCrashLog(const std::filesystem::path &path)
True if the file holds only the report header, with nothing written by the signal handler after it.
static void OpenCrashLogFromSignalHandler()
static void CrashLogWriter(const char *data)
static void Initialize(const std::string &version)
Initialize the crash handler for the application.
static std::filesystem::path GetCrashLogDirectory()
Get the path where crash logs are stored.
static bool HasUnacknowledgedCrashLog()
Check if there's a crash log from a previous session.
static std::filesystem::path PendingCrashLogPathForTesting()
static void AcknowledgeCrashLog()
Mark the current crash log as acknowledged.
static std::string version_
static void PrepareCrashLog(const std::string &version, const std::filesystem::path &directory)
static std::filesystem::path GetMostRecentCrashLog()
Get the path to the most recent crash log, if any.
static std::filesystem::path crash_log_path_
static void WriteCrashLogForTesting(const char *data)
static void PrepareCrashLogForTesting(const std::string &version, const std::filesystem::path &directory)
static absl::StatusOr< std::filesystem::path > GetTempDirectory()
Get a temporary directory for the application.
static absl::StatusOr< std::filesystem::path > GetAppDataSubdirectory(const std::string &subdir)
Get a subdirectory within the app data folder.
char g_crash_log_header[kMaxCrashLogHeaderBytes]
PathChar g_crash_log_path[kMaxCrashLogPathChars]
std::filesystem::path::value_type PathChar