244 lines
11 KiB
C++
244 lines
11 KiB
C++
#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) { // 打不开(权限等)则仅写 stderr(writeLine 已容错)
|
||
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
|