Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions src/Squid.Core/Resources/Upgrade/upgrade-windows-tentacle.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

# Force English/invariant .NET messages so exception text is plain ASCII in the
# web log on non-Latin OS locales. On a Chinese-Windows host a DNS failure
# otherwise surfaces as localized (non-Latin) text that the web log renders as
# mojibake. Best-effort -- never let a culture set fail the run.
try { [System.Threading.Thread]::CurrentThread.CurrentUICulture = [System.Globalization.CultureInfo]::GetCultureInfo('en-US') } catch { }

# -- Identity gate ----------------------------------------------
# The WindowsTentacleUpgradeStrategy wraps invocation in a
# Task Scheduler one-shot task with `/RU SYSTEM` -- equivalent to Linux's
Expand Down Expand Up @@ -238,9 +244,14 @@ function Write-UpgradeStatus {
function Append-UpgradeLog {
param([string] $Line)

# Sanitize to printable ASCII (keep tab) so OS-localized exception text or
# non-Latin paths can never mojibake in the web-surfaced upgrade log. With
# CurrentUICulture forced to en-US the content is already English; this is the
# belt-and-braces guarantee for any message that bypasses the culture setting.
$safeLine = $Line -replace '[^\x09\x20-\x7E]', '?'
$stamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
Add-Content -Path $LOG_FILE -Value "[$stamp] $Line"
Write-Host $Line
Add-Content -Path $LOG_FILE -Value "[$stamp] $safeLine"
Write-Host $safeLine
}

# -- Generic retry helper -- run an action, retrying transient failures with -----
Expand Down Expand Up @@ -301,7 +312,7 @@ function Write-UpgradeEvent {
# Minimal JSON-safe escaping: drop quotes and backslashes (events originate
# from our own controlled strings -- versions, methods, exit codes). Matches
# the Linux emit_event `tr -d '"\\'`.
$safeMsg = $Msg -replace '["\\]', ''
$safeMsg = $Msg -replace '["\\]', '' -replace '[^\x20-\x7E]', '?'

$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
$line = '{"t":"' + $now + '","phase":"' + $Phase + '","kind":"' + $Kind + '","msg":"' + $safeMsg + '"}'
Expand Down Expand Up @@ -499,8 +510,8 @@ if (Test-Path $LOCK_FILE) {
Set-Content -Path $LOCK_FILE -Value "$PID" -Force

try {
Append-UpgradeLog "[upgrade] Phase A starting -- target version $TARGET_VERSION on $RID"
Write-UpgradeStatus -Status 'IN_PROGRESS' -Detail "Phase A starting (target $TARGET_VERSION)"
Append-UpgradeLog "[upgrade] Preparing upgrade to $TARGET_VERSION on $RID"
Write-UpgradeStatus -Status 'IN_PROGRESS' -Detail "Preparing upgrade to $TARGET_VERSION"

# -- Already-on-target short-circuit -------------------------------------
# If the running binary is already at the target version, short-circuit.
Expand Down Expand Up @@ -711,7 +722,7 @@ try {
}

$INSTALL_OK = $true
Append-UpgradeLog "[upgrade-method:zip] Phase A complete -- staging dir $extractDir ready for swap"
Append-UpgradeLog "[upgrade-method:zip] Download and verification complete -- ready to install"
}
catch {
Append-UpgradeLog "[upgrade-method:zip] Unexpected failure: $($_.Exception.Message)"
Expand All @@ -735,7 +746,7 @@ try {
# Tentacle process tree without a detach wrapper, Stop-Service will
# terminate this process before Phase B completes -- an orchestration
# concern owned by E.3's strategy, not by this template.
Append-UpgradeLog "[upgrade] Phase B starting -- stopping service '$SERVICE_NAME' and swapping binary"
Append-UpgradeLog "[upgrade] Installing $TARGET_VERSION -- stopping service '$SERVICE_NAME' and swapping binary"

# Entering Phase B -- events emitted from here are tagged phase 'B'.
$script:CURRENT_PHASE = 'B'
Expand Down Expand Up @@ -766,7 +777,7 @@ try {
# touched, so any failure leaves it intact. Flat: legacy move-aside .bak swap.
if ($isVersioned) {
if (-not (Test-Path $extractDir)) {
Append-UpgradeLog "::error:: Phase B can't find Phase A staging dir at expected path: $extractDir"
Append-UpgradeLog "::error:: Cannot find the staged files at expected path: $extractDir"
Write-UpgradeStatus -Status 'ROLLBACK_CRITICAL_FAILED' -InstallMethod 'zip' -Detail "Staging dir disappeared between phases: $extractDir" -ExitCode 14
exit 14
}
Expand Down Expand Up @@ -838,7 +849,7 @@ try {
# line ~145 already prevents the truly concurrent case, but reading
# the in-scope variable removes any remaining ambiguity.
if (-not (Test-Path $extractDir)) {
Append-UpgradeLog "::error:: Phase B can't find Phase A staging dir at expected path: $extractDir"
Append-UpgradeLog "::error:: Cannot find the staged files at expected path: $extractDir"
Write-UpgradeStatus -Status 'ROLLBACK_CRITICAL_FAILED' -InstallMethod 'zip' -Detail "Staging dir disappeared between phases: $extractDir" -ExitCode 14
exit 14
}
Expand Down Expand Up @@ -925,7 +936,7 @@ try {
}

Write-UpgradeStatus -Status 'SUCCESS' -InstallMethod $INSTALL_METHOD -Detail "Upgrade to $TARGET_VERSION complete"
Append-UpgradeLog "[upgrade] Phase B complete -- version $TARGET_VERSION installed via $INSTALL_METHOD"
Append-UpgradeLog "[upgrade] Upgrade complete -- version $TARGET_VERSION installed via $INSTALL_METHOD"

# Version GC (versioned only, best-effort): keep the newest N version dirs and
# prune older ones so versions\ doesn't grow unbounded. Runs only AFTER success
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,37 @@ public void Resource_DownloadValidatedAsZip_RejectsNonZipContent()
content.ShouldContain("0x4B",
customMessage: "must validate the download's second byte is the zip 'PK' magic byte 0x4B — rejects HTML/JSON error pages and truncated downloads of any size");
}

[Fact]
public void Resource_UpgradeLog_UsesPlainLanguage_NotPhaseAbJargon()
{
// Operator feedback: "Phase A / Phase B" jargon is opaque in the web log.
// The raw upgrade log must describe what's happening in plain language.
// (The structured event `phase` field stays 'A'/'B' as the data contract;
// the web relabels it for display.)
var content = LoadResource();

content.ShouldContain("Preparing upgrade to",
customMessage: "the download phase must log 'Preparing upgrade to <version>', not 'Phase A starting'");
content.ShouldContain("Installing $TARGET_VERSION",
customMessage: "the swap phase must log 'Installing <version>', not 'Phase B starting'");
content.ShouldContain("Upgrade complete --",
customMessage: "completion must log 'Upgrade complete', not 'Phase B complete'");
}

[Fact]
public void Resource_ForcesEnglishCulture_AndSanitizesLog_PreventsExceptionMojibake()
{
// Exception messages on a non-Latin OS (e.g. Chinese Windows) are localized
// and mojibake in the web-surfaced upgrade log. Force en-US .NET messages so
// the text is English, AND ASCII-sanitize every log line as belt-and-braces.
var content = LoadResource();

content.ShouldContain("CurrentUICulture",
customMessage: "must force the thread UI culture so .NET exception messages render in English, not the OS locale");
content.ShouldContain("'en-US'",
customMessage: "must pin en-US so exception text is plain ASCII English regardless of OS locale");
content.ShouldMatch(@"\$safeLine\s*=\s*\$Line\s*-replace",
customMessage: "Append-UpgradeLog must ASCII-sanitize each line so OS-localized exception text can't mojibake the web log");
}
}
Loading