mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-08-13 10:04:13 +02:00
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:
@@ -116,10 +116,14 @@ set(QET_RES_FILES
|
|||||||
set(QET_SRC_FILES
|
set(QET_SRC_FILES
|
||||||
${QET_DIR}/sources/cli_export.cpp
|
${QET_DIR}/sources/cli_export.cpp
|
||||||
${QET_DIR}/sources/cli_export.h
|
${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.cpp
|
||||||
${QET_DIR}/sources/logging/logring.h
|
${QET_DIR}/sources/logging/logring.h
|
||||||
${QET_DIR}/sources/logging/qetlogger.cpp
|
${QET_DIR}/sources/logging/qetlogger.cpp
|
||||||
${QET_DIR}/sources/logging/qetlogger.h
|
${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.cpp
|
||||||
${QET_DIR}/sources/pdf_links.h
|
${QET_DIR}/sources/pdf_links.h
|
||||||
${QET_DIR}/sources/import/edz/edzarchive.cpp
|
${QET_DIR}/sources/import/edz/edzarchive.cpp
|
||||||
|
|||||||
+4
-2
@@ -174,7 +174,8 @@ HEADERS += $$files(sources/*.h) \
|
|||||||
$$files(sources/svg/*.h) \
|
$$files(sources/svg/*.h) \
|
||||||
$$files(sources/import/edz/*.h) \
|
$$files(sources/import/edz/*.h) \
|
||||||
$$files(sources/import/edz/lzma/*.h) \
|
$$files(sources/import/edz/lzma/*.h) \
|
||||||
$$files(sources/logging/*.h)
|
$$files(sources/logging/*.h) \
|
||||||
|
$$files(sources/logging/ui/*.h)
|
||||||
|
|
||||||
SOURCES += $$files(sources/*.cpp) \
|
SOURCES += $$files(sources/*.cpp) \
|
||||||
$$files(sources/editor/*.cpp) \
|
$$files(sources/editor/*.cpp) \
|
||||||
@@ -221,7 +222,8 @@ SOURCES += $$files(sources/*.cpp) \
|
|||||||
$$files(sources/svg/*.cpp) \
|
$$files(sources/svg/*.cpp) \
|
||||||
$$files(sources/import/edz/*.cpp) \
|
$$files(sources/import/edz/*.cpp) \
|
||||||
$$files(sources/import/edz/lzma/*.c) \
|
$$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
|
# Needed for use promote QTreeWidget in terminalstripeditor.ui
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -17,75 +17,136 @@
|
|||||||
*/
|
*/
|
||||||
#include "logring.h"
|
#include "logring.h"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
/**
|
#ifdef Q_OS_WIN
|
||||||
@brief LogRing::LogRing
|
#include <io.h>
|
||||||
Preallocates all kCapacityEntries slots up front -- the only
|
#else
|
||||||
allocation this class ever does.
|
#include <cerrno>
|
||||||
*/
|
#include <unistd.h>
|
||||||
LogRing::LogRing()
|
#endif
|
||||||
{
|
|
||||||
m_entries.resize(kCapacityEntries);
|
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
|
@brief writeAllSignalSafe
|
||||||
@param line one already-formatted log line (no further formatting
|
Loops until length bytes have been written to fd or an unrecoverable
|
||||||
is done here). Truncated to kEntryBytes - 1 bytes if longer, with a
|
error occurs. write(2) may write fewer bytes than requested and may
|
||||||
trailing marker, so the stored entry is always a complete,
|
return EINTR -- both are *more* likely from inside a signal handler
|
||||||
independently-readable line.
|
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";
|
static const char kMarker[] = "...[ring-truncated]\n";
|
||||||
const int marker_len = static_cast<int>(sizeof(kMarker)) - 1;
|
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) {
|
if (line.size() < kEntryBytes) {
|
||||||
std::memcpy(slot.data, line.constData(), static_cast<size_t>(line.size()));
|
std::memcpy(slot.data, line.constData(), static_cast<size_t>(line.size()));
|
||||||
slot.length = line.size();
|
len = line.size();
|
||||||
} else {
|
} else {
|
||||||
const int keep = kEntryBytes - marker_len;
|
const int keep = kEntryBytes - marker_len;
|
||||||
std::memcpy(slot.data, line.constData(), static_cast<size_t>(keep));
|
std::memcpy(slot.data, line.constData(), static_cast<size_t>(keep));
|
||||||
std::memcpy(slot.data + keep, kMarker, static_cast<size_t>(marker_len));
|
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;
|
slot.length.store(len, std::memory_order_release);
|
||||||
if (m_count < kCapacityEntries) {
|
|
||||||
++m_count;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
@brief LogRing::snapshot
|
|
||||||
@return the entries currently held, oldest first. Safe to call from
|
|
||||||
normal (non-signal) code only.
|
|
||||||
*/
|
|
||||||
QVector<QByteArray> LogRing::snapshot() const
|
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;
|
QVector<QByteArray> result;
|
||||||
result.reserve(m_count);
|
result.reserve(static_cast<int>(count));
|
||||||
|
|
||||||
const int start = (m_count < kCapacityEntries) ? 0 : m_next_index;
|
for (quint64 i = 0; i < count; ++i) {
|
||||||
for (int i = 0; i < m_count; ++i) {
|
const Entry &slot = m_entries[static_cast<size_t>((start + i) % cap)];
|
||||||
const Entry &slot = m_entries[static_cast<size_t>((start + i) % kCapacityEntries)];
|
const int len = slot.length.load(std::memory_order_acquire);
|
||||||
result.append(QByteArray(slot.data, slot.length));
|
if (len > 0) {
|
||||||
|
result.append(QByteArray(slot.data, len));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
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()
|
void LogRing::clear()
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&m_mutex);
|
m_write_cursor.store(0, std::memory_order_relaxed);
|
||||||
m_next_index = 0;
|
for (auto &entry : m_entries) {
|
||||||
m_count = 0;
|
entry.length.store(0, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-25
@@ -19,31 +19,33 @@
|
|||||||
#define LOGRING_H
|
#define LOGRING_H
|
||||||
|
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QMutex>
|
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
#include <atomic>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief The LogRing class
|
@brief The LogRing class
|
||||||
Fixed-capacity, always-on in-memory ring of the most recent log
|
Fixed-capacity, always-on in-memory ring of the most recent log
|
||||||
lines. Discussion #644 (step 3): the ring exists as forward-compatible
|
lines, preallocated once at construction -- append() never
|
||||||
infrastructure for a future crash-flush (step 4, not implemented
|
allocates.
|
||||||
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.
|
|
||||||
|
|
||||||
Entries are fixed-size slots rather than a byte-packed ring: with
|
Lock-free by construction, not just "thread-safe": step 4 (see
|
||||||
kCapacityEntries * kEntryBytes chosen to land exactly on the 2 MiB
|
crashhandler.h) reads this ring from inside a POSIX signal handler,
|
||||||
budget, this keeps wraparound trivial (whole-slot overwrite, so a
|
where taking any lock is unsafe -- if the crashing thread happens to
|
||||||
slot is always either fully the old entry or fully the new one --
|
be the one that already holds it (or any other thread does and never
|
||||||
no torn entries) at the cost of truncating any single line to
|
gets scheduled again), the handler hangs forever, and you lose both
|
||||||
kEntryBytes, independently of the logger's own (larger) per-message
|
the ring dump *and* the core dump. So there is no mutex here at all:
|
||||||
truncation.
|
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
|
Accepted tradeoff: if dumpToFd() runs while another thread is
|
||||||
discussion #644 specifies for a signal-handler crash path (step 4)
|
mid-append into the exact slot being read (only possible in the
|
||||||
-- no signal handler is installed by this code, so nothing calls
|
crash-handler case, and only for at most one slot), that one entry
|
||||||
into the ring from inside a signal context.
|
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
|
class LogRing
|
||||||
{
|
{
|
||||||
@@ -54,24 +56,30 @@ class LogRing
|
|||||||
LogRing();
|
LogRing();
|
||||||
|
|
||||||
/// Append one already-formatted, already-truncated log line.
|
/// Append one already-formatted, already-truncated log line.
|
||||||
/// Bytes beyond kEntryBytes - 1 are dropped with a truncation marker.
|
/// Bytes beyond kEntryBytes are dropped with a truncation marker.
|
||||||
void append(const QByteArray &line);
|
/// 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;
|
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();
|
void clear();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Entry {
|
struct Entry {
|
||||||
char data[kEntryBytes] = {};
|
char data[kEntryBytes];
|
||||||
int length = 0;
|
std::atomic<int> length{0}; // 0 = not yet written this lap
|
||||||
};
|
};
|
||||||
|
|
||||||
mutable QMutex m_mutex;
|
|
||||||
std::vector<Entry> m_entries; // preallocated once, capacity fixed
|
std::vector<Entry> m_entries; // preallocated once, capacity fixed
|
||||||
int m_next_index = 0;
|
std::atomic<quint64> m_write_cursor{0}; // monotonically increasing
|
||||||
int m_count = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // LOGRING_H
|
#endif // LOGRING_H
|
||||||
|
|||||||
@@ -17,11 +17,14 @@
|
|||||||
*/
|
*/
|
||||||
#include "qetlogger.h"
|
#include "qetlogger.h"
|
||||||
|
|
||||||
|
#include "crashhandler.h"
|
||||||
#include "../qetapp.h"
|
#include "../qetapp.h"
|
||||||
|
#include "../qetversion.h"
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QSysInfo>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -96,6 +99,24 @@ void QetLogger::init()
|
|||||||
m_file_output_ok = ensureFileOpenLocked();
|
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
|
@brief QetLogger::ensureFileOpenLocked
|
||||||
Caller must hold m_file_mutex. Opens the current session's log file
|
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);
|
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);
|
const QFileInfo info(path);
|
||||||
if (info.exists() && info.isSymLink()) {
|
if (info.exists() && info.isSymLink()) {
|
||||||
@@ -143,7 +164,7 @@ void QetLogger::rotateLocked()
|
|||||||
{
|
{
|
||||||
m_file.close();
|
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) {
|
for (int i = kRotationKeep; i >= 1; --i) {
|
||||||
const QString from = (i == 1) ? base_path : rotatedPath(i - 1);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,11 +48,22 @@
|
|||||||
- Step 3: every formatted line is also appended to an in-memory
|
- Step 3: every formatted line is also appended to an in-memory
|
||||||
LogRing (see logring.h) -- always on, fixed capacity, allocation-
|
LogRing (see logring.h) -- always on, fixed capacity, allocation-
|
||||||
free on the hot path.
|
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
|
Deliberately NOT included: log categories, a full session header
|
||||||
signal handler / crash-flush (step 4), no diagnostics export UI
|
beyond what the crash dump/report already carry, repeat collapsing,
|
||||||
(step 5), no log categories, no session header, no repeat collapsing
|
rate limiting. Those are listed in discussion #644 under "best
|
||||||
or rate limiting. Those are independent, separately-scoped follow-ups.
|
practices worth building in", not part of the numbered steps.
|
||||||
|
|
||||||
Escape hatch: if QET_LOG_DISABLE=1 is set in the environment at
|
Escape hatch: if QET_LOG_DISABLE=1 is set in the environment at
|
||||||
init() time, this class does nothing beyond a minimal, independent
|
init() time, this class does nothing beyond a minimal, independent
|
||||||
@@ -73,6 +84,11 @@ class QetLogger
|
|||||||
/// session's log filename, and opens the file.
|
/// session's log filename, and opens the file.
|
||||||
void init();
|
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.
|
/// The function installed via qInstallMessageHandler() forwards here.
|
||||||
void handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg);
|
void handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg);
|
||||||
|
|
||||||
@@ -81,10 +97,37 @@ class QetLogger
|
|||||||
/// file names.
|
/// file names.
|
||||||
void pruneOldLogFiles(int days);
|
void pruneOldLogFiles(int days);
|
||||||
|
|
||||||
/// Snapshot of the in-memory ring, oldest first. For future use
|
/// Snapshot of the in-memory ring, oldest first.
|
||||||
/// (e.g. a diagnostics export action) -- not wired to any UI here.
|
|
||||||
QVector<QByteArray> ringSnapshot() const {return m_ring.snapshot();}
|
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:
|
private:
|
||||||
QetLogger() = default;
|
QetLogger() = default;
|
||||||
QetLogger(const QetLogger &) = delete;
|
QetLogger(const QetLogger &) = delete;
|
||||||
@@ -93,6 +136,8 @@ class QetLogger
|
|||||||
void rotateLocked();
|
void rotateLocked();
|
||||||
void writeToFile(const QByteArray &line, QtMsgType type);
|
void writeToFile(const QByteArray &line, QtMsgType type);
|
||||||
QString rotatedPath(int index) const;
|
QString rotatedPath(int index) const;
|
||||||
|
QString crashDumpPath() const;
|
||||||
|
QString currentLogFilePath() const;
|
||||||
|
|
||||||
static QByteArray sanitize(const QByteArray &input);
|
static QByteArray sanitize(const QByteArray &input);
|
||||||
static QByteArray truncateMessage(const QByteArray &input, int max_bytes);
|
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
|
||||||
@@ -152,6 +152,12 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
|||||||
// went to stderr, which is invisible in a Windows GUI session.
|
// went to stderr, which is invisible in a Windows GUI session.
|
||||||
QetLogger::instance().init();
|
QetLogger::instance().init();
|
||||||
qInstallMessageHandler(qetLogMessageHandler);
|
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);
|
SingleApplication app(argc, argv, true);
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
#include "machine_info.h"
|
#include "machine_info.h"
|
||||||
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
|
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
|
||||||
#include "qetversion.h"
|
#include "qetversion.h"
|
||||||
|
#include "logging/qetlogger.h"
|
||||||
|
#include "logging/ui/diagnosticsreportdialog.h"
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -2575,6 +2577,10 @@ void QETApp::checkBackupFiles()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stale_files.isEmpty()) {
|
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;
|
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
|
@brief QETApp::fetchWindowStats
|
||||||
Updates the booleans concerning the state of the windows
|
Updates the booleans concerning the state of the windows
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ class QETApp : public QObject
|
|||||||
void openTitleBlockTemplateFiles(const QStringList &);
|
void openTitleBlockTemplateFiles(const QStringList &);
|
||||||
void configureQET();
|
void configureQET();
|
||||||
void aboutQET();
|
void aboutQET();
|
||||||
|
void showDiagnosticsReport();
|
||||||
void receiveMessage(int instanceId, QByteArray message);
|
void receiveMessage(int instanceId, QByteArray message);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -287,6 +288,7 @@ class QETApp : public QObject
|
|||||||
void initSystemTray();
|
void initSystemTray();
|
||||||
void buildSystemTrayMenu();
|
void buildSystemTrayMenu();
|
||||||
void checkBackupFiles();
|
void checkBackupFiles();
|
||||||
|
void checkCrashDump();
|
||||||
void fetchWindowStats(
|
void fetchWindowStats(
|
||||||
const QList<QETDiagramEditor *> &,
|
const QList<QETDiagramEditor *> &,
|
||||||
const QList<QETElementEditor *> &,
|
const QList<QETElementEditor *> &,
|
||||||
|
|||||||
@@ -136,6 +136,12 @@ void QETMainWindow::initCommonActions()
|
|||||||
about_qt_ = new QAction(QET::Icons::QtLogo, tr("À propos de &Qt"), this);
|
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"));
|
about_qt_ -> setStatusTip(tr("Affiche des informations sur la bibliothèque Qt", "status bar tip"));
|
||||||
connect(about_qt_, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
|
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(donate_);
|
||||||
help_menu_ -> addAction(about_qt_);
|
help_menu_ -> addAction(about_qt_);
|
||||||
help_menu_ -> addAction(about_qet_);
|
help_menu_ -> addAction(about_qet_);
|
||||||
|
help_menu_ -> addSeparator();
|
||||||
|
help_menu_ -> addAction(diagnostics_action_);
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
upgrade_ -> setVisible(true);
|
upgrade_ -> setVisible(true);
|
||||||
|
|||||||
@@ -60,8 +60,9 @@ class QETMainWindow : public QMainWindow {
|
|||||||
QAction *youtube_; ///< Launch browser on QElectroTech Youtube channel
|
QAction *youtube_; ///< Launch browser on QElectroTech Youtube channel
|
||||||
QAction *upgrade_; ///< Launch browser on QElectroTech Windows Nightly builds
|
QAction *upgrade_; ///< Launch browser on QElectroTech Windows Nightly builds
|
||||||
QAction *upgrade_M; ///< Launch browser on QElectroTech MAC_OS_X builds
|
QAction *upgrade_M; ///< Launch browser on QElectroTech MAC_OS_X builds
|
||||||
QAction *donate_; ///< Launch browser to donate link
|
QAction *donate_; ///< Launch browser to donate link
|
||||||
QAction *about_qt_; ///< launch the "About Qt" dialog
|
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 *settings_menu_; ///< Settings menu
|
||||||
QMenu *help_menu_; ///< Help menu
|
QMenu *help_menu_; ///< Help menu
|
||||||
QMenu *display_toolbars_; ///< Show/hide toolbars/docks
|
QMenu *display_toolbars_; ///< Show/hide toolbars/docks
|
||||||
|
|||||||
Reference in New Issue
Block a user