Files
qelectrotech-source-mirror/sources/main.cpp
T
ispyisail ff812f221a Rework diagnostic logging: fix the file writer, add rotation and a ring buffer
Implements steps 1-3 of discussion #644 (deliberately not steps 4/5 --
no signal handler / crash flush, no diagnostics UI; see below).

## Step 1 -- fix the existing logger (bugs, no new behavior)

- One QFile handle held open for the whole session under a mutex,
  instead of opening and closing the log file on every single message.
- The log directory and the session's date-stamped filename are
  resolved exactly once, in the new QetLogger::init() called explicitly
  from main() immediately before qInstallMessageHandler() -- not
  recomputed per message, so a session that runs past midnight now
  stays in one file instead of silently splitting.
- Age-based retention now uses lastModified() instead of lastRead():
  opening a log to attach it to a bug report no longer resets its
  retention clock.
- stderr and file output both encode UTF-8 explicitly (toUtf8()),
  replacing stderr's toLocal8Bit() and the file stream's previously
  Qt5/Qt6-inconsistent default encoding.

## Step 2 -- size-capped rotation + hardening

- The previously-unbounded daily file is now capped at 2 MiB and
  rotated (kMaxFileBytes/kRotationKeep in QetLogger), keeping
  <date>.log plus <date>.1.log .. <date>.4.log; oldest is dropped.
- Each message is truncated to 4 KB with a "...[truncated N bytes]"
  marker before it reaches the ring or the file.
- Control characters (newlines, tabs, other non-printables) in message
  content are escaped, since much of what QET logs is externally
  controlled (file paths, element names, font strings out of a .qet
  file) -- left unescaped, an embedded '\n' could forge log lines.
- The log file is refused if a symlink already exists at that path,
  and is created/rotated owner-read/write only.

## Step 3 -- in-memory ring buffer

- LogRing (sources/logging/logring.h) is a fixed-capacity, always-on
  ring of the last 4096 log lines, preallocated once at construction
  (4096 * 512 B = 2 MiB) so append() never allocates. Entries are
  stored as plain pre-formatted bytes in fixed-size slots -- the shape
  discussion #644 specifies so a *future* crash handler could dump it
  with nothing but write(2), even though no such handler exists yet.
  Thread-safe via a plain QMutex (the lock-free requirement in the
  discussion applies specifically to a signal-handler read path, which
  this step doesn't add).

## Escape hatch

QET_LOG_DISABLE=1 in the environment at startup bypasses all of the
above -- no ring, no file, no rotation -- falling back to a minimal,
self-contained stderr passthrough that doesn't share any code with the
new formatting/sanitization path, so it stays usable even if that path
is what's misbehaving.

## Deliberately not included (per the discussion's own phasing)

- No signal handler / crash-time ring flush (step 4) -- the discussion
  flags this as the highest-risk piece, explicitly meant to land last
  and behind its own switch once the rest is proven.
- No diagnostics export UI (step 5).
- No log categories, session header, repeat collapsing or rate
  limiting -- listed under "best practices worth building in", not
  part of steps 1-3.

## Testing

Built clean, no new warnings.

Verified with real runs (QT_QPA_PLATFORM=offscreen, isolated HOME):
- Log file created at the expected dataDir()/YYYYMMDD.log path, mode
  0600.
- A full startup's worth of real messages (translations, MachineInfo's
  system dump, collection loading) written correctly; every one of the
  231 lines in one run starts with a proper timestamp -- confirmed the
  sanitizer correctly escapes the raw embedded newlines/tabs in
  MachineInfo's multi-line CPU/GPU description fields into visible
  \n/\t sequences rather than letting them fragment the log.
- QET_LOG_DISABLE=1: zero log files created, stderr still worked via
  the independent legacy path.
- Rotation: pre-filled a log to just under the 2 MiB cap, ran a normal
  session, confirmed it rotated to <date>.1.log (still 0600) with a
  byte-clean split (no truncated/duplicated line at the boundary) and
  a fresh <date>.log picked up from the next line.
2026-08-03 14:25:42 +12:00

209 lines
7.1 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);
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();
}