Cockatrice/cmake/SendInstallerLog.ps1.in
Lukas Brübach 0403ac9354 [Installer] Make the Inno Setup installer the default, with its failure hook.
COCKATRICE_WIN_INSTALLER now defaults to InnoSetup. NSIS stays selectable for
one release as the legacy fallback, and the two can still be built from one
tree and compared on a real upgrade.

The switches question from the previous commit is settled from Inno Setup's
source: the setup command line is an if/else-if chain over the switches it
knows, with no fallback that rejects the rest, so /R, /PID= and /D= are
ignored rather than refused. The client therefore sends one command line that
drives either installer:

- /R and /PID= for NSIS, which also needs /D= last and unquoted;
- /VERYSILENT and the CloseApplications set for Setup, which releases the
  mapped runtime DLLs through Restart Manager instead of waiting for a pid;
- /UPDATER as the client's own mark, read only by the Inno Setup script.

An older client that passes only the NSIS switches still installs; it walks
the wizard once instead of running silently.

The Sentry offer that AbortInstall carried is ported: a silent run the updater
launched, which began replacing files and never reached the end, offers to
send its log through the same helper. The helper is embedded with a [Files]
dontcopy entry and extracted at send time; a repeated [Files] section merges
with the generated one instead of being a compile error. Setup prefixes every
log line with a timestamp, so the helper's ABORT scan now allows that prefix.

CI installs Inno Setup next to NSIS, dumps ISCCOutput.log and the generated
script when the installer fails to compile, and --package NSIS|InnoSetup
selects the installer through the CMake option instead of adding a second
CPack generator to the run.
2026-10-26 08:29:22 +01:00

124 lines
5.9 KiB
PowerShell

# Sends the installer log, from either installer, 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 --------------------------------
# Each installer records the reason before offering to send, so it travels inside the
# file and nothing reason-shaped crosses the command line (no quoting hazards).
# NSIS writes the line as "ABORT: ..." through CockLog. Inno Setup prefixes every line
# of its log with a timestamp and three spaces, so that prefix is optional here and
# removed together with the marker. 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.
$stamp = '\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\s+'
$Reason = 'unknown (no ABORT line in log)'
if (Test-Path -LiteralPath $LogPath) {
$abortLine = Select-String -LiteralPath $LogPath -Pattern ('^(' + $stamp + ')?ABORT: ') | Select-Object -Last 1
if ($abortLine) {
$Reason = ($abortLine.Line -replace ('^(' + $stamp + ')?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
}