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 && \
|
RUN apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
ca-certificates \
|
||||||
ccache \
|
ccache \
|
||||||
cmake \
|
cmake \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
git \
|
git \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ RUN apt-get update && \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
git \
|
git \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
FROM fedora:43
|
FROM fedora:43
|
||||||
|
|
||||||
RUN dnf install -y \
|
RUN dnf install -y \
|
||||||
|
ca-certificates \
|
||||||
ccache \
|
ccache \
|
||||||
cmake \
|
cmake \
|
||||||
gcc-c++ \
|
gcc-c++ \
|
||||||
git \
|
git \
|
||||||
|
libcurl-devel \
|
||||||
mariadb-devel \
|
mariadb-devel \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
openssl-devel \
|
openssl-devel \
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
FROM fedora:44
|
FROM fedora:44
|
||||||
|
|
||||||
RUN dnf install -y \
|
RUN dnf install -y \
|
||||||
|
ca-certificates \
|
||||||
ccache \
|
ccache \
|
||||||
cmake \
|
cmake \
|
||||||
gcc-c++ \
|
gcc-c++ \
|
||||||
git \
|
git \
|
||||||
|
libcurl-devel \
|
||||||
mariadb-devel \
|
mariadb-devel \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
openssl-devel \
|
openssl-devel \
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ FROM ubuntu:24.04
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
ca-certificates \
|
||||||
ccache \
|
ccache \
|
||||||
cmake \
|
cmake \
|
||||||
file \
|
file \
|
||||||
g++ \
|
g++ \
|
||||||
git \
|
git \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ RUN apt-get update && \
|
||||||
g++ \
|
g++ \
|
||||||
gcovr \
|
gcovr \
|
||||||
git \
|
git \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,12 @@ if [[ $USE_VCPKG ]]; then
|
||||||
flags+=("-DUSE_VCPKG=1")
|
flags+=("-DUSE_VCPKG=1")
|
||||||
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
|
flags+=("-DVCPKG_INSTALL_OPTIONS=--x-abi-tools-use-exact-versions")
|
||||||
fi
|
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
|
# Add cmake --build flags
|
||||||
buildflags=(--config "$BUILDTYPE")
|
buildflags=(--config "$BUILDTYPE")
|
||||||
|
|
|
||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
|
@ -60,8 +60,10 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y --no-install-recommends \
|
sudo apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
cmake \
|
cmake \
|
||||||
g++ \
|
g++ \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
libgl-dev \
|
libgl-dev \
|
||||||
liblzma-dev \
|
liblzma-dev \
|
||||||
libmariadb-dev-compat \
|
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 }}'
|
SUFFIX: '-${{ matrix.distro }}${{ matrix.version }}'
|
||||||
package: '${{ matrix.package }}'
|
package: '${{ matrix.package }}'
|
||||||
server_only: '${{ matrix.server_only }}'
|
server_only: '${{ matrix.server_only }}'
|
||||||
|
WITH_CRASH_REPORTING: 'ON'
|
||||||
run: |
|
run: |
|
||||||
source .ci/docker.sh
|
source .ci/docker.sh
|
||||||
args=()
|
args=()
|
||||||
|
|
@ -558,6 +559,8 @@ jobs:
|
||||||
VCPKG_DISABLE_METRICS: 1
|
VCPKG_DISABLE_METRICS: 1
|
||||||
VCPKG_FEATURE_FLAGS: dependencygraph
|
VCPKG_FEATURE_FLAGS: dependencygraph
|
||||||
VCPKG_TARGET_MACOS_VERSION: ${{ matrix.vcpkg_deployment_target }}
|
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
|
run: .ci/compile.sh --server --test --vcpkg
|
||||||
|
|
||||||
# CPack reports a failed makensis run as "Problem running NSIS command"
|
# CPack reports a failed makensis run as "Problem running NSIS command"
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,7 @@ endif()
|
||||||
# when ENABLE_SANITIZERS or ENABLE_COVERAGE is on.
|
# when ENABLE_SANITIZERS or ENABLE_COVERAGE is on.
|
||||||
include(Sanitizers)
|
include(Sanitizers)
|
||||||
include(Coverage)
|
include(Coverage)
|
||||||
|
include(CrashReporting)
|
||||||
|
|
||||||
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
# GNU systems need to define the Mersenne exponent for the RNG to compile w/o warning
|
||||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
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/client_update_checker.cpp
|
||||||
src/client/network/update/client/release_channel.cpp
|
src/client/network/update/client/release_channel.cpp
|
||||||
src/client/network/update/card_spoiler/spoiler_background_updater.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_graph_widget.cpp
|
||||||
src/client/latency_status_widget.cpp
|
src/client/latency_status_widget.cpp
|
||||||
src/client/lag_monitor.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_connect.cpp
|
||||||
src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.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_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_default_tags_editor.cpp
|
||||||
src/interface/widgets/dialogs/dlg_edit_avatar.cpp
|
src/interface/widgets/dialogs/dlg_edit_avatar.cpp
|
||||||
src/interface/widgets/dialogs/dlg_edit_password.cpp
|
src/interface/widgets/dialogs/dlg_edit_password.cpp
|
||||||
|
|
@ -578,6 +580,8 @@ target_link_libraries(
|
||||||
${COCKATRICE_QT_MODULES}
|
${COCKATRICE_QT_MODULES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cockatrice_target_enable_crash_reporting(cockatrice)
|
||||||
|
|
||||||
if(UNIX)
|
if(UNIX)
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
|
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 <QApplication>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QFlags>
|
#include <QFlags>
|
||||||
#include <QIODevice>
|
#include <QIODevice>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
|
|
@ -18,8 +20,11 @@
|
||||||
|
|
||||||
#define LOGGER_MAX_ENTRIES 128
|
#define LOGGER_MAX_ENTRIES 128
|
||||||
#define LOGGER_FILENAME "qdebug.txt"
|
#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(getClientVersion());
|
||||||
logBuffer.append(getSystemArchitecture());
|
logBuffer.append(getSystemArchitecture());
|
||||||
|
|
@ -35,6 +40,7 @@ Logger::Logger() : logToFileEnabled(false)
|
||||||
Logger::~Logger()
|
Logger::~Logger()
|
||||||
{
|
{
|
||||||
closeLogfileSession();
|
closeLogfileSession();
|
||||||
|
closePersistentLogSession();
|
||||||
logBuffer.clear();
|
logBuffer.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,6 +88,137 @@ void Logger::closeLogfileSession()
|
||||||
fileHandle.close();
|
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)
|
void Logger::log(QtMsgType /* type */, const QMessageLogContext & /* ctx */, const QString &message)
|
||||||
{
|
{
|
||||||
QMetaObject::invokeMethod(this, "internalLog", Qt::QueuedConnection, Q_ARG(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) {
|
if (logToFileEnabled) {
|
||||||
fileStream << message << Qt::endl; // Print to fileStream
|
fileStream << message << Qt::endl; // Print to fileStream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (persistentLogEnabled) {
|
||||||
|
persistentFileStream << message << Qt::endl;
|
||||||
|
persistentFileStream.flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Logger::getSystemArchitecture()
|
QString Logger::getSystemArchitecture()
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,9 @@ public:
|
||||||
{
|
{
|
||||||
return logBuffer;
|
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:
|
private:
|
||||||
Logger();
|
Logger();
|
||||||
|
|
@ -61,11 +64,18 @@ private:
|
||||||
QTextStream fileStream;
|
QTextStream fileStream;
|
||||||
QFile fileHandle;
|
QFile fileHandle;
|
||||||
QList<QString> logBuffer;
|
QList<QString> logBuffer;
|
||||||
QMutex mutex;
|
mutable QMutex mutex;
|
||||||
|
QString persistentLogPath;
|
||||||
|
bool persistentLogEnabled;
|
||||||
|
QTextStream persistentFileStream;
|
||||||
|
QFile persistentFileHandle;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void openLogfileSession();
|
void openLogfileSession();
|
||||||
void closeLogfileSession();
|
void closeLogfileSession();
|
||||||
|
void openPersistentLog(const QString &path);
|
||||||
|
void prunePersistentLog(const QString &path);
|
||||||
|
void closePersistentLogSession();
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
void internalLog(const QString &message);
|
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 = new QGroupBox;
|
||||||
versionGroupBox->setLayout(versionGrid);
|
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
|
// card database settings
|
||||||
startupCardUpdateCheckBehaviorSelector.addItem(""); // these will be set in retranslateUI
|
startupCardUpdateCheckBehaviorSelector.addItem(""); // these will be set in retranslateUI
|
||||||
startupCardUpdateCheckBehaviorSelector.addItem("");
|
startupCardUpdateCheckBehaviorSelector.addItem("");
|
||||||
|
|
@ -336,6 +358,7 @@ GeneralSettingsPage::GeneralSettingsPage()
|
||||||
auto *mainLayout = new QVBoxLayout;
|
auto *mainLayout = new QVBoxLayout;
|
||||||
mainLayout->addWidget(languageGroupBox);
|
mainLayout->addWidget(languageGroupBox);
|
||||||
mainLayout->addWidget(versionGroupBox);
|
mainLayout->addWidget(versionGroupBox);
|
||||||
|
mainLayout->addWidget(crashReportingGroupBox);
|
||||||
mainLayout->addWidget(cardDatabaseGroupBox);
|
mainLayout->addWidget(cardDatabaseGroupBox);
|
||||||
mainLayout->addWidget(startupGroupBox);
|
mainLayout->addWidget(startupGroupBox);
|
||||||
mainLayout->addWidget(pathsGroupBox);
|
mainLayout->addWidget(pathsGroupBox);
|
||||||
|
|
@ -569,13 +592,19 @@ void GeneralSettingsPage::retranslateUi()
|
||||||
tr("English and selected card language"));
|
tr("English and selected card language"));
|
||||||
advertiseTranslationPageLabel.setText(
|
advertiseTranslationPageLabel.setText(
|
||||||
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
|
QString("<a href='%1'>%2</a>").arg(WIKI_TRANSLATION_FAQ).arg(tr("How to help with translations")));
|
||||||
|
|
||||||
versionGroupBox->setTitle(tr("Version settings"));
|
versionGroupBox->setTitle(tr("Version settings"));
|
||||||
updateReleaseChannelLabel.setText(tr("Update channel"));
|
updateReleaseChannelLabel.setText(tr("Update channel"));
|
||||||
startupUpdateCheckCheckBox.setText(tr("Check for client updates on startup"));
|
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"));
|
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"));
|
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
|
// We can't change the strings after they're put into the QComboBox, so this is our workaround
|
||||||
int oldIndex = updateReleaseChannelBox.currentIndex();
|
int oldIndex = updateReleaseChannelBox.currentIndex();
|
||||||
updateReleaseChannelBox.clear();
|
updateReleaseChannelBox.clear();
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ private slots:
|
||||||
private:
|
private:
|
||||||
QGroupBox *languageGroupBox;
|
QGroupBox *languageGroupBox;
|
||||||
QGroupBox *versionGroupBox;
|
QGroupBox *versionGroupBox;
|
||||||
|
QGroupBox *crashReportingGroupBox;
|
||||||
QGroupBox *cardDatabaseGroupBox;
|
QGroupBox *cardDatabaseGroupBox;
|
||||||
QGroupBox *startupGroupBox;
|
QGroupBox *startupGroupBox;
|
||||||
QGroupBox *pathsGroupBox;
|
QGroupBox *pathsGroupBox;
|
||||||
|
|
@ -70,6 +71,9 @@ private:
|
||||||
QCheckBox updateNotificationCheckBox;
|
QCheckBox updateNotificationCheckBox;
|
||||||
QCheckBox newVersionOracleCheckBox;
|
QCheckBox newVersionOracleCheckBox;
|
||||||
|
|
||||||
|
QLabel crashReportingLabel;
|
||||||
|
QComboBox crashReportingBox;
|
||||||
|
|
||||||
QLabel startupCardUpdateCheckBehaviorLabel;
|
QLabel startupCardUpdateCheckBehaviorLabel;
|
||||||
QComboBox startupCardUpdateCheckBehaviorSelector;
|
QComboBox startupCardUpdateCheckBehaviorSelector;
|
||||||
QLabel cardUpdateCheckIntervalLabel;
|
QLabel cardUpdateCheckIntervalLabel;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
|
||||||
|
#include "client/crash/crash_reporter.h"
|
||||||
#include "client/network/update/card_spoiler/spoiler_background_updater.h"
|
#include "client/network/update/card_spoiler/spoiler_background_updater.h"
|
||||||
#include "client/settings/cache_settings.h"
|
#include "client/settings/cache_settings.h"
|
||||||
#include "client/sound_engine.h"
|
#include "client/sound_engine.h"
|
||||||
|
|
@ -30,6 +31,7 @@
|
||||||
#include "interface/logger.h"
|
#include "interface/logger.h"
|
||||||
#include "interface/pixel_map_generator.h"
|
#include "interface/pixel_map_generator.h"
|
||||||
#include "interface/theme_manager.h"
|
#include "interface/theme_manager.h"
|
||||||
|
#include "interface/widgets/dialogs/dlg_crash_report.h"
|
||||||
#include "interface/window_main.h"
|
#include "interface/window_main.h"
|
||||||
#include "libcockatrice/card/database/card_database.h"
|
#include "libcockatrice/card/database/card_database.h"
|
||||||
#include "libcockatrice/rng/rng_abstract.h"
|
#include "libcockatrice/rng/rng_abstract.h"
|
||||||
|
|
@ -44,6 +46,9 @@
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
|
|
@ -259,6 +264,34 @@ int main(int argc, char *argv[])
|
||||||
qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true);
|
qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true);
|
||||||
#endif
|
#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
|
// Translations
|
||||||
#ifdef Q_OS_MAC
|
#ifdef Q_OS_MAC
|
||||||
translationPath = qApp->applicationDirPath() + "/../Resources/translations";
|
translationPath = qApp->applicationDirPath() + "/../Resources/translations";
|
||||||
|
|
@ -276,7 +309,9 @@ int main(int argc, char *argv[])
|
||||||
|
|
||||||
parser.addOptions(
|
parser.addOptions(
|
||||||
{{{"c", "connect"}, QCoreApplication::translate("main", "Connect on startup"), "user:pass@host:port"},
|
{{{"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);
|
parser.process(app);
|
||||||
|
|
||||||
|
|
@ -413,6 +448,45 @@ int main(int argc, char *argv[])
|
||||||
ui.show();
|
ui.show();
|
||||||
qCInfo(MainLog) << "ui.show() finished";
|
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
|
// force shortcuts to be shown/hidden in right-click menus, regardless of system defaults
|
||||||
qApp->setAttribute(Qt::AA_DontShowShortcutsInContextMenus,
|
qApp->setAttribute(Qt::AA_DontShowShortcutsInContextMenus,
|
||||||
!SettingsCache::instance().userInterface().getShowShortcuts());
|
!SettingsCache::instance().userInterface().getShowShortcuts());
|
||||||
|
|
@ -437,6 +511,8 @@ int main(int argc, char *argv[])
|
||||||
|
|
||||||
int ret = app.exec();
|
int ret = app.exec();
|
||||||
|
|
||||||
|
crashReporter.shutdown();
|
||||||
|
|
||||||
qCInfo(MainLog) << "Event loop finished, terminating...";
|
qCInfo(MainLog) << "Event loop finished, terminating...";
|
||||||
delete rng;
|
delete rng;
|
||||||
PingPixmapGenerator::clear();
|
PingPixmapGenerator::clear();
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@ class DebugSettings : public SettingsManager
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
friend class SettingsCache;
|
friend class SettingsCache;
|
||||||
|
|
||||||
|
public:
|
||||||
explicit DebugSettings(const QString &settingPath, QObject *parent = nullptr);
|
explicit DebugSettings(const QString &settingPath, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
DebugSettings(const DebugSettings & /*other*/);
|
DebugSettings(const DebugSettings & /*other*/);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
|
||||||
|
|
@ -51,3 +51,24 @@ void PersonalSettings::setSeenTips(const QList<int> &_seenTips)
|
||||||
}
|
}
|
||||||
setValue(QVariant::fromValue(storedTipList), "seenTips", "tipOfDay");
|
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 setShowTipsOnStartup(bool _showTipsOnStartup);
|
||||||
void setSeenTips(const QList<int> &_seenTips);
|
void setSeenTips(const QList<int> &_seenTips);
|
||||||
|
|
||||||
|
enum class CrashReportingMode
|
||||||
|
{
|
||||||
|
Ask = 0,
|
||||||
|
AlwaysSend = 1,
|
||||||
|
NeverSend = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] CrashReportingMode getCrashReportingMode() const;
|
||||||
|
void setCrashReportingMode(CrashReportingMode _crashReportingMode);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void langChanged();
|
void langChanged();
|
||||||
|
void crashReportingModeChanged(CrashReportingMode mode);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit PersonalSettings(const QString &settingPath, QObject *parent = nullptr);
|
explicit PersonalSettings(const QString &settingPath, QObject *parent = nullptr);
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,21 @@ if(WITH_CLIENT)
|
||||||
${CMAKE_SOURCE_DIR}/cockatrice/src
|
${CMAKE_SOURCE_DIR}/cockatrice/src
|
||||||
${CMAKE_SOURCE_DIR}/libcockatrice_utility
|
${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()
|
endif()
|
||||||
|
|
||||||
add_subdirectory(card_zone_algorithms)
|
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 <QDir>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
#include <QSettings>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
|
|
@ -367,6 +368,32 @@ TEST_F(SettingsDefaultsTest, Personal_ShowTipsOnStartup_Default)
|
||||||
ASSERT_EQ(s.getShowTipsOnStartup(), true);
|
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 ---
|
// --- DownloadSettings ---
|
||||||
|
|
||||||
TEST_F(SettingsDefaultsTest, Download_PicDownload_Default)
|
TEST_F(SettingsDefaultsTest, Download_PicDownload_Default)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue