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
116
cockatrice/src/client/crash/crash_reporter.cpp
Normal file
116
cockatrice/src/client/crash/crash_reporter.cpp
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#include "crash_reporter.h"
|
||||
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
#include <sentry.h>
|
||||
#endif
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QRegularExpression>
|
||||
#include <QUrl>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
||||
bool CrashReporter::isReportingEnabled(const QString &dsn, PersonalSettings::CrashReportingMode consentMode)
|
||||
{
|
||||
return !dsn.isEmpty() && consentMode != PersonalSettings::CrashReportingMode::NeverSend;
|
||||
}
|
||||
|
||||
void CrashReporter::init(const QString &dsn,
|
||||
const QString &release,
|
||||
const QString &databasePath,
|
||||
PersonalSettings::CrashReportingMode consentMode)
|
||||
{
|
||||
shutdown();
|
||||
sentryDatabasePath = databasePath;
|
||||
if (!isReportingEnabled(dsn, consentMode)) {
|
||||
return;
|
||||
}
|
||||
// Only created when the SDK will actually run: no-DSN launches stay side-effect free.
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
QDir().mkpath(sentryDatabasePath);
|
||||
if (consentMode != PersonalSettings::CrashReportingMode::AlwaysSend) {
|
||||
// sentry-native persists consent to <database>/user-consent and reloads it in sentry_init
|
||||
// before old runs are processed, so a stale "given" from an earlier AlwaysSend run (or an
|
||||
// earlier Send in Ask mode) would upload the next crash before the dialog asks. Our
|
||||
// CrashReportingMode is the source of truth, so clear the persisted state first; asking
|
||||
// afterwards re-establishes it via giveConsent()/revokeConsent().
|
||||
QFile::remove(sentryDatabasePath + QStringLiteral("/user-consent"));
|
||||
}
|
||||
sentry_options_t *options = sentry_options_new();
|
||||
sentry_options_set_dsn(options, dsn.toUtf8().constData());
|
||||
sentry_options_set_release(options, release.toUtf8().constData());
|
||||
sentry_options_set_database_path(options, sentryDatabasePath.toUtf8().constData());
|
||||
// Consent-gated uploads: crashes are captured into the local database but
|
||||
// nothing leaves the machine until giveConsent().
|
||||
sentry_options_set_require_user_consent(options, 1);
|
||||
ready = (sentry_init(options) == 0);
|
||||
#else
|
||||
Q_UNUSED(release);
|
||||
ready = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool CrashReporter::isReady() const
|
||||
{
|
||||
return ready;
|
||||
}
|
||||
|
||||
void CrashReporter::shutdown()
|
||||
{
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
if (ready) {
|
||||
sentry_close();
|
||||
}
|
||||
#endif
|
||||
ready = false;
|
||||
}
|
||||
|
||||
void CrashReporter::giveConsent()
|
||||
{
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
if (ready) {
|
||||
sentry_user_consent_give();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void CrashReporter::revokeConsent()
|
||||
{
|
||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||
if (ready) {
|
||||
sentry_user_consent_revoke();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
QString CrashReporter::sanitizeLogLine(const QString &line)
|
||||
{
|
||||
static const auto colorEscapeCodePattern = QRegularExpression(QStringLiteral("\033\\[[0-9;]*m"));
|
||||
|
||||
QString sanitized = line;
|
||||
sanitized.replace(colorEscapeCodePattern, "");
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
QString CrashReporter::redactCockatriceUrls(const QString &line)
|
||||
{
|
||||
static auto urlPattern = QRegularExpression(QStringLiteral("cockatrice://\\S+"));
|
||||
|
||||
// Single pass over the original line: rescanning the replaced text would match a redacted
|
||||
// secret-free URL again and loop forever.
|
||||
QString redacted;
|
||||
redacted.reserve(line.size());
|
||||
qsizetype pos = 0;
|
||||
QRegularExpressionMatchIterator it = urlPattern.globalMatch(line);
|
||||
while (it.hasNext()) {
|
||||
QRegularExpressionMatch match = it.next();
|
||||
redacted.append(line.mid(pos, match.capturedStart() - pos));
|
||||
// The pattern only matches cockatrice:// URLs, so the check is folded into the parse:
|
||||
// scheme://host, secrets dropped.
|
||||
const QUrl parsed(match.captured(0));
|
||||
redacted.append(parsed.scheme() + QStringLiteral("://") + parsed.host());
|
||||
pos = match.capturedEnd();
|
||||
}
|
||||
redacted.append(line.mid(pos));
|
||||
return redacted;
|
||||
}
|
||||
63
cockatrice/src/client/crash/crash_reporter.h
Normal file
63
cockatrice/src/client/crash/crash_reporter.h
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* @file crash_reporter.h
|
||||
* @ingroup Core
|
||||
*/
|
||||
|
||||
#ifndef CRASH_REPORTER_H
|
||||
#define CRASH_REPORTER_H
|
||||
|
||||
#include <QString>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
|
||||
#ifndef COCKATRICE_WITH_CRASH_REPORTING
|
||||
#define COCKATRICE_WITH_CRASH_REPORTING 0
|
||||
#endif
|
||||
|
||||
#ifndef COCKATRICE_SENTRY_DSN
|
||||
#define COCKATRICE_SENTRY_DSN ""
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Sentry-backed crash-report facade, owned by main().
|
||||
*
|
||||
* Single responsibility: init/shutdown the sentry-native SDK behind consent
|
||||
* gating, plus the shared log redaction helpers. An empty DSN (or Never
|
||||
* consent) skips the SDK entirely, so the mock dialog fallback behaves exactly
|
||||
* as before. init() arms require-user-consent so cached crash envelopes never
|
||||
* upload before the user is asked; giveConsent()/revokeConsent() (driven by
|
||||
* the reporting-mode setting and the crash dialog) gate uploads, and any stale
|
||||
* persisted consent from a previous AlwaysSend run is cleared before init when
|
||||
* the mode is not AlwaysSend. The real send path is a later increment. All SDK
|
||||
* calls are guarded so OFF builds compile to no-ops.
|
||||
*/
|
||||
class CrashReporter
|
||||
{
|
||||
public:
|
||||
CrashReporter() = default;
|
||||
CrashReporter(const CrashReporter &) = delete;
|
||||
CrashReporter &operator=(const CrashReporter &) = delete;
|
||||
~CrashReporter() = default;
|
||||
|
||||
void init(const QString &dsn,
|
||||
const QString &release,
|
||||
const QString &databasePath,
|
||||
PersonalSettings::CrashReportingMode consentMode);
|
||||
[[nodiscard]] bool isReady() const;
|
||||
void shutdown();
|
||||
void giveConsent();
|
||||
void revokeConsent();
|
||||
|
||||
/**
|
||||
* @brief SDK-independent gating decision, unit-testable in OFF builds.
|
||||
*/
|
||||
static bool isReportingEnabled(const QString &dsn, PersonalSettings::CrashReportingMode consentMode);
|
||||
|
||||
static QString sanitizeLogLine(const QString &line);
|
||||
static QString redactCockatriceUrls(const QString &line);
|
||||
|
||||
private:
|
||||
bool ready = false;
|
||||
QString sentryDatabasePath;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
#include "main.h"
|
||||
|
||||
#include "client/crash/crash_reporter.h"
|
||||
#include "client/network/update/card_spoiler/spoiler_background_updater.h"
|
||||
#include "client/settings/cache_settings.h"
|
||||
#include "client/sound_engine.h"
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
#include "interface/logger.h"
|
||||
#include "interface/pixel_map_generator.h"
|
||||
#include "interface/theme_manager.h"
|
||||
#include "interface/widgets/dialogs/dlg_crash_report.h"
|
||||
#include "interface/window_main.h"
|
||||
#include "libcockatrice/card/database/card_database.h"
|
||||
#include "libcockatrice/rng/rng_abstract.h"
|
||||
|
|
@ -44,6 +46,9 @@
|
|||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDebug>
|
||||
#include <QDialog>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QIcon>
|
||||
|
|
@ -259,6 +264,34 @@ int main(int argc, char *argv[])
|
|||
qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true);
|
||||
#endif
|
||||
|
||||
// Crash reporting (increment 1): init the SDK early so a crash anywhere below is captured into the
|
||||
// local database. Uploads stay consent-gated (require_user_consent, armed in CrashReporter::init):
|
||||
// AlwaysSend gives consent here so cached envelopes flush at startup; any other mode starts
|
||||
// unconsented and the dialog below drives give/revoke. An empty DSN skips the SDK entirely and
|
||||
// the mock fallback further below behaves exactly as before.
|
||||
CrashReporter crashReporter;
|
||||
const QString crashDatabasePath = SettingsCache::instance().getSettingsPath() + QStringLiteral("sentry");
|
||||
{
|
||||
QString crashDsn = QString::fromUtf8(qgetenv("COCKATRICE_SENTRY_DSN"));
|
||||
if (crashDsn.isEmpty()) {
|
||||
crashDsn = QString::fromLatin1(COCKATRICE_SENTRY_DSN);
|
||||
}
|
||||
crashReporter.init(crashDsn, QString::fromLatin1(VERSION_STRING), crashDatabasePath,
|
||||
SettingsCache::instance().personal().getCrashReportingMode());
|
||||
if (crashReporter.isReady() && SettingsCache::instance().personal().getCrashReportingMode() ==
|
||||
PersonalSettings::CrashReportingMode::AlwaysSend) {
|
||||
// Silent opt-in: cached crash envelopes flush automatically once consent is given.
|
||||
crashReporter.giveConsent();
|
||||
}
|
||||
}
|
||||
|
||||
// Persistent session log: keeps this run's logs on disk so the next-startup crash dialog can
|
||||
// attach the crashed session's tail (the in-memory buffer is lost on crash). Opened in Append
|
||||
// mode with a per-launch session marker; read back via Logger::getPreviousSessionTail().
|
||||
const QString persistentLogPath =
|
||||
SettingsCache::instance().getSettingsPath() + QStringLiteral("logs/cockatrice.log");
|
||||
Logger::getInstance().setPersistentLogPath(persistentLogPath);
|
||||
|
||||
// Translations
|
||||
#ifdef Q_OS_MAC
|
||||
translationPath = qApp->applicationDirPath() + "/../Resources/translations";
|
||||
|
|
@ -276,7 +309,9 @@ int main(int argc, char *argv[])
|
|||
|
||||
parser.addOptions(
|
||||
{{{"c", "connect"}, QCoreApplication::translate("main", "Connect on startup"), "user:pass@host:port"},
|
||||
{{"d", "debug-output"}, QCoreApplication::translate("main", "Debug to file")}});
|
||||
{{"d", "debug-output"}, QCoreApplication::translate("main", "Debug to file")},
|
||||
{"simulate-crash",
|
||||
QCoreApplication::translate("main", "Show the mock crash-report dialog (nothing is sent)")}});
|
||||
|
||||
parser.process(app);
|
||||
|
||||
|
|
@ -413,6 +448,45 @@ int main(int argc, char *argv[])
|
|||
ui.show();
|
||||
qCInfo(MainLog) << "ui.show() finished";
|
||||
|
||||
// MOCK crash-report flow (UI + settings only): no sentry-native upload, no network.
|
||||
// Triggered by --simulate-crash or by leftover Windows crash dumps. Handled dumps are deleted
|
||||
// after Send / Don't send (or a silent NeverSend skip) so one crash never prompts twice; on
|
||||
// non-Windows nothing writes cockatrice.crash.*.dmp yet, so --simulate-crash is the trigger there.
|
||||
{
|
||||
QString crashDumpDir;
|
||||
#ifdef Q_OS_WIN
|
||||
// Mirrors the writer's location: %LOCALAPPDATA%/CrashDumps/Cockatrice, with the same
|
||||
// temp-path fallback the writer uses when the known-folder lookup fails.
|
||||
crashDumpDir = QString::fromLocal8Bit(qgetenv("LOCALAPPDATA")) + "/CrashDumps/Cockatrice";
|
||||
if (qgetenv("LOCALAPPDATA").isEmpty()) {
|
||||
crashDumpDir = QDir::tempPath();
|
||||
}
|
||||
#else
|
||||
crashDumpDir = QDir::tempPath();
|
||||
#endif
|
||||
const QStringList crashDumps =
|
||||
QDir(crashDumpDir).entryList({QStringLiteral("cockatrice.crash.*.dmp")}, QDir::Files);
|
||||
if (parser.isSet("simulate-crash") || !crashDumps.isEmpty()) {
|
||||
const PersonalSettings::CrashReportingMode crashMode =
|
||||
SettingsCache::instance().personal().getCrashReportingMode();
|
||||
if (crashMode == PersonalSettings::CrashReportingMode::AlwaysSend) {
|
||||
crashReporter.giveConsent();
|
||||
DlgCrashReport::writeMockReport(DlgCrashReport::buildRedactedPreview());
|
||||
} else if (crashMode == PersonalSettings::CrashReportingMode::Ask) {
|
||||
DlgCrashReport crashDialog(&ui);
|
||||
if (crashDialog.exec() == QDialog::Accepted) {
|
||||
crashReporter.giveConsent();
|
||||
} else {
|
||||
crashReporter.revokeConsent();
|
||||
}
|
||||
}
|
||||
// NeverSend: skip silently (no dialog, nothing written or sent).
|
||||
for (const QString &dump : crashDumps) {
|
||||
QFile::remove(crashDumpDir + QStringLiteral("/") + dump);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// force shortcuts to be shown/hidden in right-click menus, regardless of system defaults
|
||||
qApp->setAttribute(Qt::AA_DontShowShortcutsInContextMenus,
|
||||
!SettingsCache::instance().userInterface().getShowShortcuts());
|
||||
|
|
@ -437,6 +511,8 @@ int main(int argc, char *argv[])
|
|||
|
||||
int ret = app.exec();
|
||||
|
||||
crashReporter.shutdown();
|
||||
|
||||
qCInfo(MainLog) << "Event loop finished, terminating...";
|
||||
delete rng;
|
||||
PingPixmapGenerator::clear();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue