mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-08-13 10:04:13 +02:00
5dec36cb29
Stacked on the steps 1-3 branch (feature-diagnostic-logging, PR #646). Kept as its own PR rather than folded into that one, matching the discussion's own framing: step 4 is explicitly "the highest-risk piece ... lands last, behind its own switch." ## Step 4 -- crash-time ring flush (CrashHandler) Installs a handler for SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL (POSIX) / SetUnhandledExceptionFilter (Windows) that flushes the in-memory ring to a fixed crash_dump.log before the process dies. This required reworking LogRing (step 3) to be genuinely lock-free, not just mutex-protected: a signal handler that blocks on a lock the crashing thread (or another thread) already holds turns a clean crash into a hang -- no ring dump *and* no core dump, worse than doing nothing. append() now claims a slot with a single atomic fetch-add; dumpToFd() reads the preallocated entries directly and writes them with write(2) only, looping on EINTR/short writes. Accepted tradeoff: at most one entry can be read torn if a crash lands mid-append into that exact slot -- documented in logring.h, and the alternative (a seqlock to detect and retry) wasn't judged worth the complexity for that window. Other invariants implemented per the discussion: - sigaltstack with a static 64 KiB buffer, SA_ONSTACK -- a stack- overflow SIGSEGV has no usable stack for a handler without one. - Nothing under the actual handler touches Qt, QString or the allocator: the dump path and a small header (version/git/OS/Qt) are precomputed into fixed char buffers by install(), which runs once at startup in normal context. - Atomic test-and-set so only the first crash writes a dump; a second concurrent/nested fault goes straight to restore-and-re-raise. - After writing, the handler restores SIG_DFL and re-raises (POSIX) / returns EXCEPTION_CONTINUE_SEARCH (Windows) so the OS's own crash path -- core dump, Windows Error Reporting -- still runs. A handler that "fixed" the crash by swallowing the signal would destroy exactly the post-mortem evidence this whole design exists to preserve. Tested in this environment: POSIX/Linux only, all five signals. Sent each directly to a running process and confirmed (a) crash_dump.log is written with the correct header and ring contents, mode 0600, and (b) the process still terminates via the signal with the kernel's own "core dumped" flag set (exit code 128+signal, confirmed for all five). The Windows path is implemented per the discussion's guidance but is untested -- no Windows build available in this sandbox. ## Step 5 -- getting the data back out - QETApp::checkCrashDump(), called from checkBackupFiles() only when there's no stale project file to recover this run (so the two prompts never both show, per the discussion), offers an unretrieved crash dump via DiagnosticsReportDialog and then deletes it regardless of the user's choice -- offered exactly once. - A new "Aide > Enregistrer un rapport de diagnostic..." action (QETMainWindow) builds the same kind of report from the *current* session (QetLogger::buildDiagnosticsReport(): header + this session's log file) for a manual "attach this to a bug report" flow, not tied to a crash. - Both go through QetLogger::redact() before ever reaching the user: the one redaction implemented is a literal replace of the home directory with "~", since an absolute path under it leaks the account name. The discussion's fancier "optionally redact project filenames too" isn't attempted -- reliably telling a project path apart from arbitrary log text is a much fuzzier problem than a literal prefix match. - DiagnosticsReportDialog shows the full (already-redacted) content before saving, per the discussion: "the user is about to attach this to a public tracker." Verified in a real GUI session (Xvfb): triggered a SIGSEGV, relaunched, confirmed the crash-report dialog appears with the right header/content, confirmed it does not reappear on a second relaunch, and confirmed the manual "Save report" action produces a correctly-formatted report and saves it to a chosen path. Built clean, no new warnings. ## Build systems Registered in both: cmake/qet_compilation_vars.cmake, and qelectrotech.pro. The .pro needed explicit globs for the new sources/logging/ui/ subfolder -- sources/logging/*.{h,cpp} was already globbed, but unlike the other ui/ subfolders that one had no entry of its own, so diagnosticsreportdialog.{h,cpp} would not have been built under qmake.
215 lines
7.5 KiB
C++
215 lines
7.5 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/>.
|
|
*/
|
|
#include "cli_export.h"
|
|
#include "logging/qetlogger.h"
|
|
#include "machine_info.h"
|
|
#include "qet.h"
|
|
#include "qetapp.h"
|
|
#include "qetproject.h"
|
|
#include "singleapplication.h"
|
|
#include "utils/qetsettings.h"
|
|
|
|
#include <QApplication>
|
|
#include <QDomImplementation>
|
|
|
|
#include <QStyleFactory>
|
|
#include <QtConcurrentRun>
|
|
|
|
#ifdef Q_OS_MACOS
|
|
#include <QFileOpenEvent>
|
|
|
|
/**
|
|
@brief EarlyFileOpenCatcher
|
|
On macOS, a cold launch via Finder double-click can deliver the
|
|
QFileOpenEvent to QApplication before QETApp exists and before its
|
|
real eventFilter is installed (the event loop can start servicing
|
|
native/Cocoa events before our own code in main() reaches that
|
|
point). This tiny filter is installed immediately on `app` so no
|
|
QFileOpenEvent can slip through unseen; it just buffers the path.
|
|
Once QETApp is constructed, main() drains the buffer and installs
|
|
the real QETApp::eventFilter for any subsequent event.
|
|
*/
|
|
class EarlyFileOpenCatcher : public QObject
|
|
{
|
|
public:
|
|
using QObject::QObject;
|
|
QStringList bufferedFiles;
|
|
|
|
protected:
|
|
bool eventFilter(QObject *object, QEvent *e) override
|
|
{
|
|
if (e->type() == QEvent::FileOpen) {
|
|
bufferedFiles << static_cast<QFileOpenEvent *>(e)->file();
|
|
return true;
|
|
}
|
|
return QObject::eventFilter(object, e);
|
|
}
|
|
};
|
|
#endif
|
|
|
|
/**
|
|
@brief qetLogMessageHandler
|
|
Installed via qInstallMessageHandler(); forwards to QetLogger, which
|
|
holds all the actual formatting/ring/rotation state. See
|
|
logging/qetlogger.h for the rationale (discussion #644).
|
|
*/
|
|
void qetLogMessageHandler(QtMsgType type,
|
|
const QMessageLogContext &context,
|
|
const QString &msg)
|
|
{
|
|
QetLogger::instance().handleMessage(type, context, msg);
|
|
}
|
|
|
|
/**
|
|
@brief main
|
|
Main function of QElectroTech
|
|
@param argc : number of parameters
|
|
\~French number of paramètres
|
|
\~ @param argv : parameters
|
|
\~French paramètres
|
|
\~ @return exit code
|
|
*/
|
|
int main(int argc, char **argv)
|
|
{
|
|
// before creating Application:
|
|
// export environment-variable "QT_HASH_SEED" with value "0" to
|
|
// disable radomisation for hashes in order to obtain "clean" XML-diffs:
|
|
qputenv("QT_HASH_SEED", "0");
|
|
//Some setup, notably to use with QSetting.
|
|
QCoreApplication::setOrganizationName("QElectroTech");
|
|
QCoreApplication::setOrganizationDomain("qelectrotech.org");
|
|
QCoreApplication::setApplicationName("QElectroTech");
|
|
|
|
// Refuse invalid data when building QDom documents instead of
|
|
// serializing malformed XML (CVE-2026-15037). This is the default
|
|
// from Qt 6.12 on; opt in explicitly for older Qt 5/6.
|
|
QDomImplementation::setInvalidDataPolicy(
|
|
QDomImplementation::ReturnNullNode);
|
|
//Creation and execution of the application
|
|
//HighDPI
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
|
|
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
|
#else
|
|
#if TODO_LIST
|
|
#pragma message("@TODO remove code for QT 6 or later")
|
|
#endif
|
|
#endif
|
|
|
|
|
|
#if QT_VERSION > QT_VERSION_CHECK(5, 7, 0) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
|
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
|
|
#endif
|
|
|
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
|
qputenv("QT_ENABLE_HIGHDPI_SCALING", "1");
|
|
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFactorRoundingPolicy());
|
|
#endif
|
|
|
|
|
|
// Headless command-line export: render a project to PDF/PNG/SVG without
|
|
// opening the GUI, then exit. Must be handled before SingleApplication
|
|
// (which would forward the args to an already-running instance).
|
|
{
|
|
QStringList raw_args;
|
|
for (int i = 0; i < argc; ++i)
|
|
raw_args << QString::fromLocal8Bit(argv[i]);
|
|
if (CLIExport::isExportRequest(raw_args)) {
|
|
QApplication export_app(argc, argv);
|
|
// No crash-recovery backups in one-shot CLI mode: the backup write
|
|
// runs on a background thread referencing the project and races the
|
|
// process exit (intermittent segfault in QET::writeToFile).
|
|
QETProject::setBackupEnabled(false);
|
|
return CLIExport::run(export_app.arguments());
|
|
}
|
|
}
|
|
|
|
// Resolve the logger's state (log directory, session filename, open
|
|
// file handle) explicitly here, immediately before installing the
|
|
// handler -- not implicitly on whichever thread happens to log
|
|
// first. See QetLogger::init().
|
|
//
|
|
// Install the log-file message handler BEFORE the application starts:
|
|
// QETApp's constructor does the whole startup (collections, editor,
|
|
// opening the projects given on the command line), so installing the
|
|
// handler afterwards - as was done in the startup worker below - meant
|
|
// exactly the interesting lines (collection and project load timers)
|
|
// 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
|
|
app.setStyle(QStyleFactory::create("Fusion"));
|
|
// Installed as early as possible, before anything else can run an
|
|
// event loop, to catch a QFileOpenEvent that might be delivered
|
|
// during a cold launch before QETApp exists.
|
|
EarlyFileOpenCatcher early_catcher;
|
|
app.installEventFilter(&early_catcher);
|
|
#endif
|
|
|
|
if (app.isSecondary())
|
|
{
|
|
QStringList arg_list = app.arguments();
|
|
//Remove the first argument, it's the binary file
|
|
arg_list.takeFirst();
|
|
QETArguments qetarg(arg_list);
|
|
QString message = "launched-with-args: " + QET::joinWithSpaces(
|
|
QStringList(qetarg.arguments()));
|
|
app.sendMessage(message.toUtf8());
|
|
return 0;
|
|
}
|
|
|
|
QETApp qetapp;
|
|
QETApp::instance()->installEventFilter(&qetapp);
|
|
#ifdef Q_OS_MACOS
|
|
//Handle the opening of QET when user double click on a .qet .elmt .tbt file
|
|
//or drop these same files to the QET icon of the dock.
|
|
//Swap the early catcher (installed right after `app` was constructed,
|
|
//see above) for the real filter, then drain anything it buffered
|
|
//during the cold-launch window before QETApp existed.
|
|
app.removeEventFilter(&early_catcher);
|
|
app.installEventFilter(&qetapp);
|
|
if (!early_catcher.bufferedFiles.isEmpty())
|
|
qetapp.openFiles(QETArguments(early_catcher.bufferedFiles));
|
|
#endif
|
|
QObject::connect(&app, &SingleApplication::receivedMessage,
|
|
&qetapp, &QETApp::receiveMessage);
|
|
|
|
// Pre-initialise on the main (GUI) thread: the constructor calls
|
|
// qApp->screens() which is not thread-safe in Qt5 — calling instance()
|
|
// here guarantees the singleton is fully built before the worker runs.
|
|
MachineInfo::instance();
|
|
|
|
[[maybe_unused]] auto startup_future = QtConcurrent::run([=]()
|
|
{
|
|
qInfo("Start-up");
|
|
// delete old log files of max 7 days old.
|
|
QetLogger::instance().pruneOldLogFiles(7);
|
|
MachineInfo::instance()->send_info_to_debug();
|
|
});
|
|
return app.exec();
|
|
}
|
|
|