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
|
|
@ -3,11 +3,13 @@ FROM debian:12
|
|||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
git \
|
||||
libcurl4-openssl-dev \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
libmariadb-dev-compat \
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ RUN apt-get update && \
|
|||
file \
|
||||
g++ \
|
||||
git \
|
||||
libcurl4-openssl-dev \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
libmariadb-dev-compat \
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
FROM fedora:43
|
||||
|
||||
RUN dnf install -y \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
cmake \
|
||||
gcc-c++ \
|
||||
git \
|
||||
libcurl-devel \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
openssl-devel \
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
FROM fedora:44
|
||||
|
||||
RUN dnf install -y \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
cmake \
|
||||
gcc-c++ \
|
||||
git \
|
||||
libcurl-devel \
|
||||
mariadb-devel \
|
||||
ninja-build \
|
||||
openssl-devel \
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ FROM ubuntu:24.04
|
|||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
cmake \
|
||||
file \
|
||||
g++ \
|
||||
git \
|
||||
libcurl4-openssl-dev \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
libmariadb-dev-compat \
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ RUN apt-get update && \
|
|||
g++ \
|
||||
gcovr \
|
||||
git \
|
||||
libcurl4-openssl-dev \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
libmariadb-dev-compat \
|
||||
|
|
|
|||
|
|
@ -200,6 +200,12 @@ if [[ $USE_VCPKG ]]; then
|
|||
flags+=("-DUSE_VCPKG=1")
|
||||
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
|
||||
fi
|
||||
if [[ ${WITH_CRASH_REPORTING:-} == "ON" ]]; then
|
||||
# Release CI opts into the sentry-native SDK (default is OFF so from-source, distro and
|
||||
# offline package builds never fetch it). Debug builds stay SDK-free so crashes from
|
||||
# developer builds never reach Sentry.
|
||||
flags+=("-DWITH_CRASH_REPORTING=ON")
|
||||
fi
|
||||
|
||||
# Add cmake --build flags
|
||||
buildflags=(--config "$BUILDTYPE")
|
||||
|
|
|
|||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
|
@ -60,8 +60,10 @@ jobs:
|
|||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
cmake \
|
||||
g++ \
|
||||
libcurl4-openssl-dev \
|
||||
libgl-dev \
|
||||
liblzma-dev \
|
||||
libmariadb-dev-compat \
|
||||
|
|
|
|||
3
.github/workflows/desktop-build.yml
vendored
3
.github/workflows/desktop-build.yml
vendored
|
|
@ -239,6 +239,7 @@ jobs:
|
|||
SUFFIX: '-${{ matrix.distro }}${{ matrix.version }}'
|
||||
package: '${{ matrix.package }}'
|
||||
server_only: '${{ matrix.server_only }}'
|
||||
WITH_CRASH_REPORTING: 'ON'
|
||||
run: |
|
||||
source .ci/docker.sh
|
||||
args=()
|
||||
|
|
@ -558,6 +559,8 @@ jobs:
|
|||
VCPKG_DISABLE_METRICS: 1
|
||||
VCPKG_FEATURE_FLAGS: dependencygraph
|
||||
VCPKG_TARGET_MACOS_VERSION: ${{ matrix.vcpkg_deployment_target }}
|
||||
# Release legs compile the sentry-native SDK; Debug legs stay SDK-free.
|
||||
WITH_CRASH_REPORTING: ${{ matrix.type == 'Release' && 'ON' || '' }}
|
||||
run: .ci/compile.sh --server --test --vcpkg
|
||||
|
||||
# CPack reports a failed makensis run as "Problem running NSIS command"
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ endif()
|
|||
# when ENABLE_SANITIZERS or ENABLE_COVERAGE is on.
|
||||
include(Sanitizers)
|
||||
include(Coverage)
|
||||
include(CrashReporting)
|
||||
|
||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
|
|
|
|||
124
cmake/CrashReporting.cmake
Normal file
124
cmake/CrashReporting.cmake
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# Crash reporting via sentry-native (Sentry protocol, MIT).
|
||||
# Included once from the root CMakeLists.txt, right after the sanitizer/coverage helpers.
|
||||
#
|
||||
# Inputs:
|
||||
# WITH_CRASH_REPORTING -- build the client with the sentry-native SDK (default OFF, our release
|
||||
# CI passes ON; packagers and from-source builds stay SDK-free until this
|
||||
# ships something real, so offline package builds never fetch the SDK)
|
||||
# COCKATRICE_SENTRY_DSN -- Sentry DSN baked into the build (default empty = SDK init skipped at runtime)
|
||||
# COCKATRICE_SENTRY_TAG -- sentry-native release tag to vendor via FetchContent
|
||||
# ENABLE_SANITIZERS -- forces the sentry backend to "none" (crashpad vs ASan conflict)
|
||||
# Defines: cockatrice_target_enable_crash_reporting(<target>)
|
||||
|
||||
option(WITH_CRASH_REPORTING "Build with crash reporting via sentry-native" OFF)
|
||||
set(COCKATRICE_SENTRY_DSN
|
||||
""
|
||||
CACHE STRING "Sentry DSN baked into the client; empty disables crash-report upload at runtime"
|
||||
)
|
||||
# Pinned stable release; tag verified at https://github.com/getsentry/sentry-native/tags.
|
||||
set(COCKATRICE_SENTRY_TAG
|
||||
"0.17.3"
|
||||
CACHE STRING "sentry-native release tag to vendor via FetchContent"
|
||||
)
|
||||
|
||||
# Wire the SDK (or the no-op fallback) into a client target. Always callable: with the
|
||||
# option OFF the compile definitions below make crash_reporter.cpp compile to no-ops.
|
||||
function(cockatrice_target_enable_crash_reporting _target)
|
||||
if(WITH_CRASH_REPORTING AND TARGET sentry::sentry)
|
||||
target_link_libraries(${_target} PRIVATE sentry::sentry)
|
||||
target_compile_definitions(${_target} PRIVATE COCKATRICE_WITH_CRASH_REPORTING=1)
|
||||
else()
|
||||
target_compile_definitions(${_target} PRIVATE COCKATRICE_WITH_CRASH_REPORTING=0)
|
||||
endif()
|
||||
# Always baked in (possibly empty): main() prefers the COCKATRICE_SENTRY_DSN env var at runtime.
|
||||
target_compile_definitions(${_target} PRIVATE "COCKATRICE_SENTRY_DSN=\"${COCKATRICE_SENTRY_DSN}\"")
|
||||
endfunction()
|
||||
|
||||
if(WITH_CRASH_REPORTING AND WITH_CLIENT)
|
||||
include(FetchContent)
|
||||
|
||||
if(ENABLE_SANITIZERS)
|
||||
# The crash handler cannot run under ASan/UBSan, so there is no backend to capture with.
|
||||
set(_sentry_backend "none")
|
||||
elseif(WIN32 OR APPLE)
|
||||
# Out-of-process handler; shipping the handler binary is a later increment, which unifies
|
||||
# every OS on breakpad (in-process, no extra binary to ship).
|
||||
set(_sentry_backend "crashpad")
|
||||
else()
|
||||
# In-process, no extra binary to ship.
|
||||
set(_sentry_backend "breakpad")
|
||||
endif()
|
||||
# FORCE so toggling ENABLE_SANITIZERS on a reused build dir cannot leave a stale backend behind.
|
||||
set(SENTRY_BACKEND
|
||||
"${_sentry_backend}"
|
||||
CACHE STRING "Crash handler backend for sentry-native" FORCE
|
||||
)
|
||||
unset(_sentry_backend)
|
||||
|
||||
# No Qt integration: it runs find_package(Qt6) inside the vendored build, which
|
||||
# cannot see our thin/aqt Qt layouts (macOS CI) under foreign toolchains, and its
|
||||
# breadcrumbs could carry unredacted log lines into native crash events that bypass
|
||||
# before_send. Our attached redacted log preview already carries the log context.
|
||||
set(SENTRY_INTEGRATION_QT
|
||||
OFF
|
||||
CACHE BOOL "Qt log-message breadcrumbs for sentry-native (off: discovery + secret surface)" FORCE
|
||||
)
|
||||
# The transport stays at the SDK default (curl on Apple/Linux, winhttp on Windows).
|
||||
set(SENTRY_BUILD_TESTS
|
||||
OFF
|
||||
CACHE BOOL "Build sentry-native tests" FORCE
|
||||
)
|
||||
set(SENTRY_BUILD_EXAMPLES
|
||||
OFF
|
||||
CACHE BOOL "Build sentry-native examples" FORCE
|
||||
)
|
||||
|
||||
fetchcontent_declare(
|
||||
sentry
|
||||
GIT_REPOSITORY https://github.com/getsentry/sentry-native.git
|
||||
GIT_TAG ${COCKATRICE_SENTRY_TAG}
|
||||
)
|
||||
fetchcontent_makeavailable(sentry)
|
||||
|
||||
# Vendored sentry-native (and its breakpad/crashpad trees) warn under our -Wall -Wextra
|
||||
# (e.g. breakpad's -Wmissing-field-initializers), and WARNING_AS_ERROR would fail the build
|
||||
# on code we cannot fix. Those findings are not actionable for us, so opt the whole vendored
|
||||
# tree out of warnings-as-errors while keeping the warnings themselves visible; our own
|
||||
# targets keep -Werror. Interface libraries compile nothing, so they are skipped outright:
|
||||
# an INTERFACE -Wno-error would propagate to our consumers and weaken their build.
|
||||
function(_cockatrice_sentry_opt_out_of_werror _dir)
|
||||
get_property(
|
||||
_sentry_targets
|
||||
DIRECTORY "${_dir}"
|
||||
PROPERTY BUILDSYSTEM_TARGETS
|
||||
)
|
||||
foreach(_sentry_target IN LISTS _sentry_targets)
|
||||
get_target_property(_sentry_type ${_sentry_target} TYPE)
|
||||
if(_sentry_type STREQUAL "INTERFACE_LIBRARY")
|
||||
continue()
|
||||
elseif(_sentry_type MATCHES "LIBRARY|EXECUTABLE")
|
||||
get_target_property(_sentry_imported ${_sentry_target} IMPORTED)
|
||||
if(NOT _sentry_imported)
|
||||
target_compile_options(${_sentry_target} PRIVATE -Wno-error)
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
get_property(
|
||||
_sentry_subdirs
|
||||
DIRECTORY "${_dir}"
|
||||
PROPERTY SUBDIRECTORIES
|
||||
)
|
||||
foreach(_sentry_subdir IN LISTS _sentry_subdirs)
|
||||
_cockatrice_sentry_opt_out_of_werror("${_sentry_subdir}")
|
||||
endforeach()
|
||||
endfunction()
|
||||
if(NOT MSVC)
|
||||
_cockatrice_sentry_opt_out_of_werror("${sentry_SOURCE_DIR}")
|
||||
endif()
|
||||
|
||||
message(STATUS "Crash reporting: sentry-native ${COCKATRICE_SENTRY_TAG} (backend: ${SENTRY_BACKEND})")
|
||||
else()
|
||||
message(STATUS "Crash reporting: SDK not vendored "
|
||||
"(WITH_CRASH_REPORTING=${WITH_CRASH_REPORTING}, WITH_CLIENT=${WITH_CLIENT})"
|
||||
)
|
||||
endif()
|
||||
|
|
@ -15,6 +15,7 @@ set(cockatrice_SOURCES
|
|||
src/client/network/update/client/client_update_checker.cpp
|
||||
src/client/network/update/client/release_channel.cpp
|
||||
src/client/network/update/card_spoiler/spoiler_background_updater.cpp
|
||||
src/client/crash/crash_reporter.cpp
|
||||
src/client/latency_graph_widget.cpp
|
||||
src/client/latency_status_widget.cpp
|
||||
src/client/lag_monitor.cpp
|
||||
|
|
@ -30,6 +31,7 @@ set(cockatrice_SOURCES
|
|||
src/interface/widgets/dialogs/dlg_connect.cpp
|
||||
src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp
|
||||
src/interface/widgets/dialogs/dlg_create_game.cpp
|
||||
src/interface/widgets/dialogs/dlg_crash_report.cpp
|
||||
src/interface/widgets/dialogs/dlg_default_tags_editor.cpp
|
||||
src/interface/widgets/dialogs/dlg_edit_avatar.cpp
|
||||
src/interface/widgets/dialogs/dlg_edit_password.cpp
|
||||
|
|
@ -578,6 +580,8 @@ target_link_libraries(
|
|||
${COCKATRICE_QT_MODULES}
|
||||
)
|
||||
|
||||
cockatrice_target_enable_crash_reporting(cockatrice)
|
||||
|
||||
if(UNIX)
|
||||
if(APPLE)
|
||||
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
|
||||
|
|
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -34,4 +34,4 @@ int DebugSettings::getLocalGamePlayerCount() const
|
|||
QString DebugSettings::getDeckPathForPlayer(const QString &playerName) const
|
||||
{
|
||||
return getValue(playerName, "localgame", "deck").toString();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ class DebugSettings : public SettingsManager
|
|||
Q_OBJECT
|
||||
friend class SettingsCache;
|
||||
|
||||
public:
|
||||
explicit DebugSettings(const QString &settingPath, QObject *parent = nullptr);
|
||||
|
||||
private:
|
||||
DebugSettings(const DebugSettings & /*other*/);
|
||||
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -51,3 +51,24 @@ void PersonalSettings::setSeenTips(const QList<int> &_seenTips)
|
|||
}
|
||||
setValue(QVariant::fromValue(storedTipList), "seenTips", "tipOfDay");
|
||||
}
|
||||
|
||||
PersonalSettings::CrashReportingMode PersonalSettings::getCrashReportingMode() const
|
||||
{
|
||||
const int raw = getValue("crashReportingMode", QString(), QString(), 0).toInt();
|
||||
switch (raw) {
|
||||
case 1:
|
||||
return CrashReportingMode::AlwaysSend;
|
||||
case 2:
|
||||
return CrashReportingMode::NeverSend;
|
||||
case 0:
|
||||
default:
|
||||
// Clamp corrupt ini values back to Ask; the QSettings key string is unchanged (no migration).
|
||||
return CrashReportingMode::Ask;
|
||||
}
|
||||
}
|
||||
|
||||
void PersonalSettings::setCrashReportingMode(CrashReportingMode _crashReportingMode)
|
||||
{
|
||||
setValue(static_cast<int>(_crashReportingMode), "crashReportingMode");
|
||||
emit crashReportingModeChanged(_crashReportingMode);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,19 @@ public:
|
|||
void setShowTipsOnStartup(bool _showTipsOnStartup);
|
||||
void setSeenTips(const QList<int> &_seenTips);
|
||||
|
||||
enum class CrashReportingMode
|
||||
{
|
||||
Ask = 0,
|
||||
AlwaysSend = 1,
|
||||
NeverSend = 2
|
||||
};
|
||||
|
||||
[[nodiscard]] CrashReportingMode getCrashReportingMode() const;
|
||||
void setCrashReportingMode(CrashReportingMode _crashReportingMode);
|
||||
|
||||
signals:
|
||||
void langChanged();
|
||||
void crashReportingModeChanged(CrashReportingMode mode);
|
||||
|
||||
public:
|
||||
explicit PersonalSettings(const QString &settingPath, QObject *parent = nullptr);
|
||||
|
|
|
|||
|
|
@ -422,6 +422,21 @@ if(WITH_CLIENT)
|
|||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
${CMAKE_SOURCE_DIR}/libcockatrice_utility
|
||||
)
|
||||
# CrashReporter is client code; DEFINITIONS keeps the test hermetic by compiling the
|
||||
# no-op path (no sentry-native headers needed) regardless of WITH_CRASH_REPORTING.
|
||||
cockatrice_add_test(
|
||||
NAME
|
||||
crash_reporter_test
|
||||
SOURCES
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src/client/crash/crash_reporter.cpp
|
||||
crash_reporter_test.cpp
|
||||
LIBS
|
||||
libcockatrice_settings
|
||||
INCLUDE_DIRS
|
||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||
DEFINITIONS
|
||||
COCKATRICE_WITH_CRASH_REPORTING=0
|
||||
)
|
||||
endif()
|
||||
|
||||
add_subdirectory(card_zone_algorithms)
|
||||
|
|
|
|||
87
tests/crash_reporter_test.cpp
Normal file
87
tests/crash_reporter_test.cpp
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#include "client/crash/crash_reporter.h"
|
||||
|
||||
#include <QString>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libcockatrice/settings/personal_settings.h>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
TEST(CrashReporterTest, SanitizeLogLineStripsAnsiColorCodes)
|
||||
{
|
||||
ASSERT_EQ(CrashReporter::sanitizeLogLine(QStringLiteral("\033[32mhello\033[0m")), QStringLiteral("hello"));
|
||||
ASSERT_EQ(CrashReporter::sanitizeLogLine(QStringLiteral("\033[1;31mfatal\033[0m")), QStringLiteral("fatal"));
|
||||
ASSERT_EQ(CrashReporter::sanitizeLogLine(QStringLiteral("plain line")), QStringLiteral("plain line"));
|
||||
}
|
||||
|
||||
TEST(CrashReporterTest, RedactCockatriceUrlsKeepsSchemeAndHostOnly)
|
||||
{
|
||||
ASSERT_EQ(
|
||||
CrashReporter::redactCockatriceUrls(QStringLiteral("open cockatrice://opendeck?token=secret&user=me now")),
|
||||
QStringLiteral("open cockatrice://opendeck now"));
|
||||
}
|
||||
|
||||
TEST(CrashReporterTest, RedactCockatriceUrlsIsIdempotent)
|
||||
{
|
||||
// A secret-free URL must not match again after redaction (regression: this hung forever).
|
||||
ASSERT_EQ(CrashReporter::redactCockatriceUrls(QStringLiteral("open cockatrice://opendeck now")),
|
||||
QStringLiteral("open cockatrice://opendeck now"));
|
||||
}
|
||||
|
||||
TEST(CrashReporterTest, RedactCockatriceUrlsLeavesOtherTextAlone)
|
||||
{
|
||||
ASSERT_EQ(CrashReporter::redactCockatriceUrls(QStringLiteral("https://cockatrice.github.io/?token=secret")),
|
||||
QStringLiteral("https://cockatrice.github.io/?token=secret"));
|
||||
ASSERT_EQ(CrashReporter::sanitizeLogLine(QStringLiteral("")), QStringLiteral(""));
|
||||
}
|
||||
|
||||
TEST(CrashReporterTest, InitSkippedWithoutDsn)
|
||||
{
|
||||
CrashReporter reporter;
|
||||
reporter.init(QString(), QStringLiteral("3.1.2"), QStringLiteral("/tmp/cockatrice-crash-test"),
|
||||
PersonalSettings::CrashReportingMode::Ask);
|
||||
ASSERT_FALSE(reporter.isReady());
|
||||
reporter.shutdown();
|
||||
ASSERT_FALSE(reporter.isReady());
|
||||
}
|
||||
|
||||
TEST(CrashReporterTest, InitSkippedWhenNeverSend)
|
||||
{
|
||||
CrashReporter reporter;
|
||||
reporter.init(QStringLiteral("https://key@sentry.io/1"), QStringLiteral("3.1.2"),
|
||||
QStringLiteral("/tmp/cockatrice-crash-test"), PersonalSettings::CrashReportingMode::NeverSend);
|
||||
ASSERT_FALSE(reporter.isReady());
|
||||
reporter.shutdown();
|
||||
}
|
||||
|
||||
// The init() tests above compile to no-ops when the SDK is off, so they pass
|
||||
// regardless of the gating; the gating decision itself is covered here, in both
|
||||
// build modes, through the SDK-independent helper init() consults.
|
||||
TEST(CrashReporterTest, ReportingEnabledGating)
|
||||
{
|
||||
using Mode = PersonalSettings::CrashReportingMode;
|
||||
const QString dsn = QStringLiteral("https://key@sentry.io/1");
|
||||
ASSERT_FALSE(CrashReporter::isReportingEnabled(QString(), Mode::Ask));
|
||||
ASSERT_FALSE(CrashReporter::isReportingEnabled(QString(), Mode::AlwaysSend));
|
||||
ASSERT_FALSE(CrashReporter::isReportingEnabled(QString(), Mode::NeverSend));
|
||||
ASSERT_TRUE(CrashReporter::isReportingEnabled(dsn, Mode::Ask));
|
||||
ASSERT_TRUE(CrashReporter::isReportingEnabled(dsn, Mode::AlwaysSend));
|
||||
ASSERT_FALSE(CrashReporter::isReportingEnabled(dsn, Mode::NeverSend));
|
||||
}
|
||||
|
||||
TEST(CrashReporterTest, ShutdownWithoutInitIsSafe)
|
||||
{
|
||||
CrashReporter reporter;
|
||||
ASSERT_FALSE(reporter.isReady());
|
||||
reporter.shutdown();
|
||||
ASSERT_FALSE(reporter.isReady());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
#include <QDir>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QSettings>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QTemporaryDir>
|
||||
|
|
@ -367,6 +368,32 @@ TEST_F(SettingsDefaultsTest, Personal_ShowTipsOnStartup_Default)
|
|||
ASSERT_EQ(s.getShowTipsOnStartup(), true);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Personal_CrashReportingMode_Default)
|
||||
{
|
||||
PersonalSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getCrashReportingMode(), PersonalSettings::CrashReportingMode::Ask);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Personal_CrashReportingMode_SetAndGet)
|
||||
{
|
||||
PersonalSettings s(settingsPath, nullptr);
|
||||
s.setCrashReportingMode(PersonalSettings::CrashReportingMode::AlwaysSend);
|
||||
ASSERT_EQ(s.getCrashReportingMode(), PersonalSettings::CrashReportingMode::AlwaysSend);
|
||||
s.setCrashReportingMode(PersonalSettings::CrashReportingMode::NeverSend);
|
||||
ASSERT_EQ(s.getCrashReportingMode(), PersonalSettings::CrashReportingMode::NeverSend);
|
||||
s.setCrashReportingMode(PersonalSettings::CrashReportingMode::Ask);
|
||||
ASSERT_EQ(s.getCrashReportingMode(), PersonalSettings::CrashReportingMode::Ask);
|
||||
}
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Personal_CrashReportingMode_ClampsCorruptValue)
|
||||
{
|
||||
QSettings raw(settingsPath + "personal.ini", QSettings::IniFormat);
|
||||
raw.setValue("personal/crashReportingMode", 99);
|
||||
raw.sync();
|
||||
PersonalSettings s(settingsPath, nullptr);
|
||||
ASSERT_EQ(s.getCrashReportingMode(), PersonalSettings::CrashReportingMode::Ask);
|
||||
}
|
||||
|
||||
// --- DownloadSettings ---
|
||||
|
||||
TEST_F(SettingsDefaultsTest, Download_PicDownload_Default)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue