mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-01 15:24:13 +02:00
0646f9ca4f
Three weaknesses, all visible in ChuckNr11's report on #898 -- "the report appeared only once despite there being 10 or more crashes". One dump per run instead of one per install ------------------------------------------- crashDumpPath() was a single fixed crash_dump.log, and the handler opens it O_TRUNC, so each crash destroyed the evidence from the one before. Ten crashes left one dump. Dumps now go to a crashes/ directory named crash_<timestamp>_<pid>.log, and every pending one is offered together, newest first, with a banner saying how many there are. A crash that repeats is exactly the case where the earlier dumps matter, because the difference between them is the evidence. The name is built in normal context and handed to CrashHandler::install(), which copies it into a preallocated buffer as before -- the handler still writes to one fixed path, so its no-allocation invariant is untouched. The dump now says which signal fired ------------------------------------ The header is built once at install(), so every dump looked identical no matter what killed the process -- and SIGSEGV and SIGABRT point at very different bugs. Written with an async-signal-safe integer formatter into a stack buffer, since snprintf is not on the POSIX safe list. ...and where it was ------------------- The ring said what the program was doing; nothing said where it died. The dump now carries a backtrace. backtrace() is warmed once in install() so its first-call lazy resolution cannot allocate inside the handler, and backtrace_symbols_fd() writes straight to the fd -- unlike backtrace_symbols(), which mallocs and must never be used here. Guarded on __has_include(<execinfo.h>) so platforms without it are unaffected. QET's own frames currently resolve as offsets rather than names, since the binary does not export its dynamic symbols. They are still resolvable offline: the header records the exact git SHA. Building with -rdynamic would give names directly, but that is a build-flag decision for its own change. Deliberately unchanged: the four invariants in crashhandler.h. Nothing added here allocates, blocks, takes a lock, or swallows the crash. Verified: three consecutive SIGSEGVs now leave three separate dumps, each carrying "Signal: 11" and a backtrace with resolved Qt frames; launching afterwards offers all three in one dialog, newest first, and clears them once shown. ctest 8/8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
166 lines
6.7 KiB
C++
166 lines
6.7 KiB
C++
/*
|
|
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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#ifndef QETLOGGER_H
|
|
#define QETLOGGER_H
|
|
|
|
#include "logring.h"
|
|
|
|
#include <QFile>
|
|
#include <QMutex>
|
|
#include <QString>
|
|
#include <QtGlobal>
|
|
|
|
/**
|
|
@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 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<QByteArray> 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;
|
|
|
|
/// Raw contents of the pending crash dump, or an empty array if
|
|
/// there isn't one. Does not delete it -- call
|
|
/// clearPendingCrashDump() once it has been offered to the user.
|
|
QByteArray pendingCrashDumpContents() const;
|
|
|
|
/// Deletes the pending crash dump file. Call after the user has
|
|
/// been offered it (whether they chose to save it or not) so it
|
|
/// is never offered a second time.
|
|
void clearPendingCrashDump();
|
|
|
|
/// 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 "~".
|
|
/// 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;
|
|
QString crashDumpDir() const;
|
|
QString buildCrashDumpPath() const;
|
|
QStringList pendingCrashDumpFiles() 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
|