mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-27 20:44:13 +02:00
f8c1b5206a
Five things raised in review, plus tests for the parts that were only described in prose. clearPendingCrashDump() did not do what its comment said. It called pendingCrashDumpFiles() again at clear time, so it deleted whatever was in the directory then, not what had been offered. The offer sits inside a modal dialog that stays open as long as the user reads it, and SingleApplication keys its socket on the binary path, so a second QElectroTech build running alongside is a separate process that can crash and write a dump in that window. Re-listing deleted that dump unseen -- the exact failure this change exists to fix. The list is now taken once in QETApp::checkCrashDump() and passed to both pendingCrashDumpContents() and clearPendingCrashDump(). The ring is now written before the backtrace. backtrace() unwinds through libgcc, which calls dl_iterate_phdr and takes the loader lock; warming it in install() removes the allocation but not the lock. Crashing inside dlopen() (Qt plugin loading), or on a corrupted stack, could therefore hang or re-fault the handler at the backtrace and lose the ring with it. Order is now header, signal, ring, backtrace, so the cheapest and most valuable part is already on disk before anything that can block. The class comment claimed the handler takes no locks; that was not strictly true and now says so. QET_CRASH_BACKTRACE comes from find_package(Backtrace) rather than __has_include(<execinfo.h>). The header exists on FreeBSD but backtrace() lives in libexecinfo there, so the probe compiled and the link failed. A crash_dump.log left by a pre-#905 version is migrated into crashes/ at startup, named from its own mtime. Otherwise upgrading stranded it: the new code never looks at that path, so the dump from the crash that prompted the upgrade would sit there unoffered forever. Also from the review: dumps are capped at the 10 newest, so a crash loop cannot fill the log directory before any dialog is shown; crashDumpDir() no longer creates the directory as a side effect of a const getter (ensureCrashDumpDir() does that for the callers that write); and redact() now masks an AppImage's per-run /tmp/.mount_XXXXXX prefix, which backtrace_symbols_fd() writes into every frame. Two test executables, both of which were checked to fail against the behaviour they replace: - tst_crashhandler covers CrashHandler::formatInt(), which had no coverage at all despite running only inside a signal handler, where nothing can assert: zero, negatives, INT_MIN (negated through unsigned, since -INT_MIN is UB), INT_MAX, truncation and a zero-sized buffer, each checked against a sentinel-filled buffer so a write past the reported length fails. - tst_crashdumps covers the bookkeeping: ordering, empty dumps, the exclusion of this run's own path, the cap, concatenation of every offered dump, that clearing deletes only what was offered, and what redact() masks. qetlogger.cpp needs exactly one symbol from the application, QETApp::dataDir(), which the test supplies itself. Not addressed here: the timestamp in crash_<timestamp>_<pid> is the launch time, not the crash time -- correct as observed, and the commit message that implied otherwise was the thing that was wrong. Resolvable QET frames for AppImage/Flatpak/Snap/Debian need -rdynamic and archived debug symbols, which is a packaging discussion, not this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
652 lines
19 KiB
C++
652 lines
19 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 "qetlogger.h"
|
|
|
|
#include "crashhandler.h"
|
|
#include "../qetapp.h"
|
|
#include "../qetversion.h"
|
|
|
|
#include <QCoreApplication>
|
|
#include <QDateTime>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QSysInfo>
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
|
|
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();
|
|
}
|
|
|
|
void QetLogger::installCrashHandler()
|
|
{
|
|
if (m_disabled) {
|
|
return;
|
|
}
|
|
//Both run here, in normal startup context, before this run's own
|
|
//dump path is fixed: a dump left by a pre-#905 version is moved
|
|
//in so it can still be offered, and any backlog is trimmed.
|
|
migrateLegacyCrashDump();
|
|
pruneCrashDumps();
|
|
|
|
//Fixed for the life of the process: the handler copies it into a
|
|
//preallocated buffer, and pendingCrashDumpFiles() needs to know
|
|
//which file is this run's own so it doesn't offer it back.
|
|
m_crash_dump_path = buildCrashDumpPath();
|
|
CrashHandler::install(&m_ring, m_crash_dump_path);
|
|
}
|
|
|
|
/**
|
|
@brief QetLogger::crashDumpDir
|
|
@return the directory holding crash dumps.
|
|
|
|
A directory rather than a single file, because dumps are per-run and
|
|
several can be waiting at once. Creates nothing: a getter that made a
|
|
directory as a side effect surprised a reviewer on #905, and the
|
|
readers here (listing, pruning) have no business creating it.
|
|
*/
|
|
QString QetLogger::crashDumpDir() const
|
|
{
|
|
return m_log_dir % QStringLiteral("/crashes");
|
|
}
|
|
|
|
/**
|
|
@brief QetLogger::ensureCrashDumpDir
|
|
@return crashDumpDir(), created if missing.
|
|
|
|
For the callers that are about to write into it.
|
|
*/
|
|
QString QetLogger::ensureCrashDumpDir() const
|
|
{
|
|
const QString dir = crashDumpDir();
|
|
QDir().mkpath(dir);
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
@brief QetLogger::migrateLegacyCrashDump
|
|
|
|
Before #905 the handler wrote to a single m_log_dir/crash_dump.log.
|
|
After upgrading, nothing looks at that path any more: the dump of the
|
|
crash that quite possibly prompted the upgrade would sit there unseen
|
|
and undeleted forever. Move it into crashes/ under a name the
|
|
crash_*.log filter matches, so it is offered exactly once like any
|
|
other. Named from its own mtime, so it sorts by when it was written
|
|
rather than when it was moved.
|
|
*/
|
|
void QetLogger::migrateLegacyCrashDump() const
|
|
{
|
|
const QFileInfo legacy(m_log_dir % QStringLiteral("/crash_dump.log"));
|
|
if (!legacy.exists() || !legacy.isFile() || legacy.size() <= 0) {
|
|
return;
|
|
}
|
|
|
|
const QString target = ensureCrashDumpDir()
|
|
% QStringLiteral("/crash_")
|
|
% legacy.lastModified().toString(QStringLiteral("yyyyMMdd-hhmmss"))
|
|
% QStringLiteral("_legacy.log");
|
|
|
|
if (QFile::exists(target)) {
|
|
//Migrated already by an earlier run of this version.
|
|
QFile::remove(legacy.absoluteFilePath());
|
|
return;
|
|
}
|
|
QFile::rename(legacy.absoluteFilePath(), target);
|
|
}
|
|
|
|
/**
|
|
@brief QetLogger::pruneCrashDumps
|
|
|
|
A crash that repeats on startup would otherwise write one dump per
|
|
attempt without limit, since nothing is deleted until a dialog is
|
|
actually shown and answered. Keep the newest kMaxPendingCrashDumps --
|
|
enough to see a pattern, bounded however long the loop runs.
|
|
*/
|
|
void QetLogger::pruneCrashDumps() const
|
|
{
|
|
QDir dir(crashDumpDir());
|
|
if (!dir.exists()) {
|
|
return;
|
|
}
|
|
dir.setNameFilters({QStringLiteral("crash_*.log")});
|
|
dir.setFilter(QDir::Files);
|
|
dir.setSorting(QDir::Time);
|
|
|
|
const QFileInfoList entries = dir.entryInfoList();
|
|
for (int i = kMaxPendingCrashDumps ; i < entries.size() ; ++i) {
|
|
QFile::remove(entries.at(i).absoluteFilePath());
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief QetLogger::buildCrashDumpPath
|
|
@return where this run would write a crash dump.
|
|
|
|
One file per run, rather than a single fixed crash_dump.log. That old
|
|
scheme opened one path with O_TRUNC, so a second crash overwrote the
|
|
first: someone who crashed ten times still ended up with exactly one
|
|
dump, the most recent. Reported on #898 -- "the report appeared only
|
|
once despite there being 10 or more crashes" -- where losing the
|
|
earlier dumps mattered as much as never being shown them.
|
|
|
|
Built here in normal context and handed to CrashHandler::install(),
|
|
which copies it into a preallocated buffer, so the handler still
|
|
writes to one fixed path and its no-allocation invariant is untouched.
|
|
*/
|
|
QString QetLogger::buildCrashDumpPath() const
|
|
{
|
|
return ensureCrashDumpDir()
|
|
% QStringLiteral("/crash_")
|
|
% QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd-hhmmss"))
|
|
% QStringLiteral("_")
|
|
% QString::number(QCoreApplication::applicationPid())
|
|
% QStringLiteral(".log");
|
|
}
|
|
|
|
/**
|
|
@brief QetLogger::pendingCrashDumpFiles
|
|
@return dumps left by previous runs, newest first, at most
|
|
kMaxPendingCrashDumps of them.
|
|
|
|
This run's own path is excluded: it does not exist yet unless this run
|
|
is itself crashing, and a handler mid-crash is in no position to be
|
|
offered a dialog.
|
|
|
|
Callers take this list once and pass it on to
|
|
pendingCrashDumpContents() and clearPendingCrashDump(), rather than
|
|
each of those re-reading the directory. See clearPendingCrashDump().
|
|
*/
|
|
QStringList QetLogger::pendingCrashDumpFiles() const
|
|
{
|
|
QDir dir(crashDumpDir());
|
|
if (!dir.exists()) {
|
|
return QStringList();
|
|
}
|
|
dir.setNameFilters({QStringLiteral("crash_*.log")});
|
|
dir.setFilter(QDir::Files);
|
|
dir.setSorting(QDir::Time);
|
|
|
|
QStringList files;
|
|
const QFileInfoList entries = dir.entryInfoList();
|
|
for (const QFileInfo &info : entries)
|
|
{
|
|
if (info.size() <= 0) {
|
|
continue;
|
|
}
|
|
if (!m_crash_dump_path.isEmpty()
|
|
&& info.absoluteFilePath() == QFileInfo(m_crash_dump_path).absoluteFilePath()) {
|
|
continue;
|
|
}
|
|
files << info.absoluteFilePath();
|
|
if (files.size() >= kMaxPendingCrashDumps) {
|
|
break;
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
QString QetLogger::currentLogFilePath() const
|
|
{
|
|
return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log");
|
|
}
|
|
|
|
/**
|
|
@brief QetLogger::ensureFileOpenLocked
|
|
Caller must hold m_file_mutex. Opens the current session's log file
|
|
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 = currentLogFilePath();
|
|
|
|
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 = currentLogFilePath();
|
|
|
|
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<char>(c);
|
|
} else if (c < 0x20 || c == 0x7F) {
|
|
out += "\\x";
|
|
out += QByteArray::number(c, 16).rightJustified(2, '0');
|
|
} else {
|
|
out += static_cast<char>(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<size_t>(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());
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Step 5: getting the data back out ----------------------------------
|
|
|
|
bool QetLogger::hasPendingCrashDump() const
|
|
{
|
|
if (m_disabled) {
|
|
return false;
|
|
}
|
|
return !pendingCrashDumpFiles().isEmpty();
|
|
}
|
|
|
|
/**
|
|
@brief QetLogger::pendingCrashDumpContents
|
|
@param files the list from pendingCrashDumpFiles()
|
|
@return those dumps, in the order given, concatenated.
|
|
|
|
All of them rather than only the latest: a crash that repeats is the
|
|
case where the earlier dumps are most worth having, since the
|
|
difference between them is the evidence. They are separated by a
|
|
banner so a reader can tell where one ends and the next begins, and
|
|
the whole thing is redacted as a single pass.
|
|
*/
|
|
QByteArray QetLogger::pendingCrashDumpContents(const QStringList &files) const
|
|
{
|
|
if (files.isEmpty()) {
|
|
return QByteArray();
|
|
}
|
|
|
|
QByteArray all;
|
|
if (files.size() > 1) {
|
|
all += QByteArray("QET: ") + QByteArray::number(files.size())
|
|
+ " crash dumps pending, newest first.\n\n";
|
|
}
|
|
|
|
for (const QString &path : files)
|
|
{
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
continue;
|
|
}
|
|
all += "===== " + QFileInfo(path).fileName().toUtf8() + " =====\n";
|
|
all += file.readAll();
|
|
if (!all.endsWith('\n')) {
|
|
all += '\n';
|
|
}
|
|
all += '\n';
|
|
}
|
|
|
|
return redact(all);
|
|
}
|
|
|
|
/**
|
|
@brief QetLogger::clearPendingCrashDump
|
|
@param files exactly the dumps that were offered
|
|
|
|
Deletes the list it is given rather than re-reading the directory.
|
|
The offer sits inside a modal dialog that can stay open for as long
|
|
as the user cares to read it, and dumps are per-run: a second
|
|
QElectroTech -- SingleApplication keys its socket on the binary path,
|
|
so a different build is a separate instance -- can crash and write a
|
|
new dump while that dialog is up. Re-listing at this point would
|
|
delete that fresh dump without anyone ever having seen it, which is
|
|
the failure this whole change is about.
|
|
*/
|
|
void QetLogger::clearPendingCrashDump(const QStringList &files)
|
|
{
|
|
for (const QString &path : files) {
|
|
QFile::remove(path);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
QByteArray out = input;
|
|
|
|
const QByteArray home = QDir::homePath().toUtf8();
|
|
if (!home.isEmpty()) {
|
|
out.replace(home, QByteArrayLiteral("~"));
|
|
}
|
|
|
|
//backtrace_symbols_fd() writes the absolute path of each module,
|
|
//which for an AppImage is the per-run mount point
|
|
///tmp/.mount_QElectXXXXXX. Not identifying on its own, but it is
|
|
//noise in a bug report and it is a path the user never typed, so
|
|
//fold it to a stable name. Done after the home replacement above
|
|
//because the mount point is not under $HOME.
|
|
const QByteArray mount_prefix("/tmp/.mount_");
|
|
int at = out.indexOf(mount_prefix);
|
|
while (at >= 0)
|
|
{
|
|
int end = at + mount_prefix.size();
|
|
while (end < out.size() && out.at(end) != '/' && !isspace(static_cast<unsigned char>(out.at(end)))) {
|
|
++end;
|
|
}
|
|
out.replace(at, end - at, QByteArrayLiteral("<appimage>"));
|
|
at = out.indexOf(mount_prefix, at + 10);
|
|
}
|
|
|
|
return out;
|
|
}
|