mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-08 02:53:25 -08:00
[CrashReporting] Breakpad everywhere, session detection, symbol CI (inc 3)
Breakpad on all platforms (no crashpad packaging); remove the Windows MiniDumpWithFullMemory filter and the installer WER LocalDumps full-dump registration. QLockFile per-process session tracking replaces the Windows-only .dmp scan, placed after parser.process and the instance hand-off so help/version exits and handed-off secondaries never leak a lock. CI: Release-only DSN bake from SENTRY_DSN secret, tag-only sentry-cli symbol upload (org cockatrice, project native). Tests: 533/533 ctest green, format clean, code gate PASS.
This commit is contained in:
parent
69b421f1c2
commit
c40763e61d
8 changed files with 362 additions and 102 deletions
|
|
@ -206,6 +206,14 @@ if [[ ${WITH_CRASH_REPORTING:-} == "ON" ]]; then
|
||||||
# developer builds never reach Sentry.
|
# developer builds never reach Sentry.
|
||||||
flags+=("-DWITH_CRASH_REPORTING=ON")
|
flags+=("-DWITH_CRASH_REPORTING=ON")
|
||||||
fi
|
fi
|
||||||
|
if [[ ${BUILDTYPE:-Release} == "Release" && -n ${COCKATRICE_SENTRY_DSN:-} ]]; then
|
||||||
|
# Release-only: Debug builds stay DSN-free so crashes from developer builds
|
||||||
|
# never reach Sentry. Empty (local builds, forks without the secret, and non-tag
|
||||||
|
# CI builds, which never receive the secret) keeps today's mock fallback via the
|
||||||
|
# compiled-in default. CI passes the value from the SENTRY_DSN secret on tag
|
||||||
|
# Release builds only, so pre-release test artifacts never report to Sentry.
|
||||||
|
flags+=("-DCOCKATRICE_SENTRY_DSN=$COCKATRICE_SENTRY_DSN")
|
||||||
|
fi
|
||||||
|
|
||||||
# Add cmake --build flags
|
# Add cmake --build flags
|
||||||
buildflags=(--config "$BUILDTYPE")
|
buildflags=(--config "$BUILDTYPE")
|
||||||
|
|
|
||||||
108
.github/workflows/desktop-build.yml
vendored
108
.github/workflows/desktop-build.yml
vendored
|
|
@ -46,6 +46,11 @@ jobs:
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.configure.outputs.tag }}
|
tag: ${{ steps.configure.outputs.tag }}
|
||||||
sha: ${{ steps.configure.outputs.sha }}
|
sha: ${{ steps.configure.outputs.sha }}
|
||||||
|
# MAJ.MIN.PATCH[-beta[.N]] of a tag build; empty otherwise. Mirrors what
|
||||||
|
# cmake/getversion.cmake derives as PROJECT_VERSION, which the client bakes as its
|
||||||
|
# Sentry release (cmake/CrashReporting.cmake) — symbol uploads must use the same
|
||||||
|
# string so stack traces de-symbolicate against this build's binaries.
|
||||||
|
sentry_release: ${{ steps.configure.outputs.sentry_release }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: "Configure"
|
- name: "Configure"
|
||||||
|
|
@ -56,6 +61,9 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release
|
if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then # release
|
||||||
echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
|
echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
|
||||||
|
# Tag format is YYYY-MM-DD-{Release|Development}-MAJ.MIN.PATCH[-beta[.N]];
|
||||||
|
# PROJECT_VERSION is the tail after the type (see cmake/getversion.cmake).
|
||||||
|
echo "sentry_release=$(sed -E 's/^[0-9]{4}-[0-9]{2}-[0-9]{2}-(Release|Development)-//' <<< "$GITHUB_REF_NAME")" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT"
|
echo "sha=$RESOLVED_SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
|
@ -240,6 +248,11 @@ jobs:
|
||||||
package: '${{ matrix.package }}'
|
package: '${{ matrix.package }}'
|
||||||
server_only: '${{ matrix.server_only }}'
|
server_only: '${{ matrix.server_only }}'
|
||||||
WITH_CRASH_REPORTING: 'ON'
|
WITH_CRASH_REPORTING: 'ON'
|
||||||
|
# Tag builds only: artifacts from master pushes and PR branches must not report to
|
||||||
|
# Sentry. Forks get no secrets on pull_request, so they keep the mock fallback either way.
|
||||||
|
# configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN
|
||||||
|
# and the symbol upload below just like full releases.
|
||||||
|
COCKATRICE_SENTRY_DSN: ${{ needs.configure.outputs.tag != null && secrets.SENTRY_DSN || '' }}
|
||||||
run: |
|
run: |
|
||||||
source .ci/docker.sh
|
source .ci/docker.sh
|
||||||
args=()
|
args=()
|
||||||
|
|
@ -248,9 +261,47 @@ jobs:
|
||||||
args+=(--ccache "$CCACHE_SIZE")
|
args+=(--ccache "$CCACHE_SIZE")
|
||||||
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
args+=(--cmake-generator "$CMAKE_GENERATOR")
|
||||||
args+=(--suffix "$SUFFIX")
|
args+=(--suffix "$SUFFIX")
|
||||||
|
# The container inherits no environment, so the DSN rides in explicitly. -e without
|
||||||
|
# =value passes it through (possibly empty); compile.sh only consumes it for Release
|
||||||
|
# configures, and empty keeps the mock fallback.
|
||||||
|
export RUN_ARGS="${RUN_ARGS:-} -e COCKATRICE_SENTRY_DSN"
|
||||||
|
|
||||||
RUN --server --release --package "$package" "${args[@]}"
|
RUN --server --release --package "$package" "${args[@]}"
|
||||||
|
|
||||||
|
- name: "Install sentry-cli"
|
||||||
|
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
SENTRY_CLI_VERSION: '3.8.0'
|
||||||
|
run: |
|
||||||
|
export INSTALL_DIR="$GITHUB_WORKSPACE/.sentry-cli"
|
||||||
|
curl -sL https://sentry.io/get-cli/ | bash
|
||||||
|
echo "$GITHUB_WORKSPACE/.sentry-cli" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: "Upload debug symbols to Sentry"
|
||||||
|
# Like the DSN above, this runs for every tag — beta (Development) tags included.
|
||||||
|
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
|
||||||
|
# A Sentry outage or rotated token must not take down the release assets below.
|
||||||
|
continue-on-error: true
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
# Same release string the client reports its events under (COCKATRICE_SENTRY_RELEASE
|
||||||
|
# = PROJECT_VERSION, see cmake/CrashReporting.cmake), so stack traces from this build
|
||||||
|
# de-symbolicate against these symbols.
|
||||||
|
SENTRY_RELEASE: ${{ needs.configure.outputs.sentry_release }}
|
||||||
|
run: |
|
||||||
|
paths=()
|
||||||
|
for dir in build/cockatrice build/oracle build/servatrice; do
|
||||||
|
[[ -d $dir ]] && paths+=("$dir")
|
||||||
|
done
|
||||||
|
if [[ ${#paths[@]} == 0 ]]; then
|
||||||
|
echo "No build output dirs found, skipping symbol upload"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Project slug on Sentry; must match the project the baked DSN points at.
|
||||||
|
sentry-cli debug-files upload --org cockatrice --project cockatrice-desktop --release "$SENTRY_RELEASE" "${paths[@]}"
|
||||||
|
|
||||||
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
# Delete used cache to emulate a ccache update. See https://github.com/actions/cache/issues/342
|
||||||
- name: "Delete remote compiler cache (ccache)"
|
- name: "Delete remote compiler cache (ccache)"
|
||||||
if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
|
if: github.ref == 'refs/heads/master' && steps.restore_ccache.outputs.cache-hit
|
||||||
|
|
@ -536,6 +587,11 @@ jobs:
|
||||||
BUILDTYPE: '${{ matrix.type }}'
|
BUILDTYPE: '${{ matrix.type }}'
|
||||||
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
|
CCACHE_EVICTION_AGE: ${{ matrix.ccache_eviction_age }}
|
||||||
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
CMAKE_GENERATOR: ${{ matrix.cmake_generator }}
|
||||||
|
# Tag builds only: artifacts from master pushes and PR branches must not report to
|
||||||
|
# Sentry. Forks get no secrets on pull_request, so they keep the mock fallback either way.
|
||||||
|
# configure.outputs.tag is set for ANY tag, so beta (Development) tags get the DSN
|
||||||
|
# and the symbol upload below just like full releases.
|
||||||
|
COCKATRICE_SENTRY_DSN: ${{ needs.configure.outputs.tag != null && secrets.SENTRY_DSN || '' }}
|
||||||
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
|
DEVELOPER_DIR: '/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer'
|
||||||
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
|
GITHUB_TOKEN: ${{ github.token }} # needed for vcpkg dependency graph updates, see VCPKG_FEATURE_FLAGS
|
||||||
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
||||||
|
|
@ -563,6 +619,58 @@ jobs:
|
||||||
WITH_CRASH_REPORTING: ${{ matrix.type == 'Release' && 'ON' || '' }}
|
WITH_CRASH_REPORTING: ${{ matrix.type == 'Release' && 'ON' || '' }}
|
||||||
run: .ci/compile.sh --server --test --vcpkg
|
run: .ci/compile.sh --server --test --vcpkg
|
||||||
|
|
||||||
|
- name: "Install sentry-cli"
|
||||||
|
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
SENTRY_CLI_VERSION: '3.8.0'
|
||||||
|
run: |
|
||||||
|
export INSTALL_DIR="$GITHUB_WORKSPACE/.sentry-cli"
|
||||||
|
curl -sL https://sentry.io/get-cli/ | bash
|
||||||
|
echo "$GITHUB_WORKSPACE/.sentry-cli" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: "Upload debug symbols to Sentry"
|
||||||
|
# Like the DSN above, this runs for every tag — beta (Development) tags included.
|
||||||
|
if: needs.configure.outputs.tag != null && matrix.make_package == '1'
|
||||||
|
# A Sentry outage or rotated token must not take down the release assets below.
|
||||||
|
continue-on-error: true
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
# Same release string the client reports its events under (COCKATRICE_SENTRY_RELEASE
|
||||||
|
# = PROJECT_VERSION, see cmake/CrashReporting.cmake), so stack traces from this build
|
||||||
|
# de-symbolicate against these symbols.
|
||||||
|
SENTRY_RELEASE: ${{ needs.configure.outputs.sentry_release }}
|
||||||
|
run: |
|
||||||
|
paths=()
|
||||||
|
if [[ '${{ matrix.os }}' == 'Windows' ]]; then
|
||||||
|
# Top-level CMakeLists.txt links /DEBUG even in release, so PDBs land
|
||||||
|
# next to the binaries (same dirs as the PDB artifact step below).
|
||||||
|
for dir in build/cockatrice/Release build/oracle/Release build/servatrice/Release; do
|
||||||
|
[[ -d $dir ]] && paths+=("$dir")
|
||||||
|
done
|
||||||
|
else
|
||||||
|
# Release builds carry DWARF (-g, kept by CrashReporting.cmake); macOS keeps its
|
||||||
|
# debug info in dSYM bundles, so materialize them before uploading.
|
||||||
|
for app in cockatrice oracle servatrice; do
|
||||||
|
binary="build/$app/$app.app/Contents/MacOS/$app"
|
||||||
|
if [[ -f $binary ]]; then
|
||||||
|
if [[ '${{ matrix.os }}' == 'macOS' ]]; then
|
||||||
|
dsymutil "$binary" -o "$binary.dSYM"
|
||||||
|
paths+=("$binary.dSYM")
|
||||||
|
else
|
||||||
|
paths+=("$binary")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [[ ${#paths[@]} == 0 ]]; then
|
||||||
|
echo "No symbol paths found, skipping symbol upload"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Project slug on Sentry; must match the project the baked DSN points at.
|
||||||
|
sentry-cli debug-files upload --org cockatrice --project cockatrice-desktop --release "$SENTRY_RELEASE" "${paths[@]}"
|
||||||
|
|
||||||
# CPack reports a failed makensis run as "Problem running NSIS command"
|
# CPack reports a failed makensis run as "Problem running NSIS command"
|
||||||
# plus a path to NSISOutput.log, and that log lives only on the runner, so
|
# plus a path to NSISOutput.log, and that log lives only on the runner, so
|
||||||
# the actual compile error is otherwise nowhere in the workflow output.
|
# the actual compile error is otherwise nowhere in the workflow output.
|
||||||
|
|
|
||||||
|
|
@ -46,12 +46,14 @@ if(WITH_CRASH_REPORTING AND WITH_CLIENT)
|
||||||
if(ENABLE_SANITIZERS)
|
if(ENABLE_SANITIZERS)
|
||||||
# The crash handler cannot run under ASan/UBSan, so there is no backend to capture with.
|
# The crash handler cannot run under ASan/UBSan, so there is no backend to capture with.
|
||||||
set(_sentry_backend "none")
|
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()
|
else()
|
||||||
# In-process, no extra binary to ship.
|
# Breakpad on every OS (increment 3 probe, 2026-10-14): the crashpad sources
|
||||||
|
# do vendor friction-free (FetchContent inits all submodules recursively by
|
||||||
|
# default, verified populated in a local build tree), but the crashpad backend
|
||||||
|
# would need its out-of-process crashpad_handler binary installed next to the
|
||||||
|
# exe, into the macOS bundle, and through NSIS, plus handler-path wiring and
|
||||||
|
# notarization care. Breakpad is in-process, needs no extra binary and no
|
||||||
|
# packaging on any OS sentry-native builds, so simplicity wins.
|
||||||
set(_sentry_backend "breakpad")
|
set(_sentry_backend "breakpad")
|
||||||
endif()
|
endif()
|
||||||
# FORCE so toggling ENABLE_SANITIZERS on a reused build dir cannot leave a stale backend behind.
|
# FORCE so toggling ENABLE_SANITIZERS on a reused build dir cannot leave a stale backend behind.
|
||||||
|
|
@ -61,6 +63,18 @@ if(WITH_CRASH_REPORTING AND WITH_CLIENT)
|
||||||
)
|
)
|
||||||
unset(_sentry_backend)
|
unset(_sentry_backend)
|
||||||
|
|
||||||
|
if(NOT MSVC)
|
||||||
|
# Symbol upload needs debug info in Release binaries: the top-level flags build GCC
|
||||||
|
# Release as -s -O2 (stripped, no DWARF) and Clang/Apple Release as plain -O2 (no DWARF),
|
||||||
|
# both of which upload nothing useful. Keep -g and drop the strip flag while the SDK is
|
||||||
|
# on; the OFF default leaves local, distro and Debug builds untouched. MSVC already
|
||||||
|
# links /DEBUG with PDBs in release (see top-level CMakeLists.txt), so it needs nothing.
|
||||||
|
string(REGEX REPLACE "(^| )-s( |$)" "\\1\\2" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||||
|
if(NOT CMAKE_CXX_FLAGS_RELEASE MATCHES "(^| )-g( |$)")
|
||||||
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -g")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
# No Qt integration: it runs find_package(Qt6) inside the vendored build, which
|
# 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
|
# 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
|
# breadcrumbs could carry unredacted log lines into native crash events that bypass
|
||||||
|
|
|
||||||
|
|
@ -1530,11 +1530,18 @@ ${If} $PortableMode = 0
|
||||||
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||||
IntFmt $0 "0x%08X" $0
|
IntFmt $0 "0x%08X" $0
|
||||||
|
|
||||||
; Enable Windows User-Mode Dumps
|
; No Windows Error Reporting LocalDumps registration: crash capture belongs to
|
||||||
|
; the in-app reporter (Sentry/breakpad, MiniDumpNormal-class data only). Opting
|
||||||
|
; machines into OS-level full dumps (DumpType 2) would leave unredacted memory
|
||||||
|
; dumps on user machines that nothing consumes.
|
||||||
; https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
|
; https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
|
||||||
WriteRegExpandStr HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpFolder" "%LOCALAPPDATA%\CrashDumps\Cockatrice"
|
|
||||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpCount" "5"
|
; Older installers did register cockatrice.exe above (and the removed
|
||||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpType" "2"
|
; MiniDumpWithFullMemory writer filled that folder), and that key survives this
|
||||||
|
; install, so WER would keep writing full dumps. Remove it, and clear the
|
||||||
|
; legacy dumps nothing consumes anymore.
|
||||||
|
DeleteRegKey HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe"
|
||||||
|
RMDir /r "$LOCALAPPDATA\CrashDumps\Cockatrice"
|
||||||
|
|
||||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayIcon" "$INSTDIR\cockatrice.exe"
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayIcon" "$INSTDIR\cockatrice.exe"
|
||||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayName" "Cockatrice"
|
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayName" "Cockatrice"
|
||||||
|
|
@ -1635,6 +1642,12 @@ Section "un.Application" UnSecApplication
|
||||||
|
|
||||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice"
|
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice"
|
||||||
|
|
||||||
|
; Leftover WER LocalDumps key from pre-Sentry installers (see the installer note
|
||||||
|
; above): removing it here covers machines that uninstall without reinstalling.
|
||||||
|
; The legacy full-memory dumps go with it; nothing consumes them anymore.
|
||||||
|
DeleteRegKey HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe"
|
||||||
|
RMDir /r "$LOCALAPPDATA\CrashDumps\Cockatrice"
|
||||||
|
|
||||||
; Only remove the file/protocol associations if we registered them (i.e. the
|
; Only remove the file/protocol associations if we registered them (i.e. the
|
||||||
; install was not portable) and .cod is still owned by Cockatrice, so we don't
|
; install was not portable) and .cod is still owned by Cockatrice, so we don't
|
||||||
; clobber a .cod association installed by another application.
|
; clobber a .cod association installed by another application.
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,15 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QLockFile>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
#include <QThread>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <libcockatrice/settings/personal_settings.h>
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
|
|
@ -263,8 +266,13 @@ void CrashReporter::init(const QString &dsn,
|
||||||
sentry_options_set_release(options, release.toUtf8().constData());
|
sentry_options_set_release(options, release.toUtf8().constData());
|
||||||
sentry_options_set_database_path(options, sentryDatabasePath.toUtf8().constData());
|
sentry_options_set_database_path(options, sentryDatabasePath.toUtf8().constData());
|
||||||
// Consent-gated uploads: crashes are captured into the local database but
|
// Consent-gated uploads: crashes are captured into the local database but
|
||||||
// nothing leaves the machine until giveConsent().
|
// nothing leaves the machine until giveConsent(). cache_keep holds the previous run's
|
||||||
|
// crash envelope in <database>/cache instead of dropping it at init, so the Ask dialog
|
||||||
|
// decides its fate: giveConsent() flushes it via the transport retry, revokeConsent()
|
||||||
|
// deletes it below. This also covers the first crash after switching to AlwaysSend,
|
||||||
|
// which giveConsent() (running after init) would otherwise miss.
|
||||||
sentry_options_set_require_user_consent(options, 1);
|
sentry_options_set_require_user_consent(options, 1);
|
||||||
|
sentry_options_set_cache_keep(options, SENTRY_CACHE_KEEP_OFFLINE);
|
||||||
sentry_options_set_before_send(options, scrubEventBeforeSend, nullptr);
|
sentry_options_set_before_send(options, scrubEventBeforeSend, nullptr);
|
||||||
ready = (sentry_init(options) == 0);
|
ready = (sentry_init(options) == 0);
|
||||||
#else
|
#else
|
||||||
|
|
@ -291,6 +299,79 @@ void CrashReporter::shutdown()
|
||||||
ready = false;
|
ready = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CrashReporter::~CrashReporter() = default;
|
||||||
|
CrashReporter::CrashReporter() = default;
|
||||||
|
|
||||||
|
bool CrashReporter::beginSession(const QString &dir)
|
||||||
|
{
|
||||||
|
// Restartable: releasing first keeps begin->end->begin round-trips clean.
|
||||||
|
endSession();
|
||||||
|
|
||||||
|
QDir().mkpath(dir);
|
||||||
|
const QDir sessionDir(dir);
|
||||||
|
const QString selfName = QStringLiteral("session-%1").arg(QCoreApplication::applicationPid());
|
||||||
|
|
||||||
|
bool unclean = false;
|
||||||
|
const QStringList leftovers = sessionDir.entryList({QStringLiteral("session-*")}, QDir::Files);
|
||||||
|
for (const QString &name : leftovers) {
|
||||||
|
if (name == selfName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString path = sessionDir.filePath(name);
|
||||||
|
QLockFile probe(path);
|
||||||
|
// Long-lived session lock: stale means "owner dead", regardless of file age, so a
|
||||||
|
// crash followed by an immediate relaunch (younger than the 30 s default) is still
|
||||||
|
// detected. tryLock() only deletes the file when it finds it stale, so a live
|
||||||
|
// concurrent instance's lock is never touched here. (The static
|
||||||
|
// QLockFile::removeStaleLockFile(path) form does not exist in Qt5/Qt6, and the
|
||||||
|
// member removeStaleLockFile() force-removes without a staleness check, so neither
|
||||||
|
// is usable for this scan.)
|
||||||
|
probe.setStaleLockTime(0);
|
||||||
|
if (probe.tryLock(0)) {
|
||||||
|
unclean = true;
|
||||||
|
probe.unlock(); // deletes the stale file
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// tryLock failed: either a live concurrent instance owns the file, or it is
|
||||||
|
// unreadable (garbage/empty) and therefore has no live owner. Only the latter is
|
||||||
|
// cleaned up; a live instance is left alone. On Unix QLockFile creates the file
|
||||||
|
// before writing the pid, so a second instance starting at the same moment can
|
||||||
|
// briefly observe an empty (or half-written) file for a live owner: re-read once
|
||||||
|
// after a beat so a live owner's fresh lock is never mistaken for garbage and deleted.
|
||||||
|
qint64 pid = 0;
|
||||||
|
QString hostname, appname;
|
||||||
|
if (!probe.getLockInfo(&pid, &hostname, &appname)) {
|
||||||
|
QThread::msleep(100);
|
||||||
|
if (!probe.getLockInfo(&pid, &hostname, &appname)) {
|
||||||
|
QFile::remove(path);
|
||||||
|
unclean = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionLockPath = sessionDir.filePath(selfName);
|
||||||
|
sessionLock = std::make_unique<QLockFile>(sessionLockPath);
|
||||||
|
sessionLock->setStaleLockTime(0);
|
||||||
|
if (!sessionLock->tryLock(0)) {
|
||||||
|
qCWarning(CrashReportLog) << "Failed to claim session lock at" << sessionLockPath;
|
||||||
|
}
|
||||||
|
return unclean;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CrashReporter::endSession()
|
||||||
|
{
|
||||||
|
if (sessionLock) {
|
||||||
|
// unlock() removes the lock file; nothing left to clean up.
|
||||||
|
sessionLock->unlock();
|
||||||
|
sessionLock.reset();
|
||||||
|
} else if (!sessionLockPath.isEmpty()) {
|
||||||
|
// The lock was never claimed (a live concurrent instance owns its file): only our
|
||||||
|
// own unclaimed file, if tryLock left one behind, is removed.
|
||||||
|
QFile::remove(sessionLockPath);
|
||||||
|
}
|
||||||
|
sessionLockPath.clear();
|
||||||
|
}
|
||||||
|
|
||||||
void CrashReporter::giveConsent()
|
void CrashReporter::giveConsent()
|
||||||
{
|
{
|
||||||
#if COCKATRICE_WITH_CRASH_REPORTING
|
#if COCKATRICE_WITH_CRASH_REPORTING
|
||||||
|
|
@ -307,6 +388,10 @@ void CrashReporter::revokeConsent()
|
||||||
// return to unknown, which blocks uploads too but loses the decision.
|
// return to unknown, which blocks uploads too but loses the decision.
|
||||||
if (ready) {
|
if (ready) {
|
||||||
sentry_user_consent_revoke();
|
sentry_user_consent_revoke();
|
||||||
|
// Drop the held crash envelope: without this it would sit in <database>/cache and
|
||||||
|
// upload on the next give (e.g. after switching to AlwaysSend), after the user
|
||||||
|
// explicitly refused it. The SDK recreates the directory as needed.
|
||||||
|
QDir(sentryDatabasePath + QStringLiteral("/cache")).removeRecursively();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <libcockatrice/settings/personal_settings.h>
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#ifndef COCKATRICE_WITH_CRASH_REPORTING
|
#ifndef COCKATRICE_WITH_CRASH_REPORTING
|
||||||
#define COCKATRICE_WITH_CRASH_REPORTING 0
|
#define COCKATRICE_WITH_CRASH_REPORTING 0
|
||||||
|
|
@ -24,6 +25,8 @@
|
||||||
#define COCKATRICE_SENTRY_RELEASE ""
|
#define COCKATRICE_SENTRY_RELEASE ""
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
class QLockFile;
|
||||||
|
|
||||||
Q_DECLARE_LOGGING_CATEGORY(CrashReportLog);
|
Q_DECLARE_LOGGING_CATEGORY(CrashReportLog);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -34,21 +37,28 @@ Q_DECLARE_LOGGING_CATEGORY(CrashReportLog);
|
||||||
* consent) skips the SDK entirely, so the mock dialog fallback behaves exactly
|
* consent) skips the SDK entirely, so the mock dialog fallback behaves exactly
|
||||||
* as before. With a DSN, init() arms require-user-consent and before_send
|
* as before. With a DSN, init() arms require-user-consent and before_send
|
||||||
* scrubbing (stale persisted consent from a previous AlwaysSend run is cleared
|
* scrubbing (stale persisted consent from a previous AlwaysSend run is cleared
|
||||||
* before init when the mode is not AlwaysSend); giveConsent()/revokeConsent()
|
* before init when the mode is not AlwaysSend, and cached crash envelopes are
|
||||||
|
* held for the dialog instead of being dropped); giveConsent()/revokeConsent()
|
||||||
* gate uploads. attachToPendingCrashEnvelopes() folds the redacted log and the
|
* gate uploads. attachToPendingCrashEnvelopes() folds the redacted log and the
|
||||||
* optional user note into the pending native crash event(s) before consent
|
* optional user note into the pending native crash event(s) before consent
|
||||||
* releases them, so dump, stack, log and note arrive as ONE Sentry event;
|
* releases them, so dump, stack, log and note arrive as ONE Sentry event;
|
||||||
* sendReport() is the fallback for unclean exits that produced no native crash
|
* sendReport() is the fallback for unclean exits that produced no native crash
|
||||||
* event, capturing a redacted message event with the log preview attached. All
|
* event, capturing a redacted message event with the log preview attached. All
|
||||||
* SDK calls are guarded so OFF builds compile to no-ops.
|
* SDK calls are guarded so OFF builds compile to no-ops.
|
||||||
|
* Session tracking (beginSession/endSession) is SDK-independent: a QLockFile per
|
||||||
|
* process in the database dir detects unclean exits cross-platform, replacing the
|
||||||
|
* legacy Windows-only .dmp scan. The held lock makes the reporter non-copyable and
|
||||||
|
* non-movable.
|
||||||
*/
|
*/
|
||||||
class CrashReporter
|
class CrashReporter
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CrashReporter() = default;
|
CrashReporter();
|
||||||
CrashReporter(const CrashReporter &) = delete;
|
CrashReporter(const CrashReporter &) = delete;
|
||||||
CrashReporter &operator=(const CrashReporter &) = delete;
|
CrashReporter &operator=(const CrashReporter &) = delete;
|
||||||
~CrashReporter() = default;
|
CrashReporter(CrashReporter &&) = delete;
|
||||||
|
CrashReporter &operator=(CrashReporter &&) = delete;
|
||||||
|
~CrashReporter();
|
||||||
|
|
||||||
void init(const QString &dsn,
|
void init(const QString &dsn,
|
||||||
const QString &release,
|
const QString &release,
|
||||||
|
|
@ -140,12 +150,22 @@ public:
|
||||||
*/
|
*/
|
||||||
static QString describeOriginMismatch(const QString &envelopePath, const QString ¤tDsn);
|
static QString describeOriginMismatch(const QString &envelopePath, const QString ¤tDsn);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Claim this process's session lock, reporting a previous unclean exit.
|
||||||
|
* @return true if a leftover session lock from a dead process (or an unreadable
|
||||||
|
* garbage file) was found and cleaned; live concurrent instances are ignored.
|
||||||
|
*/
|
||||||
|
bool beginSession(const QString &dir);
|
||||||
|
void endSession();
|
||||||
|
|
||||||
static QString sanitizeLogLine(const QString &line);
|
static QString sanitizeLogLine(const QString &line);
|
||||||
static QString redactCockatriceUrls(const QString &line);
|
static QString redactCockatriceUrls(const QString &line);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool ready = false;
|
bool ready = false;
|
||||||
QString sentryDatabasePath;
|
QString sentryDatabasePath;
|
||||||
|
QString sessionLockPath;
|
||||||
|
std::unique_ptr<QLockFile> sessionLock;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,6 @@
|
||||||
#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>
|
||||||
|
|
@ -108,60 +105,6 @@ static void CockatriceLogger(QtMsgType type, const QMessageLogContext &ctx, cons
|
||||||
Logger::getInstance().log(type, ctx, logMessage);
|
Logger::getInstance().log(type, ctx, logMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
|
||||||
// clang-format off
|
|
||||||
#include <Windows.h>
|
|
||||||
#include <DbgHelp.h>
|
|
||||||
#include <ShlObj.h>
|
|
||||||
#include <ctime>
|
|
||||||
#include <filesystem>
|
|
||||||
|
|
||||||
#pragma comment(lib, "DbgHelp.lib") // Link the DbgHelp library
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
LONG WINAPI CockatriceUnhandledExceptionFilter(EXCEPTION_POINTERS *exceptionPointers)
|
|
||||||
{
|
|
||||||
std::filesystem::path path;
|
|
||||||
|
|
||||||
// Find %LOCALAPPDATA% (or cheat at finding it)
|
|
||||||
wchar_t *localAppDataFolder;
|
|
||||||
if (SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_CREATE, NULL, &localAppDataFolder) != S_OK) {
|
|
||||||
path = std::filesystem::temp_directory_path().parent_path().parent_path();
|
|
||||||
} else {
|
|
||||||
path = std::filesystem::path(localAppDataFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plan on writing crash files into %LOCALAPPDATA%/CrashDumps/Cockatrice/cockatrice.crash.*.dmp
|
|
||||||
path /= "CrashDumps";
|
|
||||||
path /= "Cockatrice";
|
|
||||||
if (!std::filesystem::exists(path)) {
|
|
||||||
std::filesystem::create_directories(path);
|
|
||||||
}
|
|
||||||
path /= "cockatrice.crash." + std::to_string(std::time(0)) + ".dmp";
|
|
||||||
|
|
||||||
// Create and write crash files
|
|
||||||
#ifdef UNICODE
|
|
||||||
HANDLE hDumpFile =
|
|
||||||
CreateFile(path.wstring().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
||||||
#else
|
|
||||||
HANDLE hDumpFile =
|
|
||||||
CreateFile(path.string().c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
MINIDUMP_EXCEPTION_INFORMATION mei;
|
|
||||||
mei.ExceptionPointers = exceptionPointers;
|
|
||||||
mei.ThreadId = GetCurrentThreadId();
|
|
||||||
mei.ClientPointers = 1;
|
|
||||||
|
|
||||||
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, MiniDumpWithFullMemory, &mei, nullptr,
|
|
||||||
nullptr);
|
|
||||||
|
|
||||||
CloseHandle(hDumpFile);
|
|
||||||
|
|
||||||
return EXCEPTION_EXECUTE_HANDLER;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void installNewTranslator()
|
void installNewTranslator()
|
||||||
{
|
{
|
||||||
QString lang = SettingsCache::instance().personal().getLang();
|
QString lang = SettingsCache::instance().personal().getLang();
|
||||||
|
|
@ -204,9 +147,9 @@ static QString redactActivationUrl(const QString &url)
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
#ifdef Q_OS_WIN
|
// No custom unhandled-exception filter: sentry owns native crash capture on all
|
||||||
SetUnhandledExceptionFilter(CockatriceUnhandledExceptionFilter);
|
// platforms. (The legacy MiniDumpWithFullMemory writer was removed as a privacy
|
||||||
#endif
|
// liability; unclean exits are detected via CrashReporter::beginSession below.)
|
||||||
|
|
||||||
// Logging setup
|
// Logging setup
|
||||||
#ifdef Q_OS_APPLE
|
#ifdef Q_OS_APPLE
|
||||||
|
|
@ -404,6 +347,16 @@ int main(int argc, char *argv[])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unclean-exit detection (cross-platform QLockFile session tracking, replacing the
|
||||||
|
// legacy Windows-only .dmp scan): a leftover session lock means the previous run died
|
||||||
|
// without reaching endSession(). Our own lock is held until the event loop finishes.
|
||||||
|
// Deliberately placed after parser.process() (--help/--version exit inside it and must
|
||||||
|
// not leak a lock) and after the instance hand-off (a handed-off secondary exits
|
||||||
|
// immediately and must never take a lock). Note this counts any death without
|
||||||
|
// endSession() as a crash, including SIGTERM at logout, Task Manager kills and
|
||||||
|
// session-end shutdowns; in AlwaysSend those send a report too.
|
||||||
|
const bool uncleanExit = crashReporter.beginSession(crashDatabasePath);
|
||||||
|
|
||||||
rng = new RNG_SFMT(CryptoUtil::randomUInt64());
|
rng = new RNG_SFMT(CryptoUtil::randomUInt64());
|
||||||
themeManager = new ThemeManager;
|
themeManager = new ThemeManager;
|
||||||
soundEngine = new SoundEngine;
|
soundEngine = new SoundEngine;
|
||||||
|
|
@ -499,30 +452,11 @@ int main(int argc, char *argv[])
|
||||||
|
|
||||||
// Crash-report flow: with a configured DSN the SDK owns capture and upload
|
// Crash-report flow: with a configured DSN the SDK owns capture and upload
|
||||||
// (consent-gated); with an empty DSN the dialog falls back to a local mock
|
// (consent-gated); with an empty DSN the dialog falls back to a local mock
|
||||||
// file and behaves exactly as before. Triggered by --simulate-crash or by
|
// file and behaves exactly as before. The trigger is --simulate-crash or an
|
||||||
// leftover Windows crash dumps, which are consumed after handling so one
|
// unclean previous exit (leftover session lock); the old
|
||||||
// crash never prompts twice.
|
// %LOCALAPPDATA%/CrashDumps .dmp scan is gone with the exception filter.
|
||||||
{
|
{
|
||||||
QString crashDumpDir;
|
if (parser.isSet("simulate-crash") || uncleanExit) {
|
||||||
#ifdef Q_OS_WIN
|
|
||||||
// Mirrors the writer (CockatriceUnhandledExceptionFilter): it appends CrashDumps/Cockatrice
|
|
||||||
// to %LOCALAPPDATA%, or, when its known-folder lookup fails, to the temp directory walked up
|
|
||||||
// two parent_path() steps. %LOCALAPPDATA% is this scan's stand-in for FOLDERID_LocalAppData:
|
|
||||||
// both come from the same profile, so when the lookup fails the variable is normally unset.
|
|
||||||
QString crashDumpRootPath = QString::fromLocal8Bit(qgetenv("LOCALAPPDATA"));
|
|
||||||
if (crashDumpRootPath.isEmpty()) {
|
|
||||||
// The writer's fallback expression, verbatim — QDir::cdUp() is existence-checked and
|
|
||||||
// would silently leave the path alone here, parent_path() is not.
|
|
||||||
const std::filesystem::path fallback = std::filesystem::temp_directory_path().parent_path().parent_path();
|
|
||||||
crashDumpRootPath = QString::fromStdWString(fallback.wstring());
|
|
||||||
}
|
|
||||||
crashDumpDir = QDir(crashDumpRootPath).absoluteFilePath(QStringLiteral("CrashDumps/Cockatrice"));
|
|
||||||
#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 =
|
const PersonalSettings::CrashReportingMode crashMode =
|
||||||
SettingsCache::instance().personal().getCrashReportingMode();
|
SettingsCache::instance().personal().getCrashReportingMode();
|
||||||
const bool crashReady = crashReporter.isReady();
|
const bool crashReady = crashReporter.isReady();
|
||||||
|
|
@ -549,10 +483,6 @@ int main(int argc, char *argv[])
|
||||||
crashDialog.exec();
|
crashDialog.exec();
|
||||||
}
|
}
|
||||||
// NeverSend: skip silently (no dialog, nothing written or sent).
|
// NeverSend: skip silently (no dialog, nothing written or sent).
|
||||||
// Handled dumps are consumed so one crash never prompts twice.
|
|
||||||
for (const QString &dump : crashDumps) {
|
|
||||||
QFile::remove(crashDumpDir + QStringLiteral("/") + dump);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -580,8 +510,6 @@ 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();
|
||||||
|
|
@ -589,5 +517,10 @@ int main(int argc, char *argv[])
|
||||||
UserLevelPixmapGenerator::clear();
|
UserLevelPixmapGenerator::clear();
|
||||||
ManaSymbolPixmapGenerator::clear();
|
ManaSymbolPixmapGenerator::clear();
|
||||||
|
|
||||||
|
// Released last: crashes anywhere in the teardown above (a common place for Qt apps to
|
||||||
|
// crash) are still captured by the SDK and detected as unclean exits on the next launch.
|
||||||
|
crashReporter.shutdown();
|
||||||
|
crashReporter.endSession();
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
#include "client/crash/crash_reporter.h"
|
#include "client/crash/crash_reporter.h"
|
||||||
|
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QLockFile>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QSysInfo>
|
||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
|
#include <QTextStream>
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include <libcockatrice/settings/personal_settings.h>
|
#include <libcockatrice/settings/personal_settings.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -339,6 +343,81 @@ TEST(CrashReporterTest, AttachSkipsNoteWhenEmptyAndFindsRunDirEnvelopes)
|
||||||
ASSERT_FALSE(content.contains(QByteArrayLiteral("user-note.txt")));
|
ASSERT_FALSE(content.contains(QByteArrayLiteral("user-note.txt")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(CrashReporterTest, BeginSessionOnEmptyDirReportsClean)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
CrashReporter reporter;
|
||||||
|
ASSERT_FALSE(reporter.beginSession(dir.path()));
|
||||||
|
reporter.endSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CrashReporterTest, BeginSessionIgnoresLiveConcurrentInstance)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
// A second QLockFile held in-process stands in for a live concurrent instance: same
|
||||||
|
// pid, alive, so the scan must leave it alone and report a clean start.
|
||||||
|
QLockFile liveInstance(dir.filePath(QStringLiteral("session-424242")));
|
||||||
|
ASSERT_TRUE(liveInstance.tryLock(0));
|
||||||
|
CrashReporter reporter;
|
||||||
|
ASSERT_FALSE(reporter.beginSession(dir.path()));
|
||||||
|
ASSERT_TRUE(QFile::exists(liveInstance.fileName()));
|
||||||
|
reporter.endSession();
|
||||||
|
liveInstance.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CrashReporterTest, BeginSessionDetectsStaleDeadOwnerLock)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
// A well-formed lock whose owner is dead: our own pid (so the host matches) but a
|
||||||
|
// process name that cannot match, which is how QLockFile spots pid reuse. Must be
|
||||||
|
// reported unclean and cleaned up, even though the file is brand new (this is why
|
||||||
|
// the scan probes with staleLockTime 0 rather than the 30 s default).
|
||||||
|
const QString stalePath = dir.filePath(QStringLiteral("session-stale-owner"));
|
||||||
|
QFile stale(stalePath);
|
||||||
|
ASSERT_TRUE(stale.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||||
|
QTextStream out(&stale);
|
||||||
|
out << QCoreApplication::applicationPid() << '\n'
|
||||||
|
<< QStringLiteral("definitely-not-the-test-binary") << '\n'
|
||||||
|
<< QSysInfo::machineHostName() << '\n';
|
||||||
|
stale.close();
|
||||||
|
|
||||||
|
CrashReporter reporter;
|
||||||
|
ASSERT_TRUE(reporter.beginSession(dir.path()));
|
||||||
|
ASSERT_FALSE(QFile::exists(stalePath));
|
||||||
|
reporter.endSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CrashReporterTest, BeginSessionCleansGarbageFileAndReportsUnclean)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
// An unreadable lock file has no live owner by definition: report unclean and remove it.
|
||||||
|
const QString garbagePath = dir.filePath(QStringLiteral("session-garbage"));
|
||||||
|
QFile garbage(garbagePath);
|
||||||
|
ASSERT_TRUE(garbage.open(QIODevice::WriteOnly));
|
||||||
|
garbage.write("not-a-lock-file\x00\xff", 17);
|
||||||
|
garbage.close();
|
||||||
|
|
||||||
|
CrashReporter reporter;
|
||||||
|
ASSERT_TRUE(reporter.beginSession(dir.path()));
|
||||||
|
ASSERT_FALSE(QFile::exists(garbagePath));
|
||||||
|
reporter.endSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CrashReporterTest, BeginSessionEndSessionRoundTripReportsClean)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
CrashReporter reporter;
|
||||||
|
ASSERT_FALSE(reporter.beginSession(dir.path()));
|
||||||
|
reporter.endSession();
|
||||||
|
ASSERT_FALSE(reporter.beginSession(dir.path()));
|
||||||
|
reporter.endSession();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue