geopro/src/app/Logging.cpp

244 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "Logging.hpp"
#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfoList>
#include <QMutex>
#include <QStandardPaths>
#include <QtGlobal>
#include <cstdio>
#include <cstdlib>
#include <exception>
#ifdef Q_OS_WIN
// clang-format off
#include <windows.h>
#include <dbghelp.h> // MiniDumpWriteDump链接 Dbghelp
// clang-format on
#endif
namespace geopro::app {
namespace {
QFile g_logFile;
QMutex g_mutex;
QString g_logDir;
constexpr int kRetentionDays = 14; // 旧日志/dump 保留天数
const char* levelStr(QtMsgType t) {
switch (t) {
case QtDebugMsg: return "DEBUG";
case QtInfoMsg: return "INFO";
case QtWarningMsg: return "WARN";
case QtCriticalMsg: return "ERROR";
case QtFatalMsg: return "FATAL";
}
return "INFO";
}
// 线程安全写一行(落盘 + 同步到 stderr 便于开发期观察)。
void writeLine(const QString& line) {
const QByteArray utf8 = line.toUtf8();
QMutexLocker lock(&g_mutex);
if (g_logFile.isOpen()) {
g_logFile.write(utf8);
g_logFile.write("\n", 1);
g_logFile.flush();
}
std::fwrite(utf8.constData(), 1, static_cast<size_t>(utf8.size()), stderr);
std::fputc('\n', stderr);
}
void messageHandler(QtMsgType type, const QMessageLogContext& ctx, const QString& msg) {
const QString ts = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz"));
QString line = QStringLiteral("%1 [%2] %3").arg(ts, QString::fromLatin1(levelStr(type)), msg);
if (ctx.file && type >= QtWarningMsg) // 警告及以上带源码定位,便于排查
line += QStringLiteral(" (%1:%2)").arg(QString::fromUtf8(ctx.file)).arg(ctx.line);
writeLine(line);
if (type == QtFatalMsg) {
std::abort(); // qFatal 语义:记录后终止(触发崩溃捕获 → dump
}
}
void pruneOldFiles(const QString& dir) {
const QDateTime cutoff = QDateTime::currentDateTime().addDays(-kRetentionDays);
const QFileInfoList files =
QDir(dir).entryInfoList({QStringLiteral("geopro_*.log"), QStringLiteral("crash_*.dmp")},
QDir::Files);
for (const QFileInfo& fi : files)
if (fi.lastModified() < cutoff) QFile::remove(fi.absoluteFilePath());
}
#ifdef Q_OS_WIN
// 崩溃时(写完 dump 后)追加一行摘要。直接写已打开的 g_logFile进程将终止不抢 g_mutex —
// 否则崩溃发生在持锁线程时会死锁另开第二句柄又会因独占共享冲突失败。best-effort。
void appendCrashLine(const QString& line) {
const QByteArray utf8 = line.toUtf8();
if (g_logFile.isOpen()) {
g_logFile.write(utf8);
g_logFile.write("\n", 1);
g_logFile.flush();
}
std::fwrite(utf8.constData(), 1, static_cast<size_t>(utf8.size()), stderr);
std::fputc('\n', stderr);
}
LONG WINAPI crashFilter(EXCEPTION_POINTERS* info) {
const DWORD code = (info && info->ExceptionRecord) ? info->ExceptionRecord->ExceptionCode : 0;
const void* addr = (info && info->ExceptionRecord) ? info->ExceptionRecord->ExceptionAddress : nullptr;
const QString ts = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd_HHmmss_zzz"));
const QString dumpPath = g_logDir + QStringLiteral("/crash_") + ts + QStringLiteral(".dmp");
bool dumped = false;
HANDLE hFile = CreateFileW(reinterpret_cast<const wchar_t*>(dumpPath.utf16()), GENERIC_WRITE, 0,
nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION mei{};
mei.ThreadId = GetCurrentThreadId();
mei.ExceptionPointers = info;
mei.ClientPointers = FALSE;
const MINIDUMP_TYPE flags = static_cast<MINIDUMP_TYPE>(
MiniDumpWithDataSegs | MiniDumpWithThreadInfo | MiniDumpWithHandleData);
dumped = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, flags,
info ? &mei : nullptr, nullptr, nullptr);
CloseHandle(hFile);
}
appendCrashLine(QStringLiteral("%1 [FATAL] 崩溃 code=0x%2 addr=0x%3 dump=%4")
.arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")))
.arg(static_cast<quint32>(code), 0, 16)
.arg(reinterpret_cast<quintptr>(addr), 0, 16)
.arg(dumped ? dumpPath : QStringLiteral("写入失败")));
return EXCEPTION_EXECUTE_HANDLER; // 记录后终止进程
}
#endif // Q_OS_WIN
#ifdef Q_OS_WIN
// 向量化异常处理器:在 C++ 异常0xE06D7363**抛出瞬间**(栈未展开、进程健康、符号匹配)
// 捕获并符号化调用栈写日志。即使异常随后被 try/catch如顶层护栏吞掉也已留下抛点堆栈。
constexpr DWORD kCppExceptionCode = 0xE06D7363;
thread_local bool g_inVeh = false; // 防符号化过程自身再抛异常导致的重入
LONG WINAPI throwStackVeh(EXCEPTION_POINTERS* info) {
if (!info || !info->ExceptionRecord) return EXCEPTION_CONTINUE_SEARCH;
if (info->ExceptionRecord->ExceptionCode != kCppExceptionCode) return EXCEPTION_CONTINUE_SEARCH;
if (g_inVeh) return EXCEPTION_CONTINUE_SEARCH;
g_inVeh = true;
void* frames[32];
const USHORT n = CaptureStackBackTrace(1, 32, frames, nullptr);
const HANDLE proc = GetCurrentProcess();
SymRefreshModuleList(proc); // 确保已加载模块(含本 exe 的 PDB在符号表中
alignas(SYMBOL_INFOW) char symBuf[sizeof(SYMBOL_INFOW) + 1024] = {};
auto* sym = reinterpret_cast<SYMBOL_INFOW*>(symBuf);
sym->SizeOfStruct = sizeof(SYMBOL_INFOW);
sym->MaxNameLen = 1024 / sizeof(wchar_t) - 1;
appendCrashLine(QStringLiteral("%1 [THROW] C++ 异常抛出,调用栈:")
.arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz"))));
for (USHORT i = 0; i < n; ++i) {
const DWORD64 addr = reinterpret_cast<DWORD64>(frames[i]);
// 模块名 + RVA总是可得即使符号未解析也能离线用匹配 PDB 还原。
QString modRva;
HMODULE mod = nullptr;
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(addr), &mod) &&
mod) {
wchar_t mpath[MAX_PATH] = {};
GetModuleFileNameW(mod, mpath, MAX_PATH);
QString base = QString::fromWCharArray(mpath);
base = base.mid(base.lastIndexOf('\\') + 1);
modRva = QStringLiteral("%1+0x%2").arg(base).arg(addr - reinterpret_cast<DWORD64>(mod), 0, 16);
} else {
modRva = QStringLiteral("0x%1").arg(addr, 0, 16);
}
// 符号名宽字符UNICODE 下 SymFromAddrW + WCHAR Name
QString fn;
DWORD64 symDisp = 0;
if (SymFromAddrW(proc, addr, &symDisp, sym))
fn = QStringLiteral(" %1+0x%2").arg(QString::fromWCharArray(sym->Name)).arg(symDisp, 0, 16);
// 文件:行。
QString loc;
DWORD lineDisp = 0;
IMAGEHLP_LINEW64 line = {};
line.SizeOfStruct = sizeof(IMAGEHLP_LINEW64);
if (SymGetLineFromAddrW64(proc, addr, &lineDisp, &line))
loc = QStringLiteral(" (%1:%2)").arg(QString::fromWCharArray(line.FileName)).arg(line.LineNumber);
appendCrashLine(QStringLiteral(" #%1 %2%3%4").arg(i).arg(modRva).arg(fn).arg(loc));
}
g_inVeh = false;
return EXCEPTION_CONTINUE_SEARCH; // 不处理,交回正常流程(顶层护栏 try/catch 仍会接住)
}
#endif // Q_OS_WIN
void installCrashHandlers() {
#ifdef Q_OS_WIN
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); // 抑制系统崩溃弹窗
SetUnhandledExceptionFilter(crashFilter);
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_FAIL_CRITICAL_ERRORS);
// 显式把 exe 目录作为符号搜索路径geopro_desktop.pdb 与 exe 同目录、匹配);立即(非 deferred)加载。
const QByteArray searchPath = QCoreApplication::applicationDirPath().toLocal8Bit();
if (!SymInitialize(GetCurrentProcess(), searchPath.constData(), TRUE))
std::fprintf(stderr, "[Logging] SymInitialize 失败 err=%lu\n", GetLastError());
// 显式加载本 exe 的符号invade 偶尔不加载主模块的私有符号 → 内部函数无名)。
wchar_t selfPath[MAX_PATH] = {};
GetModuleFileNameW(nullptr, selfPath, MAX_PATH);
const DWORD64 selfBase =
SymLoadModuleExW(GetCurrentProcess(), nullptr, selfPath, nullptr,
reinterpret_cast<DWORD64>(GetModuleHandleW(nullptr)), 0, nullptr, 0);
if (selfBase == 0 && GetLastError() != ERROR_SUCCESS)
std::fprintf(stderr, "[Logging] SymLoadModuleExW 失败 err=%lu\n", GetLastError());
AddVectoredExceptionHandler(1, throwStackVeh); // first=1先于 SEH 链,抛出瞬间捕获
#endif
// 未捕获 C++ 异常(跨事件循环逃逸)→ 记录后终止。SEH 过滤器通常已先写 dump。
std::set_terminate([] {
QString what = QStringLiteral("(无异常信息)");
if (std::exception_ptr e = std::current_exception()) {
try {
std::rethrow_exception(e);
} catch (const std::exception& ex) {
what = QString::fromUtf8(ex.what());
} catch (...) {
what = QStringLiteral("(非 std::exception)");
}
}
writeLine(QStringLiteral("%1 [FATAL] std::terminate 未捕获异常: %2")
.arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")), what));
std::abort();
});
}
} // namespace
void initLogging() {
const QString base = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
g_logDir = base + QStringLiteral("/logs");
QDir().mkpath(g_logDir);
pruneOldFiles(g_logDir);
const QString fname = QStringLiteral("geopro_%1.log")
.arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd")));
g_logFile.setFileName(g_logDir + QStringLiteral("/") + fname);
const bool opened = g_logFile.open(QIODevice::Append | QIODevice::Text);
if (!opened) { // 打不开(权限等)则仅写 stderrwriteLine 已容错)
std::fprintf(stderr, "[Logging] 无法打开日志文件: %s\n", qUtf8Printable(g_logFile.fileName()));
} else if (g_logFile.size() == 0) {
g_logFile.write("\xEF\xBB\xBF", 3); // 新建文件写 UTF-8 BOM便于中文 Windows 记事本正确识别编码
}
qInstallMessageHandler(messageHandler);
installCrashHandlers();
qInfo("=== Geopro 启动 v%s | 日志目录 %s ===",
qUtf8Printable(QCoreApplication::applicationVersion().isEmpty()
? QStringLiteral("dev")
: QCoreApplication::applicationVersion()),
qUtf8Printable(g_logDir));
}
QString logDirectory() { return g_logDir; }
} // namespace geopro::app