Merge pull request #647 from ispyisail/feature-diagnostic-logging-crash

Add crash-time ring flush and diagnostics export UI (discussion #644, steps 4-5)
This commit is contained in:
Laurent Trinques
2026-08-06 13:26:14 +02:00
committed by GitHub
15 changed files with 758 additions and 74 deletions
+4
View File
@@ -116,10 +116,14 @@ set(QET_RES_FILES
set(QET_SRC_FILES
${QET_DIR}/sources/cli_export.cpp
${QET_DIR}/sources/cli_export.h
${QET_DIR}/sources/logging/crashhandler.cpp
${QET_DIR}/sources/logging/crashhandler.h
${QET_DIR}/sources/logging/logring.cpp
${QET_DIR}/sources/logging/logring.h
${QET_DIR}/sources/logging/qetlogger.cpp
${QET_DIR}/sources/logging/qetlogger.h
${QET_DIR}/sources/logging/ui/diagnosticsreportdialog.cpp
${QET_DIR}/sources/logging/ui/diagnosticsreportdialog.h
${QET_DIR}/sources/pdf_links.cpp
${QET_DIR}/sources/pdf_links.h
${QET_DIR}/sources/import/edz/edzarchive.cpp
+4 -2
View File
@@ -174,7 +174,8 @@ HEADERS += $$files(sources/*.h) \
$$files(sources/svg/*.h) \
$$files(sources/import/edz/*.h) \
$$files(sources/import/edz/lzma/*.h) \
$$files(sources/logging/*.h)
$$files(sources/logging/*.h) \
$$files(sources/logging/ui/*.h)
SOURCES += $$files(sources/*.cpp) \
$$files(sources/editor/*.cpp) \
@@ -221,7 +222,8 @@ SOURCES += $$files(sources/*.cpp) \
$$files(sources/svg/*.cpp) \
$$files(sources/import/edz/*.cpp) \
$$files(sources/import/edz/lzma/*.c) \
$$files(sources/logging/*.cpp)
$$files(sources/logging/*.cpp) \
$$files(sources/logging/ui/*.cpp)
# Needed for use promote QTreeWidget in terminalstripeditor.ui
+171
View File
@@ -0,0 +1,171 @@
/*
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/>.
*/
#include "crashhandler.h"
#include "logring.h"
#include "../qetversion.h"
#include <QByteArray>
#include <QSysInfo>
#include <atomic>
#include <cstring>
#ifdef Q_OS_WIN
#include <fcntl.h>
#include <io.h>
#include <share.h>
#include <sys/stat.h>
#include <windows.h>
#else
#include <cerrno>
#include <csignal>
#include <fcntl.h>
#include <unistd.h>
#endif
namespace {
// Everything the handler touches is preallocated here and filled in by
// install() (normal context, runs once at startup) -- nothing under the
// actual signal/exception path may allocate or touch QString/Qt.
const LogRing *g_ring = nullptr;
char g_dump_path[1024] = {};
char g_header[1024] = {};
int g_header_len = 0;
// Guards against two threads crashing at once, or the handler itself
// faulting while dumping: only the first crash writes a dump. See
// crashhandler.h invariant 4.
std::atomic<bool> g_already_dumped{false};
#ifndef Q_OS_WIN
// A stack-overflow SIGSEGV leaves no usable stack for a handler to run
// on at all, hence the alternate signal stack (invariant: sized well
// above any known SIGSTKSZ so this doesn't depend on
// sysconf(_SC_SIGSTKSZ), which some libc versions require at runtime
// rather than offering as a compile-time constant).
char g_altstack[65536];
const int kHandledSignals[] = {SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL};
void restoreDefaultAndReraise(int sig)
{
struct sigaction sa {};
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(sig, &sa, nullptr);
raise(sig);
}
void signalHandler(int sig)
{
if (g_already_dumped.exchange(true, std::memory_order_acq_rel)) {
// Not the first crash (concurrent fault on another thread, or
// this handler faulting while dumping): skip straight to
// restore-and-re-raise rather than risk a second, interleaved
// write to the same file.
restoreDefaultAndReraise(sig);
return;
}
// open/write/close are all on the POSIX async-signal-safe function
// list; nothing else is called here.
const int fd = ::open(g_dump_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd >= 0) {
if (g_header_len > 0) {
::write(fd, g_header, static_cast<size_t>(g_header_len));
}
if (g_ring) {
g_ring->dumpToFd(fd);
}
::close(fd);
}
restoreDefaultAndReraise(sig);
}
#else // Q_OS_WIN
LONG WINAPI windowsExceptionFilter(EXCEPTION_POINTERS *)
{
bool expected = false;
if (!g_already_dumped.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
return EXCEPTION_CONTINUE_SEARCH;
}
int fd = -1;
errno_t err = _sopen_s(&fd, g_dump_path,
_O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY,
_SH_DENYWR, _S_IREAD | _S_IWRITE);
if (err == 0 && fd >= 0) {
if (g_header_len > 0) {
_write(fd, g_header, g_header_len);
}
if (g_ring) {
g_ring->dumpToFd(fd);
}
_close(fd);
}
// Do not suppress Windows Error Reporting / an attached debugger --
// same invariant as re-raising on POSIX (see crashhandler.h,
// invariant 3).
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
} // namespace
void CrashHandler::install(const LogRing *ring, const QString &dump_path)
{
g_ring = ring;
const QByteArray path_utf8 = dump_path.toUtf8();
std::strncpy(g_dump_path, path_utf8.constData(), sizeof(g_dump_path) - 1);
const QByteArray header = QByteArray("QET crash dump\n")
+ "Version: " + QetVersion::displayedVersion().toUtf8() + "\n"
+ "Git: " GIT_COMMIT_SHA "\n"
+ "OS: " + QSysInfo::prettyProductName().toUtf8() + " (" + QSysInfo::currentCpuArchitecture().toUtf8() + ")\n"
+ "Qt: " QT_VERSION_STR "\n"
+ "---\n";
g_header_len = qMin<int>(header.size(), static_cast<int>(sizeof(g_header)) - 1);
std::memcpy(g_header, header.constData(), static_cast<size_t>(g_header_len));
#ifdef Q_OS_WIN
SetUnhandledExceptionFilter(windowsExceptionFilter);
#else
stack_t ss;
ss.ss_sp = g_altstack;
ss.ss_size = sizeof(g_altstack);
ss.ss_flags = 0;
sigaltstack(&ss, nullptr);
struct sigaction sa {};
sa.sa_handler = signalHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_ONSTACK;
for (int sig : kHandledSignals) {
sigaction(sig, &sa, nullptr);
}
#endif
}
+85
View File
@@ -0,0 +1,85 @@
/*
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 CRASHHANDLER_H
#define CRASHHANDLER_H
#include <QString>
class LogRing;
/**
@brief The CrashHandler class
Discussion #644, step 4: on a fatal crash, flush the in-memory
LogRing to a fixed file before the process dies, so the last N log
lines leading up to the crash survive it -- today they only exist in
memory and are lost with the process.
This is the highest-risk piece of the whole logging rework (the
discussion's own words: "lands last, behind its own switch"), so its
invariants are worth restating plainly:
1. The handler must never block. It takes no locks -- LogRing itself
is lock-free for exactly this reason (see logring.h). A handler
that can hang is worse than no handler: it turns a clean crash
(which at least produces a core dump) into a hung process that has
to be force-killed, producing neither a core dump nor a ring dump.
2. The handler must never allocate. Under heap corruption -- a
plausible *cause* of the very crash being handled -- malloc may
itself deadlock or fault. Every buffer this code touches at crash
time (the dump path, the header, the ring's own storage) is
preallocated by install(), which runs once at startup in normal
(non-signal) context.
3. The handler must not swallow the crash. After writing the dump it
restores the default disposition for the signal and re-raises, so
the OS still produces a core dump (POSIX) / Windows Error
Reporting still sees the exception. A handler that "fixed" the
crash by not re-raising would destroy the post-mortem evidence a
core dump provides.
4. Only the *first* crash writes a dump. An atomic test-and-set
guards against two threads faulting simultaneously (or the handler
itself faulting while dumping) producing an interleaved or
truncated file; every crash after the first goes straight to
restore-and-re-raise.
Tested in this environment: POSIX/Linux only (sigaction, sigaltstack,
SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL). The Windows path
(SetUnhandledExceptionFilter) and macOS-specific behaviour (signal
handling itself is POSIX and shares the Linux code path, but sandbox
profiles can affect where the dump file may be written) are
implemented per the discussion's guidance but could not be exercised
here -- there is no Windows or macOS build available in this sandbox.
Please sanity-check both before relying on them in the field.
*/
class CrashHandler
{
public:
/// Installs the crash handler. Must be called from normal
/// (non-signal) startup code, after the LogRing it will dump
/// exists, and only once. `ring` must outlive the process (in
/// practice: the LogRing owned by QetLogger's function-local
/// static instance, which is never destroyed before exit).
/// `dump_path` is resolved and copied into a fixed-size internal
/// buffer here; nothing under the actual signal/exception path
/// touches QString.
static void install(const LogRing *ring, const QString &dump_path);
private:
CrashHandler() = delete;
};
#endif // CRASHHANDLER_H
+99 -38
View File
@@ -17,75 +17,136 @@
*/
#include "logring.h"
#include <algorithm>
#include <cstring>
/**
@brief LogRing::LogRing
Preallocates all kCapacityEntries slots up front -- the only
allocation this class ever does.
*/
LogRing::LogRing()
{
m_entries.resize(kCapacityEntries);
}
#ifdef Q_OS_WIN
#include <io.h>
#else
#include <cerrno>
#include <unistd.h>
#endif
static_assert(std::atomic<int>::is_always_lock_free,
"LogRing::Entry::length must be a lock-free atomic<int> -- "
"dumpToFd() reads it from a signal handler and must never block.");
namespace {
/**
@brief LogRing::append
@param line one already-formatted log line (no further formatting
is done here). Truncated to kEntryBytes - 1 bytes if longer, with a
trailing marker, so the stored entry is always a complete,
independently-readable line.
@brief writeAllSignalSafe
Loops until length bytes have been written to fd or an unrecoverable
error occurs. write(2) may write fewer bytes than requested and may
return EINTR -- both are *more* likely from inside a signal handler
than in normal code, so a single write() call is not enough here.
Async-signal-safe: only calls write(2)/errno, nothing else.
*/
void LogRing::append(const QByteArray &line)
void writeAllSignalSafe(int fd, const char *data, int length) noexcept
{
int remaining = length;
const char *p = data;
while (remaining > 0) {
#ifdef Q_OS_WIN
const int n = _write(fd, p, static_cast<unsigned int>(remaining));
if (n <= 0) {
return;
}
#else
const ssize_t n = ::write(fd, p, static_cast<size_t>(remaining));
if (n < 0) {
if (errno == EINTR) {
continue;
}
return; // unrecoverable -- give up silently, never block/throw
}
if (n == 0) {
return;
}
#endif
p += n;
remaining -= static_cast<int>(n);
}
}
} // namespace
LogRing::LogRing() :
// The sized constructor value-initialises each Entry in place; unlike
// resize(), it doesn't require Entry to be move/copy-constructible,
// which std::atomic<int> deliberately never is. The only allocation
// this class ever does.
m_entries(kCapacityEntries)
{
}
void LogRing::append(const QByteArray &line) noexcept
{
static const char kMarker[] = "...[ring-truncated]\n";
const int marker_len = static_cast<int>(sizeof(kMarker)) - 1;
QMutexLocker locker(&m_mutex);
const quint64 idx = m_write_cursor.fetch_add(1, std::memory_order_relaxed);
Entry &slot = m_entries[static_cast<size_t>(idx % static_cast<quint64>(kCapacityEntries))];
Entry &slot = m_entries[static_cast<size_t>(m_next_index)];
// Zero the length first so a concurrent reader landing on this exact
// slot mid-copy sees "not ready" rather than the previous lap's
// (now-being-overwritten) content at a stale length.
slot.length.store(0, std::memory_order_relaxed);
int len;
if (line.size() < kEntryBytes) {
std::memcpy(slot.data, line.constData(), static_cast<size_t>(line.size()));
slot.length = line.size();
len = line.size();
} else {
const int keep = kEntryBytes - marker_len;
std::memcpy(slot.data, line.constData(), static_cast<size_t>(keep));
std::memcpy(slot.data + keep, kMarker, static_cast<size_t>(marker_len));
slot.length = kEntryBytes;
len = kEntryBytes;
}
m_next_index = (m_next_index + 1) % kCapacityEntries;
if (m_count < kCapacityEntries) {
++m_count;
}
slot.length.store(len, std::memory_order_release);
}
/**
@brief LogRing::snapshot
@return the entries currently held, oldest first. Safe to call from
normal (non-signal) code only.
*/
QVector<QByteArray> LogRing::snapshot() const
{
QMutexLocker locker(&m_mutex);
const quint64 cursor = m_write_cursor.load(std::memory_order_acquire);
const quint64 cap = static_cast<quint64>(kCapacityEntries);
const quint64 count = (cursor < cap) ? cursor : cap;
const quint64 start = (cursor < cap) ? 0 : (cursor - cap);
QVector<QByteArray> result;
result.reserve(m_count);
result.reserve(static_cast<int>(count));
const int start = (m_count < kCapacityEntries) ? 0 : m_next_index;
for (int i = 0; i < m_count; ++i) {
const Entry &slot = m_entries[static_cast<size_t>((start + i) % kCapacityEntries)];
result.append(QByteArray(slot.data, slot.length));
for (quint64 i = 0; i < count; ++i) {
const Entry &slot = m_entries[static_cast<size_t>((start + i) % cap)];
const int len = slot.length.load(std::memory_order_acquire);
if (len > 0) {
result.append(QByteArray(slot.data, len));
}
}
return result;
}
void LogRing::dumpToFd(int fd) const noexcept
{
const quint64 cursor = m_write_cursor.load(std::memory_order_acquire);
const quint64 cap = static_cast<quint64>(kCapacityEntries);
const quint64 count = (cursor < cap) ? cursor : cap;
const quint64 start = (cursor < cap) ? 0 : (cursor - cap);
for (quint64 i = 0; i < count; ++i) {
const Entry &slot = m_entries[static_cast<size_t>((start + i) % cap)];
const int len = slot.length.load(std::memory_order_acquire);
if (len > 0) {
writeAllSignalSafe(fd, slot.data, len);
}
}
}
void LogRing::clear()
{
QMutexLocker locker(&m_mutex);
m_next_index = 0;
m_count = 0;
m_write_cursor.store(0, std::memory_order_relaxed);
for (auto &entry : m_entries) {
entry.length.store(0, std::memory_order_relaxed);
}
}
+33 -25
View File
@@ -19,31 +19,33 @@
#define LOGRING_H
#include <QByteArray>
#include <QMutex>
#include <QVector>
#include <atomic>
#include <vector>
/**
@brief The LogRing class
Fixed-capacity, always-on in-memory ring of the most recent log
lines. Discussion #644 (step 3): the ring exists as forward-compatible
infrastructure for a future crash-flush (step 4, not implemented
here) as well as an on-demand "what just happened" snapshot, so its
entries are stored pre-formatted as plain bytes in storage
preallocated once at construction -- append() never allocates.
lines, preallocated once at construction -- append() never
allocates.
Entries are fixed-size slots rather than a byte-packed ring: with
kCapacityEntries * kEntryBytes chosen to land exactly on the 2 MiB
budget, this keeps wraparound trivial (whole-slot overwrite, so a
slot is always either fully the old entry or fully the new one --
no torn entries) at the cost of truncating any single line to
kEntryBytes, independently of the logger's own (larger) per-message
truncation.
Lock-free by construction, not just "thread-safe": step 4 (see
crashhandler.h) reads this ring from inside a POSIX signal handler,
where taking any lock is unsafe -- if the crashing thread happens to
be the one that already holds it (or any other thread does and never
gets scheduled again), the handler hangs forever, and you lose both
the ring dump *and* the core dump. So there is no mutex here at all:
append() claims a slot with a single atomic fetch-add, and
dumpToFd()/snapshot() read the preallocated entries directly.
Thread-safe via a plain QMutex. This is *not* the lock-free design
discussion #644 specifies for a signal-handler crash path (step 4)
-- no signal handler is installed by this code, so nothing calls
into the ring from inside a signal context.
Accepted tradeoff: if dumpToFd() runs while another thread is
mid-append into the exact slot being read (only possible in the
crash-handler case, and only for at most one slot), that one entry
may be read torn -- part old content, part new. Every other entry is
unaffected. This is deliberate: the alternative (a seqlock or similar
to detect and retry torn reads) adds real complexity for a window
that, per discussion #644, is not worth trading "the handler must
never block" against.
*/
class LogRing
{
@@ -54,24 +56,30 @@ class LogRing
LogRing();
/// Append one already-formatted, already-truncated log line.
/// Bytes beyond kEntryBytes - 1 are dropped with a truncation marker.
void append(const QByteArray &line);
/// Bytes beyond kEntryBytes are dropped with a truncation marker.
/// Never allocates, never blocks. Safe to call from any normal
/// (non-signal) thread concurrently.
void append(const QByteArray &line) noexcept;
/// Snapshot of the entries currently held, oldest first.
/// Snapshot of the entries currently held, oldest first. Normal
/// (non-signal) context only.
QVector<QByteArray> snapshot() const;
/// Async-signal-safe: writes every entry currently held to fd via
/// write(2) only -- no allocation, no Qt, no locks. May write a
/// torn entry under the rare race described above; never blocks.
void dumpToFd(int fd) const noexcept;
void clear();
private:
struct Entry {
char data[kEntryBytes] = {};
int length = 0;
char data[kEntryBytes];
std::atomic<int> length{0}; // 0 = not yet written this lap
};
mutable QMutex m_mutex;
std::vector<Entry> m_entries; // preallocated once, capacity fixed
int m_next_index = 0;
int m_count = 0;
std::atomic<quint64> m_write_cursor{0}; // monotonically increasing
};
#endif // LOGRING_H
+98 -2
View File
@@ -17,11 +17,14 @@
*/
#include "qetlogger.h"
#include "crashhandler.h"
#include "../qetapp.h"
#include "../qetversion.h"
#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QSysInfo>
#include <cstdio>
namespace {
@@ -96,6 +99,24 @@ void QetLogger::init()
m_file_output_ok = ensureFileOpenLocked();
}
void QetLogger::installCrashHandler()
{
if (m_disabled) {
return;
}
CrashHandler::install(&m_ring, crashDumpPath());
}
QString QetLogger::crashDumpPath() const
{
return m_log_dir % QStringLiteral("/crash_dump.log");
}
QString QetLogger::currentLogFilePath() const
{
return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log");
}
/**
@brief QetLogger::ensureFileOpenLocked
Caller must hold m_file_mutex. Opens the current session's log file
@@ -110,7 +131,7 @@ bool QetLogger::ensureFileOpenLocked()
QDir().mkpath(m_log_dir);
const QString path = m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log");
const QString path = currentLogFilePath();
const QFileInfo info(path);
if (info.exists() && info.isSymLink()) {
@@ -143,7 +164,7 @@ void QetLogger::rotateLocked()
{
m_file.close();
const QString base_path = m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log");
const QString base_path = currentLogFilePath();
for (int i = kRotationKeep; i >= 1; --i) {
const QString from = (i == 1) ? base_path : rotatedPath(i - 1);
@@ -333,3 +354,78 @@ void QetLogger::pruneOldLogFiles(int days)
}
}
}
// --- Step 5: getting the data back out ----------------------------------
bool QetLogger::hasPendingCrashDump() const
{
if (m_disabled) {
return false;
}
const QFileInfo info(crashDumpPath());
return info.exists() && info.isFile() && info.size() > 0;
}
QByteArray QetLogger::pendingCrashDumpContents() const
{
QFile file(crashDumpPath());
if (!file.open(QIODevice::ReadOnly)) {
return QByteArray();
}
return redact(file.readAll());
}
void QetLogger::clearPendingCrashDump()
{
QFile::remove(crashDumpPath());
}
QByteArray QetLogger::buildDiagnosticsReport() const
{
QByteArray header;
header += "QElectroTech diagnostics report\n";
header += "Generated: " % QDateTime::currentDateTime().toString(Qt::ISODate) % "\n";
header += "Version: " % QetVersion::displayedVersion() % "\n";
header += "Git: " GIT_COMMIT_SHA "\n";
header += "OS: " % QSysInfo::prettyProductName() % " (" % QSysInfo::currentCpuArchitecture() % ")\n";
header += "Qt: " QT_VERSION_STR "\n";
header += "---\n";
QByteArray body;
QFile file(currentLogFilePath());
if (file.open(QIODevice::ReadOnly)) {
body = file.readAll();
} else {
// Fall back to the in-memory ring if the file itself can't be
// read (e.g. file output already failed this session).
for (const QByteArray &line : m_ring.snapshot()) {
body += line;
}
}
return redact(header + body);
}
/**
@brief QetLogger::redact
Replaces the user's home directory with "~" wherever it appears.
Applied before a crash dump or a diagnostics report is ever shown to
the user: both are destined to be attached to a public bug tracker,
and an absolute path under the home directory leaks the account name
(discussion #644's privacy section: "/home/laurent/... leaks a
username"). This is the one redaction implemented here; the
discussion's fancier "optionally redact project filenames too" is
not attempted -- reliably telling a project path apart from
arbitrary log text is a much fuzzier problem than a literal prefix
match against a known directory.
*/
QByteArray QetLogger::redact(const QByteArray &input)
{
const QByteArray home = QDir::homePath().toUtf8();
if (home.isEmpty()) {
return input;
}
QByteArray out = input;
out.replace(home, QByteArrayLiteral("~"));
return out;
}
+51 -6
View File
@@ -48,11 +48,22 @@
- 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 in this step (see discussion #644): no
signal handler / crash-flush (step 4), no diagnostics export UI
(step 5), no log categories, no session header, no repeat collapsing
or rate limiting. Those are independent, separately-scoped follow-ups.
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
@@ -73,6 +84,11 @@ class QetLogger
/// 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);
@@ -81,10 +97,37 @@ class QetLogger
/// file names.
void pruneOldLogFiles(int days);
/// Snapshot of the in-memory ring, oldest first. For future use
/// (e.g. a diagnostics export action) -- not wired to any UI here.
/// 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;
@@ -93,6 +136,8 @@ class QetLogger
void rotateLocked();
void writeToFile(const QByteArray &line, QtMsgType type);
QString rotatedPath(int index) const;
QString crashDumpPath() const;
QString currentLogFilePath() const;
static QByteArray sanitize(const QByteArray &input);
static QByteArray truncateMessage(const QByteArray &input, int max_bytes);
@@ -0,0 +1,91 @@
/*
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/>.
*/
#include "diagnosticsreportdialog.h"
#include "../../qetmessagebox.h"
#include <QDialogButtonBox>
#include <QFile>
#include <QFileDialog>
#include <QFontDatabase>
#include <QLabel>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
DiagnosticsReportDialog::DiagnosticsReportDialog(
const QString &title,
const QString &intro,
const QByteArray &content,
QWidget *parent) :
QDialog(parent)
{
setWindowTitle(title);
resize(700, 500);
auto *layout = new QVBoxLayout(this);
auto *intro_label = new QLabel(intro, this);
intro_label->setWordWrap(true);
layout->addWidget(intro_label);
auto *preview = new QPlainTextEdit(this);
preview->setReadOnly(true);
preview->setLineWrapMode(QPlainTextEdit::NoWrap);
preview->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
preview->setPlainText(QString::fromUtf8(content));
layout->addWidget(preview);
auto *buttons = new QDialogButtonBox(this);
QPushButton *save_button = buttons->addButton(tr("Enregistrer..."), QDialogButtonBox::ActionRole);
buttons->addButton(QDialogButtonBox::Close);
connect(save_button, &QPushButton::clicked, this, &DiagnosticsReportDialog::saveToFile);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(buttons->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &QDialog::accept);
layout->addWidget(buttons);
// Stash the content for saveToFile(); the preview widget already
// holds a QString copy but we save the original UTF-8 bytes to avoid
// any round-trip surprises.
setProperty("qet_report_content", content);
}
void DiagnosticsReportDialog::saveToFile()
{
const QString path = QFileDialog::getSaveFileName(
this,
tr("Enregistrer le rapport de diagnostic"),
QStringLiteral("qet-diagnostic-report.txt"),
tr("Fichiers texte (*.txt);;Tous les fichiers (*)"));
if (path.isEmpty()) {
return;
}
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) {
QET::QetMessageBox::critical(
this,
tr("Erreur"),
tr("Impossible d'écrire dans le fichier « %1 ».").arg(path));
return;
}
file.write(property("qet_report_content").toByteArray());
file.close();
}
@@ -0,0 +1,51 @@
/*
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 DIAGNOSTICSREPORTDIALOG_H
#define DIAGNOSTICSREPORTDIALOG_H
#include <QDialog>
/**
@brief The DiagnosticsReportDialog class
Discussion #644, step 5: "Show what's in it before saving -- the user
is about to attach this to a public tracker." Used for both the
after-a-crash offer (QETApp::checkBackupFiles()) and the manual
"Help > Diagnostics > Save report" action -- the only difference
between the two is the intro text and where the content comes from
(QetLogger::pendingCrashDumpContents() vs. buildDiagnosticsReport()).
The content passed in is expected to already be redacted
(QetLogger::redact()) -- this dialog just displays and optionally
saves whatever it's given.
*/
class DiagnosticsReportDialog : public QDialog
{
Q_OBJECT
public:
explicit DiagnosticsReportDialog(
const QString &title,
const QString &intro,
const QByteArray &content,
QWidget *parent = nullptr);
private slots:
void saveToFile();
};
#endif // DIAGNOSTICSREPORTDIALOG_H
+6
View File
@@ -152,6 +152,12 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
// went to stderr, which is invisible in a Windows GUI session.
QetLogger::instance().init();
qInstallMessageHandler(qetLogMessageHandler);
// Step 4 (discussion #644): flush the ring to a crash-dump file if
// the process dies from here on. Installed right after the ring
// exists (init() just constructed it) and as early as reasonably
// possible, so it also covers whatever runs between here and
// QETApp's own construction below.
QetLogger::instance().installCrashHandler();
SingleApplication app(argc, argv, true);
#ifdef Q_OS_MACOS
+53
View File
@@ -40,6 +40,8 @@
#include "machine_info.h"
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
#include "qetversion.h"
#include "logging/qetlogger.h"
#include "logging/ui/diagnosticsreportdialog.h"
#include <cstdlib>
#include <iostream>
@@ -2575,6 +2577,10 @@ void QETApp::checkBackupFiles()
}
if (stale_files.isEmpty()) {
// Only offer an unretrieved crash dump when there's no project
// to recover this run -- discussion #644 step 5 is explicit
// that the two prompts must never both show at once.
checkCrashDump();
return;
}
@@ -2628,6 +2634,53 @@ void QETApp::checkBackupFiles()
}
}
/**
@brief QETApp::checkCrashDump
Discussion #644, step 5: if the crash handler (step 4) left an
unretrieved dump from a previous run, offer it to the user. Only
called from checkBackupFiles() when there was no stale project file
to recover this run, so the two prompts never both show at once.
*/
void QETApp::checkCrashDump()
{
QetLogger &logger = QetLogger::instance();
if (!logger.hasPendingCrashDump()) {
return;
}
const QByteArray content = logger.pendingCrashDumpContents();
DiagnosticsReportDialog dialog(
tr("Rapport de plantage"),
tr("QElectroTech ne s'est pas fermé correctement lors de sa dernière exécution.\n"
"Voici les derniers messages enregistrés avant l'arrêt -- vous pouvez les "
"enregistrer pour les joindre à un rapport de bug."),
content);
dialog.exec();
// Offered once, then marked retrieved -- regardless of whether the
// user chose to save it -- so it is never offered a second time.
logger.clearPendingCrashDump();
}
/**
@brief QETApp::showDiagnosticsReport
Discussion #644, step 5: the manual "Help > Diagnostics > Save
report" action. Unlike checkCrashDump(), this is about the *current*,
still-running session, not a previous one.
*/
void QETApp::showDiagnosticsReport()
{
const QByteArray content = QetLogger::instance().buildDiagnosticsReport();
DiagnosticsReportDialog dialog(
tr("Rapport de diagnostic"),
tr("Ceci contient les derniers messages de journalisation de cette session. "
"Vérifiez le contenu avant de le joindre à un rapport de bug public."),
content);
dialog.exec();
}
/**
@brief QETApp::fetchWindowStats
Updates the booleans concerning the state of the windows
+2
View File
@@ -271,6 +271,7 @@ class QETApp : public QObject
void openTitleBlockTemplateFiles(const QStringList &);
void configureQET();
void aboutQET();
void showDiagnosticsReport();
void receiveMessage(int instanceId, QByteArray message);
private:
@@ -287,6 +288,7 @@ class QETApp : public QObject
void initSystemTray();
void buildSystemTrayMenu();
void checkBackupFiles();
void checkCrashDump();
void fetchWindowStats(
const QList<QETDiagramEditor *> &,
const QList<QETElementEditor *> &,
+8
View File
@@ -136,6 +136,12 @@ void QETMainWindow::initCommonActions()
about_qt_ = new QAction(QET::Icons::QtLogo, tr("À propos de &Qt"), this);
about_qt_ -> setStatusTip(tr("Affiche des informations sur la bibliothèque Qt", "status bar tip"));
connect(about_qt_, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
diagnostics_action_ = new QAction(QET::Icons::DialogInformation, tr("Enregistrer un rapport de diagnostic..."), this);
diagnostics_action_ -> setStatusTip(tr("Génère un rapport avec les derniers messages de journalisation, pour l'inclure dans un rapport de bug", "status bar tip"));
connect(diagnostics_action_, &QAction::triggered, this, []() {
QETApp::instance()->showDiagnosticsReport();
});
}
/**
@@ -158,6 +164,8 @@ void QETMainWindow::initCommonMenus()
help_menu_ -> addAction(donate_);
help_menu_ -> addAction(about_qt_);
help_menu_ -> addAction(about_qet_);
help_menu_ -> addSeparator();
help_menu_ -> addAction(diagnostics_action_);
#ifdef Q_OS_WIN32
upgrade_ -> setVisible(true);
+1
View File
@@ -62,6 +62,7 @@ class QETMainWindow : public QMainWindow {
QAction *upgrade_M; ///< Launch browser on QElectroTech MAC_OS_X builds
QAction *donate_; ///< Launch browser to donate link
QAction *about_qt_; ///< launch the "About Qt" dialog
QAction *diagnostics_action_; ///< Open the diagnostics report dialog (discussion #644, step 5)
QMenu *settings_menu_; ///< Settings menu
QMenu *help_menu_; ///< Help menu
QMenu *display_toolbars_; ///< Show/hide toolbars/docks