mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-10-26 09:30:30 -05:00
[CrashReporting] Offer to send the installer log on abort (#7524)
* [CrashReporting] Installer log send offer on abort (inc 4) DSN parsed to CPACK_SENTRY_* (scheme-aware for http self-hosts) before include(CPack); new SendInstallerLog.ps1.in builds a byte-accurate Sentry envelope (128 KiB tail cap, X-Sentry-Auth, exit 0/1) with the abort reason read from the log file itself, so the NSIS side needs no string functions and quoting stays trivial. AbortInstall asks consent (attended installs only, silent path untouched, abort never swallowed). Empty/malformed DSN disables the offer; no secrets in repo. * [CrashReporting] Embed installer-log sender via a relative NSIS path The AbortInstall hook embedded SendInstallerLog.ps1 through the absolute CPACK_SENTRY_PS1_PATH, which carries CMake's forward slashes (D:/a/...). makensis does not treat a forward-slash drive path as absolute, so it resolved it relative to project.nsi, found nothing and aborted packaging on the Windows jobs of every branch in the stack. Embed it with the same '..\..\..\' relative path that already resolves NSIS.definitions.nsh: both are configure_file'd into the build root, three levels up from where CPack places project.nsi. Drop the now-unused CPACK_SENTRY_PS1_PATH cache variable. * [CrashReporting] Drop the installer /CRASHTEST hook, tidy the log sender Remove the /CRASHTEST switch and the matching COCKATRICE_SENTRY_DSN environment override on the installer abort path. With no deliberate in-installer trigger left, a package built without a baked DSN must never offer to post a user's log to an endpoint chosen by whatever happens to be in the environment, so the prompt is now gated on CPACK_SENTRY_ENABLED alone. This also retires the comment that claimed /S /R /CRASHTEST reaches the prompt: the switch was evaluated before /R set ReinstallMode, so that form never showed it. Alongside: correct the CPACK_SENTRY_RELEASE comment (PROJECT_VERSION keeps its "-<label>" on Development tags where CPACK_PACKAGE_VERSION does not), delete the unused CPACK_SENTRY_HOST/CPACK_SENTRY_PROJECT_ID cache vars, document the DSN regex's path-prefix fallback, log a declined send, keep the uploaded installer log free of helper-injected newlines, and bound the post with -TimeoutSec/-NonInteractive. * [CrashReporting] Derive the installer DSN vars as plain variables Review nit: every CPACK_SENTRY_* value is recomputed from COCKATRICE_SENTRY_DSN on each configure, so CACHE ... FORCE bought nothing. Set empty defaults once, override them in the match branch, and drop the duplicated else() copies. Still CPACK_-prefixed, so include(CPack) still snapshots them into CPackConfig.cmake. * [CrashReporting] Gate the installer log send at build time Review nit: test CPACK_SENTRY_ENABLED with the NSIS preprocessor instead of copying it into $SentryEnabled and branching at runtime, so a package built without a DSN compiles the prompt out rather than carrying dead code. The configure_file that produces the helper is gated the same way, so a build without reporting neither references nor embeds it. * [CrashReporting] Disclose the Windows user name in the send prompt Review nit: say that the log can include the Windows user name (a real part of what gets uploaded) and drop the "No account needed." clause, which answered a question the prompt never raised. * [CrashReporting] Show progress while the installer log is sent Review nit: DetailPrint a line before nsExec runs, so the details pane says something while the send is in flight instead of staying blank for up to the 60 s /TIMEOUT. * [CrashReporting] Rename the Sentry event local in the installer helper Review nit: $Event is a PowerShell automatic variable, so name the local $sentryEvent instead. Also drop $firstLine: Select-String returns the matching line alone, so the reason is already one physical line and the extra split added nothing. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
@@ -337,6 +337,15 @@ elseif(WIN32)
|
||||
# Configure file with custom definitions for NSIS.
|
||||
configure_file("${COCKATRICE_CMAKE_PATH}/NSIS.definitions.nsh.in" "${PROJECT_BINARY_DIR}/NSIS.definitions.nsh")
|
||||
|
||||
# Installer-log sender for the AbortInstall Sentry hook (values parsed out of COCKATRICE_SENTRY_DSN in
|
||||
# cmake/CrashReporting.cmake). Generated only when a DSN was parsed: cmake/NSIS.template.in embeds it under
|
||||
# the same CPACK_SENTRY_ENABLED check, so a build without reporting never references or carries the helper.
|
||||
if(CPACK_SENTRY_ENABLED)
|
||||
configure_file(
|
||||
"${COCKATRICE_CMAKE_PATH}/SendInstallerLog.ps1.in" "${PROJECT_BINARY_DIR}/SendInstallerLog.ps1" @ONLY
|
||||
)
|
||||
endif()
|
||||
|
||||
# Every runtime DLL a Windows install has to contain, as paths relative to the
|
||||
# install root. .ci/compile.sh reads this and fails the build when the packaged
|
||||
# installer is missing one of them, or carries a runtime it does not know about.
|
||||
|
||||
@@ -179,3 +179,34 @@ if(WITH_CRASH_REPORTING
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Installer failure reporting (increment 4): parse COCKATRICE_SENTRY_DSN into the parts the Windows installer
|
||||
# needs. These are CPACK_* so CPack snapshots them at include(CPack) time and substitutes them into
|
||||
# cmake/NSIS.template.in at package time; on non-Windows they are inert (only the NSIS template consumes them).
|
||||
# Set from COCKATRICE_SENTRY_DSN on every configure, so a changed or removed DSN can never leave a stale value
|
||||
# behind. Plain variables, not CACHE: they only need to be in scope when include(CPack) later snapshots every
|
||||
# CPACK_* variable into CPackConfig.cmake.
|
||||
set(CPACK_SENTRY_ENABLED "0")
|
||||
set(CPACK_SENTRY_KEY "")
|
||||
set(CPACK_SENTRY_ENVELOPE_URL "")
|
||||
set(CPACK_SENTRY_DSN_FULL "")
|
||||
# Deliberately PROJECT_VERSION: that is what the client bakes as its Sentry release and what CI passes to
|
||||
# `sentry-cli debug-files upload`, so installer and client events group under one release. It is not the
|
||||
# CPACK_PACKAGE_VERSION the installer logs in its own log line — on Development tags PROJECT_VERSION carries a
|
||||
# "-<label>" suffix (getversion.cmake) while CPACK_PACKAGE_VERSION is only MAJOR.MINOR.PATCH.
|
||||
set(CPACK_SENTRY_RELEASE "${PROJECT_VERSION}")
|
||||
|
||||
# Canonical <scheme>://<key>@<host>/<project-id>. Deliberately unanchored, so a trailing slash still matches.
|
||||
# A self-hosted DSN with a path prefix (e.g. GlitchTip behind a subpath) does not match and falls through to the
|
||||
# disabled-and-warn branch instead of deriving a wrong endpoint from the tail of the path.
|
||||
if(COCKATRICE_SENTRY_DSN MATCHES "(https?)://([^@]+)@([^/]+)/([0-9]+)")
|
||||
set(CPACK_SENTRY_ENABLED "1")
|
||||
set(CPACK_SENTRY_KEY "${CMAKE_MATCH_2}")
|
||||
# Scheme comes from the DSN, not an assumption: self-hosted (GlitchTip) DSNs may be plain http.
|
||||
set(CPACK_SENTRY_ENVELOPE_URL "${CMAKE_MATCH_1}://${CMAKE_MATCH_3}/api/${CMAKE_MATCH_4}/envelope/")
|
||||
set(CPACK_SENTRY_DSN_FULL "${COCKATRICE_SENTRY_DSN}")
|
||||
elseif(NOT COCKATRICE_SENTRY_DSN STREQUAL "")
|
||||
message(WARNING "COCKATRICE_SENTRY_DSN is set but malformed "
|
||||
"(expected <scheme>://<key>@<host>/<project-id>); installer log reporting disabled"
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -37,6 +37,9 @@ Var LogFindHandle
|
||||
Var LogFindName
|
||||
Var LogMatchLabel
|
||||
Var LogMatchPattern
|
||||
; Sentry installer-log hook state (see AbortInstall)
|
||||
Var SentryResult
|
||||
Var SentryOutput
|
||||
|
||||
!include LogicLib.nsh
|
||||
!include FileFunc.nsh
|
||||
@@ -909,6 +912,48 @@ Function AbortInstall
|
||||
${If} $ReinstallMode = 1
|
||||
${OrIfNot} ${Silent}
|
||||
MessageBox MB_OK|MB_ICONEXCLAMATION "$AbortReason$\r$\n$\r$\nA detailed installer log was written to:$\r$\n$InstallLogPath"
|
||||
; Offer to send the installer log to Sentry so aborted installs can be diagnosed. This block already runs
|
||||
; only when someone is at the keyboard (attended install, or the updater's /R upgrade whose launcher has
|
||||
; quit), so a silent or package-manager install never reaches the prompt and its behavior is unchanged.
|
||||
; Only a package built with a baked DSN offers to send, so the prompt can only appear when a send
|
||||
; can actually go out. A preprocessor guard rather than a runtime test: a package built without a
|
||||
; DSN compiles the block out entirely, so it cannot even carry the helper or its baked endpoint.
|
||||
; No /SD here: a /SD would suppress this prompt on the silent /R upgrade, which is exactly the
|
||||
; attended-update case this offer exists for. The runtime gate above (ReinstallMode/Silent) is the
|
||||
; hang protection — a package-manager /S install without /R never reaches this box, matching the
|
||||
; two sibling MessageBox sites in this file.
|
||||
!if "@CPACK_SENTRY_ENABLED@" == "1"
|
||||
MessageBox MB_YESNO|MB_ICONQUESTION "Send the installer log to the developers to help fix this? The log contains file paths from this machine, which may include your Windows user name." IDYES sentry_send
|
||||
!insertmacro CockLog "sentry send: declined by user"
|
||||
Goto sentry_done
|
||||
sentry_send:
|
||||
; The abort reason is already in the log file (CockLog writes "ABORT: ..." as the
|
||||
; first thing AbortInstall does), so the helper reads it from there. Nothing
|
||||
; reason-shaped crosses the command line, which keeps quoting trivial and needs
|
||||
; no NSIS string functions.
|
||||
InitPluginsDir
|
||||
; Embedded by the same relative path that resolves NSIS.definitions.nsh above (both are
|
||||
; configure_file'd into the build root, which is three levels up from project.nsi). An
|
||||
; absolute CMake path here carries forward slashes, which makensis does not treat as
|
||||
; absolute and so fails to find the file at package time.
|
||||
File "/oname=$PLUGINSDIR\send-installer-log.ps1" "..\..\..\SendInstallerLog.ps1"
|
||||
; The send can take up to the /TIMEOUT below, so put something on the details pane
|
||||
; the moment the user says yes rather than leaving the window blank until it returns.
|
||||
DetailPrint "Sending installer log..."
|
||||
nsExec::ExecToStack /TIMEOUT=60000 'powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\send-installer-log.ps1" -LogPath "$InstallLogPath"'
|
||||
Pop $SentryResult
|
||||
Pop $SentryOutput
|
||||
; $SentryOutput is a single line by construction: the helper flattens its message
|
||||
; and this log file is uploaded verbatim, so a stray newline must not be able to
|
||||
; forge extra lines into the artifact we send.
|
||||
!insertmacro CockLog "sentry send: result=$SentryResult output=$SentryOutput"
|
||||
${If} $SentryResult == "0"
|
||||
MessageBox MB_OK|MB_ICONINFORMATION "The installer log was sent. Thank you for helping improve Cockatrice!"
|
||||
${Else}
|
||||
MessageBox MB_OK|MB_ICONEXCLAMATION "The installer log could not be sent. You can still share it manually; it was written to:$\r$\n$InstallLogPath"
|
||||
${EndIf}
|
||||
sentry_done:
|
||||
!endif
|
||||
${EndIf}
|
||||
SetErrorLevel 1
|
||||
Abort
|
||||
|
||||
120
cmake/SendInstallerLog.ps1.in
Normal file
120
cmake/SendInstallerLog.ps1.in
Normal file
@@ -0,0 +1,120 @@
|
||||
# Sends the NSIS installer log to Sentry as an envelope carrying one error
|
||||
# event plus the log as an attachment.
|
||||
#
|
||||
# Generated at configure time from this template by CMake configure_file: the
|
||||
# CPACK_SENTRY_* placeholders are baked in then, so the real values only ever
|
||||
# exist in the build directory and the packaged installer, never in the repo.
|
||||
# Runs on stock Windows PowerShell 5.1 with no extra modules.
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$LogPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$EnvelopeUrl = '@CPACK_SENTRY_ENVELOPE_URL@'
|
||||
$SentryKey = '@CPACK_SENTRY_KEY@'
|
||||
$Release = '@CPACK_SENTRY_RELEASE@'
|
||||
$Dsn = '@CPACK_SENTRY_DSN_FULL@'
|
||||
|
||||
try {
|
||||
# The envelope endpoint is TLS-only; stock powershell.exe on older systems
|
||||
# still negotiates TLS 1.0 otherwise, so opt into 1.2 explicitly.
|
||||
try {
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
||||
} catch {
|
||||
}
|
||||
|
||||
# --- Log payload, tail-capped at 128 KiB ---------------------------------
|
||||
$MaxLogBytes = 128 * 1024
|
||||
$TruncNotice = '[log truncated to last 128 KB]'
|
||||
if (Test-Path -LiteralPath $LogPath) {
|
||||
$raw = [System.IO.File]::ReadAllBytes($LogPath)
|
||||
} else {
|
||||
$raw = [System.Text.Encoding]::UTF8.GetBytes('(installer log not found: ' + $LogPath + ')')
|
||||
}
|
||||
if ($raw.Length -gt $MaxLogBytes) {
|
||||
$tail = New-Object byte[] $MaxLogBytes
|
||||
[System.Buffer]::BlockCopy($raw, $raw.Length - $MaxLogBytes, $tail, 0, $MaxLogBytes)
|
||||
$prefix = [System.Text.Encoding]::UTF8.GetBytes($TruncNotice + "`r`n")
|
||||
$logBytes = New-Object byte[] ($prefix.Length + $tail.Length)
|
||||
[System.Buffer]::BlockCopy($prefix, 0, $logBytes, 0, $prefix.Length)
|
||||
[System.Buffer]::BlockCopy($tail, 0, $logBytes, $prefix.Length, $tail.Length)
|
||||
} else {
|
||||
$logBytes = $raw
|
||||
}
|
||||
|
||||
# --- Abort reason, read from the log itself --------------------------------
|
||||
# AbortInstall logs "ABORT: ..." first, so the reason travels inside the file
|
||||
# and nothing reason-shaped crosses the command line (no quoting hazards).
|
||||
# Select-String yields the matching line alone, so the reason is already one
|
||||
# physical line for the message; anything else stays in the attached log.
|
||||
# Non-ASCII paths may garble here; the attachment is byte-exact regardless.
|
||||
$Reason = 'unknown (no ABORT line in log)'
|
||||
if (Test-Path -LiteralPath $LogPath) {
|
||||
$abortLine = Select-String -LiteralPath $LogPath -Pattern '^ABORT: ' | Select-Object -Last 1
|
||||
if ($abortLine) {
|
||||
$Reason = ($abortLine.Line -replace '^ABORT:\s*', '')
|
||||
}
|
||||
}
|
||||
|
||||
# --- Event ---------------------------------------------------------------
|
||||
$eventId = [guid]::NewGuid().ToString('N')
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||
# $Event is an automatic variable (Microsoft.PowerShell.Utility\Register-ObjectEvent),
|
||||
# so the local is named $sentryEvent to avoid shadowing it.
|
||||
$sentryEvent = [ordered]@{
|
||||
event_id = $eventId
|
||||
timestamp = $now
|
||||
platform = 'other'
|
||||
level = 'error'
|
||||
logger = 'cockatrice-installer'
|
||||
release = $Release
|
||||
environment = 'installer'
|
||||
sdk = @{ name = 'cockatrice.installer'; version = $Release }
|
||||
message = @{ formatted = 'Installer aborted: ' + $Reason }
|
||||
extra = @{
|
||||
reason = $Reason
|
||||
os = [System.Environment]::OSVersion.VersionString
|
||||
}
|
||||
}
|
||||
$eventJson = $sentryEvent | ConvertTo-Json -Compress -Depth 6
|
||||
$eventBytes = [System.Text.Encoding]::UTF8.GetBytes($eventJson)
|
||||
|
||||
# --- Envelope, assembled as bytes (the log is opaque binary) -------------
|
||||
$sentAt = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||
$headerLine = '{"event_id":"' + $eventId + '","sent_at":"' + $sentAt + '","dsn":"' + $Dsn + '"}'
|
||||
$eventHeader = '{"type":"event","length":' + $eventBytes.Length + '}'
|
||||
$attachHeader = '{"type":"attachment","filename":"Cockatrice-installer.log","content_type":"text/plain","length":' + $logBytes.Length + '}'
|
||||
$enc = [System.Text.Encoding]::UTF8
|
||||
$lf = [byte[]]@(10)
|
||||
$body = New-Object System.IO.MemoryStream
|
||||
foreach ($chunk in @($enc.GetBytes($headerLine), $lf, $enc.GetBytes($eventHeader), $lf, $eventBytes, $lf, $enc.GetBytes($attachHeader), $lf, $logBytes)) {
|
||||
$body.Write($chunk, 0, $chunk.Length)
|
||||
}
|
||||
$bodyBytes = $body.ToArray()
|
||||
$body.Close()
|
||||
|
||||
# --- Send ----------------------------------------------------------------
|
||||
# The key travels in the auth header only; stdout below never contains it.
|
||||
$auth = 'Sentry sentry_version=7, sentry_client=cockatrice.installer/' + $Release + ', sentry_key=' + $SentryKey
|
||||
Invoke-RestMethod -Uri $EnvelopeUrl -Method Post -TimeoutSec 45 -ContentType 'application/x-sentry-envelope' -Headers @{ 'X-Sentry-Auth' = $auth } -Body $bodyBytes | Out-Null
|
||||
Write-Output ('SENT ' + $eventId)
|
||||
exit 0
|
||||
} catch {
|
||||
# Scrub the key/DSN out of the failure text in case a transport error echoes them.
|
||||
$msg = $_.Exception.Message
|
||||
if ($msg -and $SentryKey) {
|
||||
$msg = $msg.Replace($SentryKey, '<redacted>')
|
||||
}
|
||||
if ($msg -and $Dsn) {
|
||||
$msg = $msg.Replace($Dsn, '<redacted>')
|
||||
}
|
||||
# Flatten to one line: this text is echoed into the installer log, which is
|
||||
# itself uploaded as an attachment, so a multi-line transport error must not
|
||||
# be able to forge extra lines in the artifact we send.
|
||||
if ($msg) {
|
||||
$msg = ($msg -replace '\s+', ' ').Trim()
|
||||
}
|
||||
Write-Output ('FAILED: ' + $msg)
|
||||
exit 1
|
||||
}
|
||||
Reference in New Issue
Block a user