diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 57a012916..9150f21fe 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -116,6 +116,10 @@ set(QET_RES_FILES set(QET_SRC_FILES ${QET_DIR}/sources/cli_export.cpp ${QET_DIR}/sources/cli_export.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/pdf_links.cpp ${QET_DIR}/sources/pdf_links.h ${QET_DIR}/sources/import/edz/edzarchive.cpp diff --git a/sources/logging/logring.cpp b/sources/logging/logring.cpp new file mode 100644 index 000000000..91eb7583c --- /dev/null +++ b/sources/logging/logring.cpp @@ -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 . +*/ +#include "logring.h" + +#include +#include + +/** + @brief LogRing::LogRing + Preallocates all kCapacityEntries slots up front -- the only + allocation this class ever does. +*/ +LogRing::LogRing() +{ + m_entries.resize(kCapacityEntries); +} + +/** + @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. +*/ +void LogRing::append(const QByteArray &line) +{ + static const char kMarker[] = "...[ring-truncated]\n"; + const int marker_len = static_cast(sizeof(kMarker)) - 1; + + QMutexLocker locker(&m_mutex); + + Entry &slot = m_entries[static_cast(m_next_index)]; + + if (line.size() < kEntryBytes) { + std::memcpy(slot.data, line.constData(), static_cast(line.size())); + slot.length = line.size(); + } else { + const int keep = kEntryBytes - marker_len; + std::memcpy(slot.data, line.constData(), static_cast(keep)); + std::memcpy(slot.data + keep, kMarker, static_cast(marker_len)); + slot.length = kEntryBytes; + } + + m_next_index = (m_next_index + 1) % kCapacityEntries; + 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 LogRing::snapshot() const +{ + QMutexLocker locker(&m_mutex); + + QVector result; + result.reserve(m_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((start + i) % kCapacityEntries)]; + result.append(QByteArray(slot.data, slot.length)); + } + + return result; +} + +void LogRing::clear() +{ + QMutexLocker locker(&m_mutex); + m_next_index = 0; + m_count = 0; +} diff --git a/sources/logging/logring.h b/sources/logging/logring.h new file mode 100644 index 000000000..26f0d492e --- /dev/null +++ b/sources/logging/logring.h @@ -0,0 +1,77 @@ +/* + 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 . +*/ +#ifndef LOGRING_H +#define LOGRING_H + +#include +#include +#include +#include + +/** + @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. + + 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. + + 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. +*/ +class LogRing +{ + public: + static constexpr int kCapacityEntries = 4096; + static constexpr int kEntryBytes = 512; // 4096 * 512 = 2 MiB total + + LogRing(); + + /// Append one already-formatted, already-truncated log line. + /// Bytes beyond kEntryBytes - 1 are dropped with a truncation marker. + void append(const QByteArray &line); + + /// Snapshot of the entries currently held, oldest first. + QVector snapshot() const; + + void clear(); + + private: + struct Entry { + char data[kEntryBytes] = {}; + int length = 0; + }; + + mutable QMutex m_mutex; + std::vector m_entries; // preallocated once, capacity fixed + int m_next_index = 0; + int m_count = 0; +}; + +#endif // LOGRING_H diff --git a/sources/logging/qetlogger.cpp b/sources/logging/qetlogger.cpp new file mode 100644 index 000000000..df4dd6736 --- /dev/null +++ b/sources/logging/qetlogger.cpp @@ -0,0 +1,335 @@ +/* + 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 . +*/ +#include "qetlogger.h" + +#include "../qetapp.h" + +#include +#include +#include +#include + +namespace { + +/** + @brief legacyStderrOutput + The QET_LOG_DISABLE=1 escape hatch. Deliberately independent of + every other function in this file -- including sanitize()/ + formatLine(), which are exactly the new code a problem might be in + -- so this path stays usable even if the rest of the rework + misbehaves. No ring, no file, no rotation, no mutex. +*/ +void legacyStderrOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + const QByteArray local_msg = msg.toLocal8Bit(); + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; + + const char *level = "Unknown"; + switch (type) { + case QtDebugMsg: level = "Debug"; break; + case QtInfoMsg: level = "Info"; break; + case QtWarningMsg: level = "Warning"; break; + case QtCriticalMsg: level = "Critical"; break; + case QtFatalMsg: level = "Fatal"; break; + } + + fprintf(stderr, "%s: %s (%s:%u, %s)\n", + level, local_msg.constData(), file, context.line, function); +} + +/** + @brief ReentrancyGuard + Sets the referenced flag on construction, clears it on destruction + (including via early return / exception unwinding). Used as the + per-thread guard against the logger recursing into itself. +*/ +struct ReentrancyGuard +{ + bool &flag; + explicit ReentrancyGuard(bool &f) : flag(f) {flag = true;} + ~ReentrancyGuard() {flag = false;} +}; + +} // namespace + +/** + @brief QetLogger::instance + Function-local static: guaranteed constructed exactly once, in a + thread-safe way, on first use -- but the *meaningful* initialisation + (log path resolution, opening the file) happens in init(), called + explicitly from main() at a defined point, not implicitly on + whichever thread happens to log first. +*/ +QetLogger &QetLogger::instance() +{ + static QetLogger logger; + return logger; +} + +void QetLogger::init() +{ + m_disabled = (qgetenv("QET_LOG_DISABLE") == "1"); + if (m_disabled) { + return; + } + + m_log_dir = QETApp::dataDir(); + m_base_name = QDate::currentDate().toString(QStringLiteral("yyyyMMdd")); + + QMutexLocker locker(&m_file_mutex); + m_file_output_ok = ensureFileOpenLocked(); +} + +/** + @brief QetLogger::ensureFileOpenLocked + Caller must hold m_file_mutex. Opens the current session's log file + if not already open. Refuses to follow a pre-existing symlink at + that path, and creates the file owner-read/write only. +*/ +bool QetLogger::ensureFileOpenLocked() +{ + if (m_file.isOpen()) { + return true; + } + + QDir().mkpath(m_log_dir); + + const QString path = m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log"); + + const QFileInfo info(path); + if (info.exists() && info.isSymLink()) { + // Filesystem hardening: refuse a pre-planted symlink rather than + // silently appending to whatever it points at. + return false; + } + + m_file.setFileName(path); + if (!m_file.open(QIODevice::WriteOnly | QIODevice::Append)) { + return false; + } + m_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); + m_bytes_written_current_file = m_file.size(); + return true; +} + +QString QetLogger::rotatedPath(int index) const +{ + return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".") % QString::number(index) % QStringLiteral(".log"); +} + +/** + @brief QetLogger::rotateLocked + Caller must hold m_file_mutex. Shifts .3.log -> .4.log (dropping the + previous .4.log), .2.log -> .3.log, .1.log -> .2.log, .log -> .1.log, + then opens a fresh, empty current file. +*/ +void QetLogger::rotateLocked() +{ + m_file.close(); + + const QString base_path = m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log"); + + for (int i = kRotationKeep; i >= 1; --i) { + const QString from = (i == 1) ? base_path : rotatedPath(i - 1); + const QString to = rotatedPath(i); + + if (QFile::exists(to)) { + QFile::remove(to); + } + if (QFile::exists(from)) { + QFile::rename(from, to); + } + } + + m_bytes_written_current_file = 0; + m_file_output_ok = ensureFileOpenLocked(); +} + +void QetLogger::writeToFile(const QByteArray &line, QtMsgType type) +{ + QMutexLocker locker(&m_file_mutex); + + if (!m_file_output_ok) { + // Write-failure policy: once file output has failed, stop + // attempting it rather than spin-retrying every message. The + // ring keeps running regardless. + return; + } + + const qint64 written = m_file.write(line); + if (written != line.size()) { + m_file_output_ok = false; + m_file.close(); + return; + } + m_bytes_written_current_file += written; + + if (type >= QtWarningMsg) { + m_file.flush(); + } + + if (m_bytes_written_current_file >= kMaxFileBytes) { + rotateLocked(); + } +} + +/** + @brief QetLogger::sanitize + Escapes newlines, carriage returns and other control characters. + Much of what QET logs is externally controlled (file paths, element + names, font strings read out of a .qet file); left unescaped, a + crafted string containing '\n' can forge additional log lines. + Operates on already-UTF-8-encoded bytes: this is safe because UTF-8 + continuation bytes are always >= 0x80, so any byte < 0x20 found here + is a genuine ASCII control character, never part of a multi-byte + sequence. +*/ +QByteArray QetLogger::sanitize(const QByteArray &input) +{ + QByteArray out; + out.reserve(input.size()); + + for (unsigned char c : input) { + if (c == '\n') { + out += "\\n"; + } else if (c == '\r') { + out += "\\r"; + } else if (c == '\t') { + out += static_cast(c); + } else if (c < 0x20 || c == 0x7F) { + out += "\\x"; + out += QByteArray::number(c, 16).rightJustified(2, '0'); + } else { + out += static_cast(c); + } + } + + return out; +} + +/** + @brief QetLogger::truncateMessage + Caps a single message at max_bytes, appending a marker stating how + many bytes were dropped, so one pathological caller (e.g. dumping an + entire XML document to qDebug()) can't consume an unbounded amount + of the ring's or file's byte budget. +*/ +QByteArray QetLogger::truncateMessage(const QByteArray &input, int max_bytes) +{ + if (input.size() <= max_bytes) { + return input; + } + + const int dropped = input.size() - max_bytes; + QByteArray out = input.left(max_bytes); + out += " ...[truncated "; + out += QByteArray::number(dropped); + out += " bytes]"; + return out; +} + +QByteArray QetLogger::formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg) +{ + // Includes the date (not just the time) so that a session crossing + // midnight -- now kept in a single file -- doesn't read as ambiguous. + const QByteArray timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz")).toUtf8(); + + const char *level = "Unknown"; + switch (type) { + case QtDebugMsg: level = "Debug"; break; + case QtInfoMsg: level = "Info"; break; + case QtWarningMsg: level = "Warning"; break; + case QtCriticalMsg: level = "Critical"; break; + case QtFatalMsg: level = "Fatal"; break; + } + + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; + + QByteArray line = timestamp; + line += ' '; + line += level; + line += ": "; + line += sanitized_msg; + + if (type == QtInfoMsg) { + line += " \n"; + } else { + line += " ("; + line += file; + line += ":"; + line += QByteArray::number(context.line ? context.line : 0); + line += ", "; + line += function; + line += ")\n"; + } + + return line; +} + +void QetLogger::handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + if (m_disabled) { + legacyStderrOutput(type, context, msg); + return; + } + + static thread_local bool in_handler = false; + if (in_handler) { + // The logger itself triggered a message (e.g. from inside a Qt + // call it made) -- drop it rather than recurse. + return; + } + ReentrancyGuard guard(in_handler); + + const QByteArray sanitized = truncateMessage(sanitize(msg.toUtf8()), kMaxMessageBytes); + const QByteArray line = formatLine(type, context, sanitized); + + fwrite(line.constData(), 1, static_cast(line.size()), stderr); + + m_ring.append(line); + writeToFile(line, type); +} + +void QetLogger::pruneOldLogFiles(int days) +{ + if (m_disabled) { + return; + } + + const QDate today = QDate::currentDate(); + const QStringList filters = { + QStringLiteral("????????.log"), // base files, e.g. 20260803.log + QStringLiteral("????????.?.log"), // rotated files, e.g. 20260803.1.log + }; + + const QDir dir(m_log_dir); + const auto entries = dir.entryInfoList(filters, QDir::Files); + for (const QFileInfo &file_info : entries) { + if (!file_info.isFile()) { + continue; + } + // lastModified(), not lastRead(): reading the log (opening it to + // attach to a bug report, a backup job, an indexer) must not + // reset the retention clock and keep it alive indefinitely. + if (file_info.lastModified().date().daysTo(today) > days) { + QFile::remove(file_info.absoluteFilePath()); + } + } +} diff --git a/sources/logging/qetlogger.h b/sources/logging/qetlogger.h new file mode 100644 index 000000000..eb576bec2 --- /dev/null +++ b/sources/logging/qetlogger.h @@ -0,0 +1,114 @@ +/* + 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 . +*/ +#ifndef QETLOGGER_H +#define QETLOGGER_H + +#include "logring.h" + +#include +#include +#include +#include + +/** + @brief The QetLogger class + Rework of QET's diagnostic logging (discussion #644, steps 1-3): + + - Step 1: one file handle held open for the session under a mutex + instead of opening/closing per message; the log path (including + the date-stamped filename) is resolved exactly once, at init(), + instead of being recomputed on every message -- a session that + crosses midnight now stays in one file; retention now uses + lastModified() instead of lastRead(); stderr and file output both + use UTF-8 explicitly (previously stderr used the local 8-bit + codec and the file's encoding silently differed between Qt5 and + Qt6). + - Step 2: the previously-unbounded daily file is now size-capped + and rotated (kMaxFileBytes per file, kRotationKeep old files kept + beyond the current one); each message is truncated to + kMaxMessageBytes and control characters are escaped before being + written, so one pathological caller can't blow the size budget or + forge log lines; the log file is refused if it already exists as + a symlink and is created owner-read/write only. + - Step 3: every formatted line is also appended to an in-memory + LogRing (see logring.h) -- always on, fixed capacity, allocation- + free on the hot path. + + 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. + + Escape hatch: if QET_LOG_DISABLE=1 is set in the environment at + init() time, this class does nothing beyond a minimal, independent + stderr passthrough -- no ring, no file, no rotation -- so a problem + in this rework can be worked around without a rebuild. +*/ +class QetLogger +{ + public: + static constexpr qint64 kMaxFileBytes = 2 * 1024 * 1024; // 2 MiB per file + static constexpr int kRotationKeep = 4; // .1.log .. .4.log + static constexpr int kMaxMessageBytes = 4096; // per-message truncation + + static QetLogger &instance(); + + /// Must be called exactly once, from main(), before + /// qInstallMessageHandler(). Resolves the log directory and the + /// session's log filename, and opens the file. + void init(); + + /// The function installed via qInstallMessageHandler() forwards here. + void handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg); + + /// Replaces the old delete_old_log_files(): same call shape, fixed + /// to use lastModified() (not lastRead()) and to also match rotated + /// file names. + void pruneOldLogFiles(int days); + + /// Snapshot of the in-memory ring, oldest first. For future use + /// (e.g. a diagnostics export action) -- not wired to any UI here. + QVector ringSnapshot() const {return m_ring.snapshot();} + + private: + QetLogger() = default; + QetLogger(const QetLogger &) = delete; + + bool ensureFileOpenLocked(); + void rotateLocked(); + void writeToFile(const QByteArray &line, QtMsgType type); + QString rotatedPath(int index) const; + + static QByteArray sanitize(const QByteArray &input); + static QByteArray truncateMessage(const QByteArray &input, int max_bytes); + static QByteArray formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg); + + bool m_disabled = false; + + QString m_log_dir; + QString m_base_name; // e.g. "20260803", resolved once in init() + + QMutex m_file_mutex; + QFile m_file; + qint64 m_bytes_written_current_file = 0; + bool m_file_output_ok = false; + + LogRing m_ring; +}; + +#endif // QETLOGGER_H diff --git a/sources/main.cpp b/sources/main.cpp index eede1bfb8..a605f593c 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "cli_export.h" +#include "logging/qetlogger.h" #include "machine_info.h" #include "qet.h" #include "qetapp.h" @@ -62,131 +63,16 @@ class EarlyFileOpenCatcher : public QObject #endif /** - @brief myMessageOutput - for debugging - @param type : the messages that can be sent to a message handler - @param context : were? wat? - @param msg : Message + @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 myMessageOutput(QtMsgType type, +void qetLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { - - QString txt=QTime::currentTime().toString("hh:mm:ss.zzz"); - QByteArray dbs =txt.toLocal8Bit(); - QByteArray localMsg = msg.toLocal8Bit(); - const char *file = context.file ? context.file : ""; - const char *function = context.function ? context.function : ""; - - switch (type) { - case QtDebugMsg: - fprintf(stderr, - "%s Debug: %s (%s:%u, %s)\n", - dbs.constData(), - localMsg.constData(), - file, - context.line, - function); - txt+=" Debug: "; - break; - case QtInfoMsg: - fprintf(stderr, - "%s Info: %s \n", - dbs.constData(), - localMsg.constData()); - txt+=" Info: "; - break; - case QtWarningMsg: - fprintf(stderr, - "%s Warning: %s (%s:%u, %s)\n", - dbs.constData(), - localMsg.constData(), - file, context.line, - function); - txt+=" Warning: "; - break; - case QtCriticalMsg: - fprintf(stderr, - "%s Critical: %s (%s:%u, %s)\n", - dbs.constData(), - localMsg.constData(), - file, - context.line, - function); - txt+=" Critical: "; - break; - case QtFatalMsg: - fprintf(stderr, - "%s Fatal: %s (%s:%u, %s)\n", - dbs.constData(), - localMsg.constData(), - file, - context.line, - function); - txt+=" Fatal: "; - break; - default: - fprintf(stderr, - "%s Unknown: %s (%s:%u, %s)\n", - dbs.constData(), - localMsg.constData(), - file, - context.line, - function); - txt+=" Unknown: "; - } - txt+= msg; - if(type==QtInfoMsg){ - txt+=" \n"; - } else { - txt+= " ("; - txt+= context.file ? context.file : ""; - txt+= ":"; - txt+=QString::number(context.line ? context.line :0); - txt+= ", "; - txt+= context.function ? context.function : ""; - txt+=")\n"; - } - QFile outFile(QETApp::dataDir() - +"/" - +QDate::currentDate().toString("yyyyMMdd") - +".log"); - if(outFile.open(QIODevice::WriteOnly | QIODevice::Append)) - { - QTextStream ts(&outFile); - ts << txt; - } - outFile.close(); -} - -/** - @brief delete_old_log_files - delete old log files - @param days : max days old -*/ -void delete_old_log_files(int days) -{ - const QDate today = QDate::currentDate(); - const QString path = QETApp::dataDir() % "/"; - - QString filter("%1%1%1%1%1%1%1%1.log"); // pattern - filter = filter.arg("[0123456789]"); // valid characters - - Q_FOREACH (auto fileInfo, - QDir(path).entryInfoList( - QStringList(filter), - QDir::Files)) - { - if (fileInfo.lastRead().date().daysTo(today) > days) - { - QString filepath = fileInfo.absoluteFilePath(); - QDir deletefile; - deletefile.setPath(filepath); - deletefile.remove(filepath); - qDebug() << "File " % filepath % " is deleted!"; - } - } + QetLogger::instance().handleMessage(type, context, msg); } /** @@ -253,13 +139,19 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto } } + // 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. - qInstallMessageHandler(myMessageOutput); + QetLogger::instance().init(); + qInstallMessageHandler(qetLogMessageHandler); SingleApplication app(argc, argv, true); #ifdef Q_OS_MACOS @@ -308,7 +200,7 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto { qInfo("Start-up"); // delete old log files of max 7 days old. - delete_old_log_files(7); + QetLogger::instance().pruneOldLogFiles(7); MachineInfo::instance()->send_info_to_debug(); }); return app.exec();