Commit f8cf3b1e authored by Harald Sitter's avatar Harald Sitter 🏳️‍🌈
Browse files

new tool drkonqi-coredump-gui

it is a GUI for coredumpctl - duh.

There are two main motivations behind this

- provide more user friendly access to coredumps (and thus allow more
crashes to be reported to the relevant authors)
- it is the foundation for plans to aggregate crashes. right now
multiple crashes result in multiple drkonqis but realistically we could
just feed them all into a singular UI. coredump-gui kind of gets that
started as the UI bits actually aren't coredump related at all so we can
iterate from here

this also contains some rejiggering of the existing coredump code to
facilitate more code sharing through a new internal static library.

architecture-wise the gui is super simple. there's a central Patient
concept which "abstracts" a crashed application. Patients are managed by
a PatientModel (which is really just a generic QObject model) and get
fed into that model by the existing coredumpwatcher tech. the qml bits
then simply view the model/patients. without coredumpd in the picture
the Patients would be synthesized through other means as yet to be
invented.
parent fcca1043
Pipeline #159447 failed with stage
in 1 minute and 16 seconds
......@@ -12,6 +12,7 @@ Dependencies:
'frameworks/kconfigwidgets': '@latest'
'frameworks/kcoreaddons': '@latest'
'frameworks/kcrash': '@latest'
'frameworks/kdeclarative': '@latest'
'frameworks/ki18n': '@latest'
'frameworks/kidletime': '@latest'
'frameworks/kio': '@latest'
......
......@@ -28,7 +28,7 @@ include(ECMDeprecationSettings)
kde_enable_exceptions()
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Widgets Test DBus Concurrent)
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Widgets Test DBus Concurrent Quick)
find_package(
KF5
......@@ -39,6 +39,7 @@ find_package(
CoreAddons
Service
ConfigWidgets
Declarative
JobWidgets
KIO
Crash
......
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
# SPDX-FileCopyrightText: 2021-2022 Harald Sitter <sitter@kde.org>
add_library(drkonqi-coredump STATIC coredump.cpp coredumpwatcher.cpp)
target_link_libraries(drkonqi-coredump PUBLIC Qt::Core Qt::Network Systemd::systemd)
add_subdirectory(cleanup)
add_subdirectory(processor)
add_subdirectory(launcher)
add_subdirectory(gui)
/*
SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2019-2022 Harald Sitter <sitter@kde.org>
*/
#include "coredump.h"
Coredump::Coredump(QByteArray cursor, EntriesHash data)
: m_cursor(std::move(cursor))
, m_rawData(std::move(data))
, uid(m_rawData[QByteArrayLiteral("COREDUMP_UID")].toInt())
, pid(m_rawData[QByteArrayLiteral("COREDUMP_PID")].toInt())
, exe(QString::fromLocal8Bit(m_rawData[QByteArrayLiteral("COREDUMP_EXE")]))
, filename(QString::fromLocal8Bit(m_rawData[keyFilename()]))
, systemd_unit(QString::fromLocal8Bit(m_rawData[QByteArrayLiteral("_SYSTEMD_UNIT")]))
{
}
Coredump::Coredump(const QJsonDocument &document)
: Coredump(QByteArray() /* not from journal, has no cursor */, documentToHash(document))
{
}
QByteArray Coredump::keyFilename()
{
return QByteArrayLiteral("COREDUMP_FILENAME");
}
Coredump::EntriesHash Coredump::documentToHash(const QJsonDocument &document)
{
const QVariantMap variantMap = document.toVariant().toMap();
EntriesHash hash;
for (auto it = variantMap.cbegin(); it != variantMap.cend(); ++it) {
hash.insert(it.key().toUtf8(), it->value<QByteArray>());
}
return hash;
}
/*
SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2019-2021 Harald Sitter <sitter@kde.org>
SPDX-FileCopyrightText: 2019-2022 Harald Sitter <sitter@kde.org>
*/
#pragma once
......@@ -10,34 +10,20 @@
#include <QJsonDocument>
#include <QString>
#include "memory.h"
class Coredump
{
public:
using EntriesHash = QHash<QByteArray, QByteArray>;
Coredump(QByteArray cursor, EntriesHash data)
: m_cursor(std::move(cursor))
, m_rawData(std::move(data))
, uid(m_rawData[QByteArrayLiteral("COREDUMP_UID")].toInt())
, pid(m_rawData[QByteArrayLiteral("COREDUMP_PID")].toInt())
, exe(QString::fromLocal8Bit(m_rawData[QByteArrayLiteral("COREDUMP_EXE")]))
, filename(QString::fromLocal8Bit(m_rawData[keyFilename()]))
, systemd_unit(QString::fromLocal8Bit(m_rawData[QByteArrayLiteral("_SYSTEMD_UNIT")]))
{
}
explicit Coredump(const QJsonDocument &document)
: Coredump(QByteArray() /* not from journal, has no cursor */, documentToHash(document))
{
}
Coredump(QByteArray cursor, EntriesHash data);
explicit Coredump(const QJsonDocument &document);
~Coredump() = default;
// In a function cause it is used in more than one location.
static QByteArray keyFilename()
{
return QByteArrayLiteral("COREDUMP_FILENAME");
}
static QByteArray keyFilename();
// Other bits and bobs
const QByteArray m_cursor;
......@@ -51,14 +37,6 @@ public:
const QString systemd_unit;
private:
static EntriesHash documentToHash(const QJsonDocument &document)
{
const QVariantMap variantMap = document.toVariant().toMap();
EntriesHash hash;
for (auto it = variantMap.cbegin(); it != variantMap.cend(); ++it) {
hash.insert(it.key().toUtf8(), it->value<QByteArray>());
}
return hash;
}
static EntriesHash documentToHash(const QJsonDocument &document);
Q_DISABLE_COPY_MOVE(Coredump)
};
/*
SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2019-2022 Harald Sitter <sitter@kde.org>
*/
#include "coredumpwatcher.h"
#include <cerrno>
#include <optional>
#include <utility>
#include <sys/resource.h>
#include <sys/un.h>
#include <systemd/sd-journal.h>
#include <unistd.h>
#include "coredump.h"
#include "socket.h"
static std::optional<Coredump> makeDump(sd_journal *context)
{
auto cursorExpected = contextual_owning_ptr_call<char>(sd_journal_get_cursor, context);
if (cursorExpected.ret != 0) {
qFatal("Failed to get entry cursor");
return std::nullopt;
}
Coredump::EntriesHash entries;
const void *data = nullptr;
size_t length = 0;
SD_JOURNAL_FOREACH_DATA(context, data, length)
{
// size_t is uint, QBA uses int, make sure we don't overflow the int!
int dataSize = static_cast<int>(length);
Q_ASSERT(dataSize >= 0);
Q_ASSERT(static_cast<quint64>(dataSize) == length);
QByteArray entry(static_cast<const char *>(data), dataSize);
const int offset = entry.indexOf('=');
if (offset < 0) {
qWarning() << "this entry looks funny it has no separating = character" << entry;
continue;
}
const QByteArray key = entry.left(offset);
if (key == QByteArrayLiteral("COREDUMP")) {
// The literal COREDUMP= entry is the actual core when configured for journal storage in coredump.conf.
// Synthesize a filename instead so we can use the same validity checks for all storage types.
entries.insert(Coredump::keyFilename(), QByteArrayLiteral("/dev/null"));
continue;
}
const QByteArray value = entry.mid(offset + 1);
// Always add to raw data, they get serialized back into the INI file for drkonqi.
entries.insert(key, value);
}
return std::make_optional<Coredump>(cursorExpected.value.get(), entries);
}
CoredumpWatcher::CoredumpWatcher(std::unique_ptr<sd_journal> context_, QString bootId_, const QString &instance_, QObject *parent)
: QObject(parent)
, context(std::move(context_))
, bootId(std::move(bootId_))
, instance(instance_)
, instanceFilter(QStringLiteral("systemd-coredump@%1").arg(instance_))
{
}
void CoredumpWatcher::processLog()
{
int i = 0;
while (sd_journal_next(context.get()) > 0) {
++i;
const auto optionalDump = makeDump(context.get());
if (!optionalDump.has_value()) {
qWarning() << "Failed to make a dump :O";
continue;
}
const Coredump &dump = optionalDump.value();
if (!dump.systemd_unit.startsWith(instanceFilter)) {
// Older systemds have trouble templating a correct instance. We only
// perform a startsWith check here, but will filter more aggressively
// whenever possible via the constructor.
continue;
}
if (dump.exe.isEmpty() && dump.filename.isEmpty()) {
qDebug() << "Entry doesn't look like a dump. This may have been a vaccum run. Nothing to process.";
// Do not finish here. Vaccum log entires are created from real coredump processes. We should eventually
// find a dump.
continue;
}
qDebug() << dump.exe << dump.pid << dump.filename;
Q_EMIT newDump(dump);
constexpr int maximumInBatch = 128; // give the event loop a chance to do other stuff as well
if (i >= maximumInBatch) {
// reschedule run
QMetaObject::invokeMethod(this, &CoredumpWatcher::processLog, Qt::QueuedConnection);
return;
}
}
Q_EMIT atLogEnd();
}
void CoredumpWatcher::errnoError(const QString &msg, int err)
{
Q_EMIT error(msg + QStringLiteral(": (%1) ").arg(QString::number(err)) + QString::fromLocal8Bit(strerror(err)));
}
void CoredumpWatcher::start()
{
Q_ASSERT(context);
sd_journal_flush_matches(context.get()); // reset match
if (sd_journal_add_match(context.get(), "SYSLOG_IDENTIFIER=systemd-coredump", 0) != 0) {
Q_EMIT error(QStringLiteral("Failed to install id match"));
return;
}
if (!bootId.isEmpty()) {
const QString bootIdMatch = QStringLiteral("_BOOT_ID=%1").arg(bootId);
if (sd_journal_add_match(context.get(), qPrintable(bootIdMatch), 0) != 0) {
Q_EMIT error(QStringLiteral("Failed to install boot id match"));
return;
}
}
if (!instance.isEmpty()) {
if (instance.count(QLatin1Char('-')) >= 3) {
// older systemds have a bug where %I doesn't actually expand correctly and only contains the first element.
// This prevents us from matching through sd API. Instead processLog will filter based on the instance
// information prefix. It's still unique enough.
// Auto-generated instance names are of the form
// $iid-$pid-$uid where iid is a growing instance id number.
// Additionally we'll filter by chrono proximity, iids that are too far in the past will be discarded.
// This is because iid on its own isn't necessarily unique in the event that it wraps around whatever
// integer limit it has.
if (sd_journal_add_match(context.get(), qPrintable(QStringLiteral("_SYSTEMD_UNIT=%1").arg(instanceFilter)), 0) != 0) {
Q_EMIT error(QStringLiteral("Failed to install unit match"));
return;
}
}
}
// TODO if the user can kauth maybe watch all users; mind in a processor context that is useless!
const QString uidMatch = QStringLiteral("COREDUMP_UID=%1").arg(getuid());
if (sd_journal_add_match(context.get(), qPrintable(uidMatch), 0) != 0) {
Q_EMIT error(QStringLiteral("Failed to install boot id match"));
return;
}
const int fd = sd_journal_get_fd(context.get());
if (fd < 0) {
errnoError(QStringLiteral("Failed to get listening socket"), -fd);
return;
}
notifier = std::make_unique<QSocketNotifier>(fd, QSocketNotifier::Read);
connect(notifier.get(), &QSocketNotifier::activated, this, [this] {
if (sd_journal_process(context.get()) != SD_JOURNAL_APPEND) {
return;
}
processLog();
});
if (int ret = sd_journal_seek_head(context.get()); ret != 0) {
errnoError(QStringLiteral("Failed to go to tail"), -fd);
return;
}
// Make sure to read whatever we have pending on next loop.
QMetaObject::invokeMethod(this, &CoredumpWatcher::processLog);
}
/*
SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
SPDX-FileCopyrightText: 2019-2022 Harald Sitter <sitter@kde.org>
*/
#pragma once
#include <QObject>
#include <QSocketNotifier>
#include <systemd/sd-journal.h>
#include "memory.h"
class Coredump;
class CoredumpWatcher : public QObject
{
Q_OBJECT
public:
explicit CoredumpWatcher(std::unique_ptr<sd_journal> context_, QString bootId_, const QString &instance_, QObject *parent = nullptr);
void start();
Q_SIGNALS:
void finished();
void error(const QString &msg);
void newDump(const Coredump &dump);
/// Emitted when the current iteration has reached the log end. Roughly meaning that it has loaded all past entries.
void atLogEnd();
private:
void processLog();
void errnoError(const QString &msg, int err);
const std::unique_ptr<sd_journal> context = nullptr;
std::unique_ptr<QSocketNotifier> notifier = nullptr;
const QString bootId;
const QString instance;
const QString instanceFilter; // systemd-coredump@%1 instance name
};
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-FileCopyrightText: 2020-2022 Harald Sitter <sitter@kde.org>
add_executable(drkonqi-coredump-gui main.cpp PatientModel.cpp Patient.cpp DetailsLoader.cpp qml/qml.qrc)
target_compile_definitions(drkonqi-coredump-gui
PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>
PRIVATE -DTRANSLATION_DOMAIN=\"drkonqi_coredump_gui\")
target_link_libraries(drkonqi-coredump-gui
DrKonqiInternal
KF5::Declarative
Qt::Quick
KF5::I18n
drkonqi-coredump
)
install(TARGETS drkonqi-coredump-gui ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES org.kde.drkonqi.coredump.gui.desktop DESTINATION ${KDE_INSTALL_APPDIR})
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
#include "DetailsLoader.h"
#include <KLocalizedString>
void DetailsLoader::setPatient(Patient *patient)
{
m_patient = patient;
if (m_patient) {
load();
} else {
m_LoaderProcess = nullptr;
}
}
void DetailsLoader::load()
{
m_LoaderProcess = std::make_unique<QProcess>();
m_LoaderProcess->setProgram(QStringLiteral("coredumpctl"));
m_LoaderProcess->setArguments(m_patient->coredumpctlArguments(QStringLiteral("info")));
m_LoaderProcess->setProcessChannelMode(QProcess::MergedChannels);
connect(m_LoaderProcess.get(), &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
switch (exitStatus) {
case QProcess::NormalExit:
if (exitCode == 0) {
Q_EMIT details(QString::fromLocal8Bit(m_LoaderProcess->readAll()));
} else {
Q_EMIT error(i18nc("@info", "Subprocess exited with error: %1", QString::fromLocal8Bit(m_LoaderProcess->readAll())));
}
break;
case QProcess::CrashExit:
Q_EMIT error(i18nc("@info", "Subprocess crashed. Check your installation."));
break;
}
m_LoaderProcess = nullptr;
});
m_LoaderProcess->start();
}
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
#pragma once
#include <memory>
#include <QObject>
#include <QProcess>
#include "Patient.h"
Q_DECLARE_METATYPE(Patient *)
class DetailsLoader : public QObject
{
Q_OBJECT
Q_PROPERTY(Patient *patient MEMBER m_patient WRITE setPatient NOTIFY patientChanged)
Patient *m_patient = nullptr;
void setPatient(Patient *patient);
Q_SIGNAL void patientChanged();
public:
using QObject::QObject;
Q_SIGNALS:
void details(const QString &details);
void error(const QString &error);
private:
void load();
std::unique_ptr<QProcess> m_LoaderProcess;
};
#!/bin/sh
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-FileCopyrightText: 2020-2022 Harald Sitter <sitter@kde.org>
# Our l10n scripting isn't working with spaces anywhere and we actively rely on word splitting in our Messages.sh.
# shellcheck disable=SC2046
${podir:?} # ensure it is defined
$XGETTEXT $(find . -name \*.cpp -o -name \*.h) -o "$podir"/drkonqi5_coredump_gui.pot
# Extract JavaScripty files as what they are, otherwise for example template literals won't work correctly (by default we extract as C++).
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
$XGETTEXT --join-existing --language=JavaScript $(find . -name \*.qml -o -name \*.js) -o "$podir"/drkonqi5_coredump_gui.pot
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2020-2022 Harald Sitter <sitter@kde.org>
#include "Patient.h"
#include <QDebug>
#include <QFileInfo>
#include <QProcess>
#include <KShell>
#include <KTerminalLauncherJob>
#include "../coredump/coredump.h"
Patient::Patient(const Coredump &dump)
: m_signal(dump.m_rawData["COREDUMP_SIGNAL"].toInt())
, m_appName(QFileInfo(dump.exe).fileName())
, m_pid(dump.pid)
, m_canDebug(QFileInfo::exists(QString::fromUtf8(dump.m_rawData.value("COREDUMP_FILENAME"))))
, m_timestamp(dump.m_rawData["COREDUMP_TIMESTAMP"].toLong())
, m_coredumpExe(dump.m_rawData["COREDUMP_EXE"])
, m_coredumpCom(dump.m_rawData["COREDUMP_COMM"])
{
}
QStringList Patient::coredumpctlArguments(const QString &command) const
{
return {command, QString::number(m_pid), QString::fromUtf8(m_coredumpExe), QString::fromUtf8(m_coredumpCom)};
}
void Patient::debug() const
{
const QString arguments = KShell::joinArgs(coredumpctlArguments(QStringLiteral("debug")));
auto job = new KTerminalLauncherJob(QStringLiteral("coredumpctl %1").arg(arguments));
connect(job, &KJob::result, this, [job] {
job->deleteLater();
if (job->error() != KJob::NoError) {
qWarning() << job->errorText();
}
});
job->start();
}
QString Patient::dateTime() const
{
QDateTime time;
time.setMSecsSinceEpoch(m_timestamp / 1000);
return time.toString();
}
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2020-2022 Harald Sitter <sitter@kde.org>
#pragma once
#include <QObject>
class Coredump;
class Patient : public QObject
{
Q_OBJECT
#define MEMBER_PROPERTY(type, name) \
Q_PROPERTY(type name MEMBER m_##name NOTIFY changed) \
type m_##name
// NB: these all share the same changed signal but its only ever emitted once.
// They are effectively CONSTANT, but not really because they change during construction.
MEMBER_PROPERTY(int, signal) = -1;
MEMBER_PROPERTY(QString, appName);
MEMBER_PROPERTY(pid_t, pid) = 0;
MEMBER_PROPERTY(bool, canDebug) = false;
MEMBER_PROPERTY(time_t, timestamp) = 0;
#undef MEMBER_PROPERTY
Q_PROPERTY(QString dateTime READ dateTime NOTIFY changed)
public:
explicit Patient(const Coredump &dump);
QStringList coredumpctlArguments(const QString &command) const;
Q_INVOKABLE void debug() const;
QString dateTime() const;
Q_SIGNALS:
void changed();
private:
const QByteArray m_coredumpExe;
const QByteArray m_coredumpCom;
};
Q_DECLARE_METATYPE(time_t)
Q_DECLARE_METATYPE(pid_t)
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
// SPDX-FileCopyrightText: 2020-2022 Harald Sitter <sitter@kde.org>
#include "PatientModel.h"
#include <chrono>
#include <QDebug>
#include <QMetaMethod>
using namespace std::chrono_literals;
PatientModel::PatientModel(const QMetaObject &mo, QObject *parent)
: QAbstractListModel(parent)
{
initRoleNames(mo);
}
QHash<int, QByteArray> PatientModel::roleNames() const
{
return m_roles;
}
int PatientModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent); // this is a flat list we decidedly don't care about the parent
return m_objects.count();
}
QVariant PatientModel::data(const QModelIndex &index, int role) const
{
if (!hasIndex(index.row(), index.column())) {
return {};
}
// return QVariant::fromValue((QObject *)0x1);
QObject *obj = m_objects.at(index.row());
switch ((ItemRole)role) {
case ObjectRole:
return QVariant::fromValue(obj);
case IndexRole:
return QVariant::fromValue(index.row());
}
const QByteArray prop = m_objectProperties.value(role);