/* Copyright 2006-2026 The QElectroTech Team This file is part of QElectroTech. QElectroTech is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. QElectroTech is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with QElectroTech. If not, see . */ #ifndef QETLOGGER_H #define QETLOGGER_H #include "logring.h" #include #include #include #include /** @brief The QetLogger class Rework of QET's diagnostic logging (discussion #644, steps 1-3): - Step 1: one file handle held open for the session under a mutex instead of opening/closing per message; the log path (including the date-stamped filename) is resolved exactly once, at init(), instead of being recomputed on every message -- a session that crosses midnight now stays in one file; retention now uses lastModified() instead of lastRead(); stderr and file output both use UTF-8 explicitly (previously stderr used the local 8-bit codec and the file's encoding silently differed between Qt5 and Qt6). - Step 2: the previously-unbounded daily file is now size-capped and rotated (kMaxFileBytes per file, kRotationKeep old files kept beyond the current one); each message is truncated to kMaxMessageBytes and control characters are escaped before being written, so one pathological caller can't blow the size budget or forge log lines; the log file is refused if it already exists as a symlink and is created owner-read/write only. - Step 3: every formatted line is also appended to an in-memory LogRing (see logring.h) -- always on, fixed capacity, allocation- free on the hot path. - Step 4: installCrashHandler() wires the ring up to CrashHandler (see crashhandler.h), so a SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL (or, on Windows, an unhandled structured exception) flushes the ring to a fixed crash-dump file before the process dies. - Step 5: hasPendingCrashDump()/pendingCrashDumpContents()/ clearPendingCrashDump() let startup code (see QETApp::checkBackupFiles()) notice and offer an unretrieved crash dump from the *previous* run; buildDiagnosticsReport() is the equivalent for a manual "save a report right now" action on the *current*, still-running session. Both go through redact() before ever reaching the user, since both are destined for a public bug tracker. Deliberately NOT included: log categories, a full session header beyond what the crash dump/report already carry, repeat collapsing, rate limiting. Those are listed in discussion #644 under "best practices worth building in", not part of the numbered steps. Escape hatch: if QET_LOG_DISABLE=1 is set in the environment at init() time, this class does nothing beyond a minimal, independent stderr passthrough -- no ring, no file, no rotation -- so a problem in this rework can be worked around without a rebuild. */ class QetLogger { public: static constexpr qint64 kMaxFileBytes = 2 * 1024 * 1024; // 2 MiB per file static constexpr int kRotationKeep = 4; // .1.log .. .4.log static constexpr int kMaxMessageBytes = 4096; // per-message truncation static constexpr int kMaxPendingCrashDumps = 10; // newest kept, rest pruned static QetLogger &instance(); /// Must be called exactly once, from main(), before /// qInstallMessageHandler(). Resolves the log directory and the /// session's log filename, and opens the file. void init(); /// Step 4: installs the crash handler (see crashhandler.h). Must /// be called after init() (the ring and the dump path must exist /// first) and, like init(), only once. void installCrashHandler(); /// The function installed via qInstallMessageHandler() forwards here. void handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg); /// Replaces the old delete_old_log_files(): same call shape, fixed /// to use lastModified() (not lastRead()) and to also match rotated /// file names. void pruneOldLogFiles(int days); /// Snapshot of the in-memory ring, oldest first. QVector ringSnapshot() const {return m_ring.snapshot();} // --- Step 5: getting the data back out ------------------------- /// True if a previous run's crash handler left an unretrieved /// dump behind. bool hasPendingCrashDump() const; /// Dumps left by previous runs, newest first, capped at /// kMaxPendingCrashDumps. This run's own dump path is never /// included. Take this list once and pass the same list to /// pendingCrashDumpContents() and clearPendingCrashDump(): that /// is what makes "only what was offered gets deleted" true, /// rather than re-reading the directory at each step and /// deleting a dump that arrived in between unseen. QStringList pendingCrashDumpFiles() const; /// Raw contents of `files`, newest first, concatenated and /// redacted. Deletes nothing -- pass the same list to /// clearPendingCrashDump() once it has been offered. QByteArray pendingCrashDumpContents(const QStringList &files) const; /// Deletes exactly `files`, nothing else. Call after the user /// has been offered them (whether they chose to save them or /// not) so they are never offered a second time. void clearPendingCrashDump(const QStringList &files); /// Builds a redacted diagnostics bundle from the *current* session /// (header + this session's log file so far) for the manual /// "Save report" action -- as opposed to pendingCrashDumpContents(), /// which is about a *previous*, already-terminated session. QByteArray buildDiagnosticsReport() const; /// Replaces occurrences of the user's home directory with "~", /// and an AppImage's per-run /tmp/.mount_XXXXXX prefix with /// "" -- the latter because backtrace_symbols_fd() /// writes absolute module paths into the dump. Applied to both /// the crash dump and buildDiagnosticsReport() before they are /// ever shown to the user, since both are destined for a public /// bug tracker. static QByteArray redact(const QByteArray &input); private: QetLogger() = default; QetLogger(const QetLogger &) = delete; bool ensureFileOpenLocked(); void rotateLocked(); void writeToFile(const QByteArray &line, QtMsgType type); QString rotatedPath(int index) const; /// Pure path getter: creates nothing. Callers that are about /// to write there call ensureCrashDumpDir() instead. QString crashDumpDir() const; QString ensureCrashDumpDir() const; QString buildCrashDumpPath() const; /// Moves a crash_dump.log left by a pre-#905 version into /// crashes/, so upgrading does not strand it unoffered. void migrateLegacyCrashDump() const; /// Keeps the newest kMaxPendingCrashDumps dumps and deletes /// the rest, so a crash loop cannot fill the log directory /// before anyone gets the chance to see a dialog. void pruneCrashDumps() const; QString currentLogFilePath() const; static QByteArray sanitize(const QByteArray &input); static QByteArray truncateMessage(const QByteArray &input, int max_bytes); static QByteArray formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg); bool m_disabled = false; QString m_log_dir; QString m_base_name; // e.g. "20260803", resolved once in init() /// This run's own dump path, fixed at installCrashHandler(): /// the handler writes here, and it is excluded when collecting /// dumps left by previous runs. QString m_crash_dump_path; QMutex m_file_mutex; QFile m_file; qint64 m_bytes_written_current_file = 0; bool m_file_output_ok = false; LogRing m_ring; }; #endif // QETLOGGER_H