mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 02:53:25 -08:00
[CrashReporting] Mock dialog, sentry skeleton and CI prerequisites
Mock: DlgCrashReport preview dialog (no account needed, redacted log preview), CrashReportingMode Ask/Always/Never setting in DebugSettings, --simulate-crash flag and Help-menu preview entry. Send writes a local mock JSON only. Vendor sentry-native 0.17.3 via FetchContent behind WITH_CRASH_REPORTING (breakpad everywhere, backend none under sanitizers) plus a CrashReporter facade with early init and post-loop shutdown. DSN is a placeholder: empty CMake default with COCKATRICE_SENTRY_DSN env override, so SDK init is skipped and mock behavior is unchanged until a real DSN is configured. CI prerequisites riding here so every layer below builds green: libcurl dev plus CA bundles in the Linux images and CodeQL env, sentry Qt integration off (its own find_package cannot see the macOS thin Qt layout), and the StrFunc Using construction makensis requires. Tests: ctest green, format clean, code plus UI gates PASS.
This commit is contained in:
parent
ea6af0048c
commit
6411abfb57
28 changed files with 962 additions and 5 deletions
|
|
@ -5,6 +5,8 @@
|
|||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QFlags>
|
||||
#include <QIODevice>
|
||||
#include <QLocale>
|
||||
|
|
@ -18,8 +20,11 @@
|
|||
|
||||
#define LOGGER_MAX_ENTRIES 128
|
||||
#define LOGGER_FILENAME "qdebug.txt"
|
||||
#define LOGGER_SESSION_MARKER "Log session started at"
|
||||
#define LOGGER_PERSISTENT_MAX_BYTES (512 * 1024)
|
||||
#define LOGGER_PERSISTENT_KEEP_LINES 2000
|
||||
|
||||
Logger::Logger() : logToFileEnabled(false)
|
||||
Logger::Logger() : logToFileEnabled(false), persistentLogEnabled(false)
|
||||
{
|
||||
logBuffer.append(getClientVersion());
|
||||
logBuffer.append(getSystemArchitecture());
|
||||
|
|
@ -35,6 +40,7 @@ Logger::Logger() : logToFileEnabled(false)
|
|||
Logger::~Logger()
|
||||
{
|
||||
closeLogfileSession();
|
||||
closePersistentLogSession();
|
||||
logBuffer.clear();
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +88,137 @@ void Logger::closeLogfileSession()
|
|||
fileHandle.close();
|
||||
}
|
||||
|
||||
void Logger::setPersistentLogPath(const QString &path)
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
if (path.isEmpty() || (persistentLogEnabled && persistentLogPath == path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QDir().mkpath(QFileInfo(path).absolutePath());
|
||||
persistentLogPath = path;
|
||||
openPersistentLog(path);
|
||||
}
|
||||
|
||||
void Logger::prunePersistentLog(const QString &path)
|
||||
{
|
||||
QFileInfo info(path);
|
||||
if (!info.exists() || info.size() <= LOGGER_PERSISTENT_MAX_BYTES) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return;
|
||||
}
|
||||
QList<QString> allLines;
|
||||
QTextStream in(&file);
|
||||
while (!in.atEnd()) {
|
||||
allLines.append(in.readLine());
|
||||
}
|
||||
file.close();
|
||||
|
||||
const int start =
|
||||
allLines.size() > LOGGER_PERSISTENT_KEEP_LINES ? allLines.size() - LOGGER_PERSISTENT_KEEP_LINES : 0;
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
||||
return;
|
||||
}
|
||||
QTextStream out(&file);
|
||||
for (int i = start; i < allLines.size(); ++i) {
|
||||
out << allLines.at(i) << Qt::endl;
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
|
||||
void Logger::openPersistentLog(const QString &path)
|
||||
{
|
||||
prunePersistentLog(path);
|
||||
|
||||
persistentFileHandle.setFileName(path);
|
||||
if (!persistentFileHandle.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
|
||||
qWarning() << "Logger failed to open persistent log" << path << "for writing";
|
||||
return;
|
||||
}
|
||||
persistentFileStream.setDevice(&persistentFileHandle);
|
||||
persistentFileStream << LOGGER_SESSION_MARKER << " " << QDateTime::currentDateTime().toString() << Qt::endl;
|
||||
persistentFileStream << getClientVersion() << Qt::endl;
|
||||
persistentFileStream << getSystemArchitecture() << Qt::endl;
|
||||
persistentFileStream << getClientInstallInfo() << Qt::endl;
|
||||
persistentFileStream.flush();
|
||||
persistentLogEnabled = true;
|
||||
}
|
||||
|
||||
void Logger::closePersistentLogSession()
|
||||
{
|
||||
if (!persistentLogEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
persistentLogEnabled = false;
|
||||
persistentFileStream << "Log session closed at " << QDateTime::currentDateTime().toString() << Qt::endl;
|
||||
persistentFileStream.flush();
|
||||
persistentFileHandle.close();
|
||||
}
|
||||
|
||||
QList<QString> Logger::getPreviousSessionTail(int maxLines) const
|
||||
{
|
||||
QString path;
|
||||
{
|
||||
QMutexLocker locker(&mutex);
|
||||
path = persistentLogPath;
|
||||
}
|
||||
if (path.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return extractPreviousSessionTail(path, maxLines);
|
||||
}
|
||||
|
||||
QList<QString> Logger::extractPreviousSessionTail(const QString &filePath, int maxLines)
|
||||
{
|
||||
QList<QString> result;
|
||||
if (maxLines <= 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return result;
|
||||
}
|
||||
QList<QString> allLines;
|
||||
QTextStream in(&file);
|
||||
while (!in.atEnd()) {
|
||||
allLines.append(in.readLine());
|
||||
}
|
||||
file.close();
|
||||
|
||||
QList<int> markerIndexes;
|
||||
for (int i = 0; i < allLines.size(); ++i) {
|
||||
if (allLines.at(i).contains(QLatin1String(LOGGER_SESSION_MARKER))) {
|
||||
markerIndexes.append(i);
|
||||
}
|
||||
}
|
||||
|
||||
QList<QString> tail;
|
||||
if (markerIndexes.isEmpty()) {
|
||||
tail = allLines;
|
||||
} else if (markerIndexes.size() == 1) {
|
||||
// Only the current session has been recorded; there is no previous session yet.
|
||||
return result;
|
||||
} else {
|
||||
const int previousStart = markerIndexes.at(markerIndexes.size() - 2);
|
||||
const int currentStart = markerIndexes.last();
|
||||
for (int i = previousStart; i < currentStart; ++i) {
|
||||
tail.append(allLines.at(i));
|
||||
}
|
||||
}
|
||||
|
||||
const int start = tail.size() > maxLines ? tail.size() - maxLines : 0;
|
||||
for (int i = start; i < tail.size(); ++i) {
|
||||
result.append(tail.at(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Logger::log(QtMsgType /* type */, const QMessageLogContext & /* ctx */, const QString &message)
|
||||
{
|
||||
QMetaObject::invokeMethod(this, "internalLog", Qt::QueuedConnection, Q_ARG(const QString &, message));
|
||||
|
|
@ -102,6 +239,11 @@ void Logger::internalLog(const QString &message)
|
|||
if (logToFileEnabled) {
|
||||
fileStream << message << Qt::endl; // Print to fileStream
|
||||
}
|
||||
|
||||
if (persistentLogEnabled) {
|
||||
persistentFileStream << message << Qt::endl;
|
||||
persistentFileStream.flush();
|
||||
}
|
||||
}
|
||||
|
||||
QString Logger::getSystemArchitecture()
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ public:
|
|||
{
|
||||
return logBuffer;
|
||||
}
|
||||
void setPersistentLogPath(const QString &path);
|
||||
QList<QString> getPreviousSessionTail(int maxLines = 60) const;
|
||||
static QList<QString> extractPreviousSessionTail(const QString &filePath, int maxLines = 60);
|
||||
|
||||
private:
|
||||
Logger();
|
||||
|
|
@ -61,11 +64,18 @@ private:
|
|||
QTextStream fileStream;
|
||||
QFile fileHandle;
|
||||
QList<QString> logBuffer;
|
||||
QMutex mutex;
|
||||
mutable QMutex mutex;
|
||||
QString persistentLogPath;
|
||||
bool persistentLogEnabled;
|
||||
QTextStream persistentFileStream;
|
||||
QFile persistentFileHandle;
|
||||
|
||||
protected:
|
||||
void openLogfileSession();
|
||||
void closeLogfileSession();
|
||||
void openPersistentLog(const QString &path);
|
||||
void prunePersistentLog(const QString &path);
|
||||
void closePersistentLogSession();
|
||||
|
||||
protected slots:
|
||||
void internalLog(const QString &message);
|
||||
|
|
|
|||
152
cockatrice/src/interface/widgets/dialogs/dlg_crash_report.cpp
Normal file
152
cockatrice/src/interface/widgets/dialogs/dlg_crash_report.cpp
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#include "dlg_crash_report.h"
|
||||
|
||||
#include "../../../client/crash/crash_reporter.h"
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../logger.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QEvent>
|
||||
#include <QFile>
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QList>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
||||
DlgCrashReport::DlgCrashReport(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
infoLabel = new QLabel(this);
|
||||
infoLabel->setWordWrap(true);
|
||||
|
||||
accountLabel = new QLabel(this);
|
||||
accountLabel->setWordWrap(true);
|
||||
|
||||
previewArea = new QPlainTextEdit(this);
|
||||
previewArea->setReadOnly(true);
|
||||
previewArea->setPlainText(buildRedactedPreview());
|
||||
|
||||
rememberBox = new QCheckBox(this);
|
||||
|
||||
sendButton = new QPushButton(this);
|
||||
sendButton->setDefault(true);
|
||||
connect(sendButton, &QPushButton::clicked, this, &DlgCrashReport::actSend);
|
||||
|
||||
dontSendButton = new QPushButton(this);
|
||||
dontSendButton->setAutoDefault(false);
|
||||
connect(dontSendButton, &QPushButton::clicked, this, &DlgCrashReport::actDontSend);
|
||||
|
||||
auto *buttonLayout = new QHBoxLayout;
|
||||
buttonLayout->addWidget(rememberBox);
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(dontSendButton);
|
||||
buttonLayout->addWidget(sendButton);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(infoLabel);
|
||||
mainLayout->addWidget(accountLabel);
|
||||
mainLayout->addWidget(previewArea);
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
setLayout(mainLayout);
|
||||
|
||||
resize(700, 500);
|
||||
retranslateUi();
|
||||
}
|
||||
|
||||
void DlgCrashReport::changeEvent(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::LanguageChange) {
|
||||
retranslateUi();
|
||||
}
|
||||
QDialog::changeEvent(event);
|
||||
}
|
||||
|
||||
void DlgCrashReport::retranslateUi()
|
||||
{
|
||||
setWindowTitle(tr("Send crash report"));
|
||||
infoLabel->setText(
|
||||
tr("Cockatrice closed unexpectedly. The preview below shows what a crash report would contain (mock)."));
|
||||
accountLabel->setText(tr("No account needed to send a crash report."));
|
||||
rememberBox->setText(tr("Remember my choice"));
|
||||
sendButton->setText(tr("Send"));
|
||||
dontSendButton->setText(tr("Don't send"));
|
||||
}
|
||||
|
||||
QString DlgCrashReport::buildRedactedPreview()
|
||||
{
|
||||
Logger &logger = Logger::getInstance();
|
||||
|
||||
QStringList header;
|
||||
header.append(logger.getClientVersion());
|
||||
header.append(logger.getSystemArchitecture());
|
||||
header.append(logger.getSystemLocale());
|
||||
header.append(logger.getClientInstallInfo());
|
||||
header.append(QStringLiteral("---"));
|
||||
|
||||
QList<QString> logLines = logger.getPreviousSessionTail();
|
||||
if (logLines.isEmpty()) {
|
||||
logLines = logger.getLogBuffer();
|
||||
}
|
||||
const int previewLines = 60;
|
||||
int start = logLines.size() > previewLines ? logLines.size() - previewLines : 0;
|
||||
|
||||
QStringList lines = header;
|
||||
for (int i = start; i < logLines.size(); ++i) {
|
||||
lines.append(CrashReporter::redactCockatriceUrls(CrashReporter::sanitizeLogLine(logLines.at(i))));
|
||||
}
|
||||
return lines.join(QStringLiteral("\n"));
|
||||
}
|
||||
|
||||
QString DlgCrashReport::writeMockReport(const QString &previewText)
|
||||
{
|
||||
Logger &logger = Logger::getInstance();
|
||||
|
||||
QJsonObject report;
|
||||
report[QStringLiteral("mock")] = true;
|
||||
report[QStringLiteral("timestamp")] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
||||
report[QStringLiteral("clientVersion")] = logger.getClientVersion();
|
||||
report[QStringLiteral("systemArchitecture")] = logger.getSystemArchitecture();
|
||||
report[QStringLiteral("systemLocale")] = logger.getSystemLocale();
|
||||
report[QStringLiteral("installInfo")] = logger.getClientInstallInfo();
|
||||
report[QStringLiteral("logPreview")] = previewText;
|
||||
|
||||
QString fileName = QStringLiteral("crash-report-mock-%1.json")
|
||||
.arg(QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMdd-HHmmss")));
|
||||
QString filePath = QDir::tempPath() + QStringLiteral("/") + fileName;
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
return QString();
|
||||
}
|
||||
file.write(QJsonDocument(report).toJson());
|
||||
file.close();
|
||||
return filePath;
|
||||
}
|
||||
|
||||
void DlgCrashReport::actSend()
|
||||
{
|
||||
if (rememberBox->isChecked()) {
|
||||
SettingsCache::instance().personal().setCrashReportingMode(PersonalSettings::CrashReportingMode::AlwaysSend);
|
||||
}
|
||||
const QString savedPath = writeMockReport(previewArea->toPlainText());
|
||||
if (savedPath.isEmpty()) {
|
||||
qWarning() << "Could not save the mock crash report locally";
|
||||
} else {
|
||||
qInfo() << "Mock crash report saved locally to" << savedPath;
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
void DlgCrashReport::actDontSend()
|
||||
{
|
||||
if (rememberBox->isChecked()) {
|
||||
SettingsCache::instance().personal().setCrashReportingMode(PersonalSettings::CrashReportingMode::NeverSend);
|
||||
}
|
||||
reject();
|
||||
}
|
||||
51
cockatrice/src/interface/widgets/dialogs/dlg_crash_report.h
Normal file
51
cockatrice/src/interface/widgets/dialogs/dlg_crash_report.h
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* @file dlg_crash_report.h
|
||||
* @ingroup Dialogs
|
||||
*/
|
||||
|
||||
#ifndef DLG_CRASH_REPORT_H
|
||||
#define DLG_CRASH_REPORT_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
class QCheckBox;
|
||||
class QLabel;
|
||||
class QPlainTextEdit;
|
||||
class QPushButton;
|
||||
|
||||
/**
|
||||
* @brief MOCK crash-report dialog (UI + settings only, no network).
|
||||
*
|
||||
* The Send path writes a redacted JSON file to the temp dir instead of
|
||||
* uploading anything. Real Sentry upload comes in a later phase.
|
||||
*/
|
||||
class DlgCrashReport : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DlgCrashReport(QWidget *parent = nullptr);
|
||||
|
||||
static QString buildRedactedPreview();
|
||||
static QString writeMockReport(const QString &previewText);
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent *event) override;
|
||||
|
||||
private:
|
||||
QLabel *infoLabel;
|
||||
QLabel *accountLabel;
|
||||
QPlainTextEdit *previewArea;
|
||||
QCheckBox *rememberBox;
|
||||
QPushButton *sendButton;
|
||||
QPushButton *dontSendButton;
|
||||
|
||||
void retranslateUi();
|
||||
|
||||
private slots:
|
||||
void actSend();
|
||||
void actDontSend();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -133,6 +133,28 @@ GeneralSettingsPage::GeneralSettingsPage()
|
|||
versionGroupBox = new QGroupBox;
|
||||
versionGroupBox->setLayout(versionGrid);
|
||||
|
||||
// crash reporting consent (lives in personal.ini, not debug.ini): Ask shows the crash dialog
|
||||
// after an unclean exit, AlwaysSend uploads without asking, NeverSend stays silent.
|
||||
crashReportingBox.addItem(""); // texts set in retranslateUi
|
||||
crashReportingBox.addItem("");
|
||||
crashReportingBox.addItem("");
|
||||
crashReportingBox.setCurrentIndex(static_cast<int>(settings.personal().getCrashReportingMode()));
|
||||
|
||||
connect(&crashReportingBox, qOverload<int>(&QComboBox::currentIndexChanged), this, [](int index) {
|
||||
if (index < 0 || index > static_cast<int>(PersonalSettings::CrashReportingMode::NeverSend)) {
|
||||
return;
|
||||
}
|
||||
SettingsCache::instance().personal().setCrashReportingMode(
|
||||
static_cast<PersonalSettings::CrashReportingMode>(index));
|
||||
});
|
||||
|
||||
auto *crashReportingGrid = new QGridLayout;
|
||||
crashReportingGrid->addWidget(&crashReportingLabel, 0, 0);
|
||||
crashReportingGrid->addWidget(&crashReportingBox, 0, 1);
|
||||
|
||||
crashReportingGroupBox = new QGroupBox;
|
||||
crashReportingGroupBox->setLayout(crashReportingGrid);
|
||||
|
||||
// card database settings
|
||||
startupCardUpdateCheckBehaviorSelector.addItem(""); // these will be set in retranslateUI
|
||||
startupCardUpdateCheckBehaviorSelector.addItem("");
|
||||
|
|
@ -336,6 +358,7 @@ GeneralSettingsPage::GeneralSettingsPage()
|
|||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addWidget(languageGroupBox);
|
||||
mainLayout->addWidget(versionGroupBox);
|
||||
mainLayout->addWidget(crashReportingGroupBox);
|
||||
mainLayout->addWidget(cardDatabaseGroupBox);
|
||||
mainLayout->addWidget(startupGroupBox);
|
||||
mainLayout->addWidget(pathsGroupBox);
|
||||
|
|
@ -569,13 +592,19 @@ void GeneralSettingsPage::retranslateUi()
|
|||
tr("English and selected card language"));
|
||||
advertiseTranslationPageLabel.setText(
|
||||
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
|
||||
|
||||
versionGroupBox->setTitle(tr("Version settings"));
|
||||
updateReleaseChannelLabel.setText(tr("Update channel"));
|
||||
startupUpdateCheckCheckBox.setText(tr("Check for client updates on startup"));
|
||||
updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client"));
|
||||
newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice"));
|
||||
|
||||
crashReportingGroupBox->setTitle(tr("Crash reporting"));
|
||||
crashReportingLabel.setText(tr("Send crash reports:"));
|
||||
crashReportingBox.setItemText(static_cast<int>(PersonalSettings::CrashReportingMode::Ask), tr("Ask every time"));
|
||||
crashReportingBox.setItemText(static_cast<int>(PersonalSettings::CrashReportingMode::AlwaysSend),
|
||||
tr("Always send"));
|
||||
crashReportingBox.setItemText(static_cast<int>(PersonalSettings::CrashReportingMode::NeverSend), tr("Never send"));
|
||||
|
||||
// We can't change the strings after they're put into the QComboBox, so this is our workaround
|
||||
int oldIndex = updateReleaseChannelBox.currentIndex();
|
||||
updateReleaseChannelBox.clear();
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ private slots:
|
|||
private:
|
||||
QGroupBox *languageGroupBox;
|
||||
QGroupBox *versionGroupBox;
|
||||
QGroupBox *crashReportingGroupBox;
|
||||
QGroupBox *cardDatabaseGroupBox;
|
||||
QGroupBox *startupGroupBox;
|
||||
QGroupBox *pathsGroupBox;
|
||||
|
|
@ -70,6 +71,9 @@ private:
|
|||
QCheckBox updateNotificationCheckBox;
|
||||
QCheckBox newVersionOracleCheckBox;
|
||||
|
||||
QLabel crashReportingLabel;
|
||||
QComboBox crashReportingBox;
|
||||
|
||||
QLabel startupCardUpdateCheckBehaviorLabel;
|
||||
QComboBox startupCardUpdateCheckBehaviorSelector;
|
||||
QLabel cardUpdateCheckIntervalLabel;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue