#include "Logging.hpp" #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN // clang-format off #include #include // 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(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(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(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( 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(code), 0, 16) .arg(reinterpret_cast(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(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(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(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(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(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