fix(installer): the adopt-existing guard never fired

Stage 4 decided whether the IIS objects it was about to reconcile were its own by
testing for .installed-version. Stage 2 writes that file, and stage 2 always runs
first in a '-Stage all' install - so by the time the guard looked, the stamp it
had just written made every server look like one this installer built, including
the hand-built ones the guard exists to protect.

Stage 2 now records whether a stamp was present BEFORE it writes its own, and
stage 4 reads that observation. Running stage 4 alone still tests the file, which
is correct there: no stage 2 has run to disturb it.

Found by review, not by test - the guard has no coverage, because exercising it
needs a live IIS.
This commit is contained in:
cproudlock
2026-08-03 13:47:07 -04:00
parent 3606d8d696
commit e58f376643

View File

@@ -149,6 +149,11 @@ try { if (-not (Test-Path $script:LogDir)) { New-Item -ItemType Directory -Path
catch { $script:LogDir = $env:TEMP }
$script:LogPath = Join-Path $script:LogDir ("shopdb-install-{0}.log" -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
$script:Created = New-Object System.Collections.ArrayList # for rollback
# Was this server already stamped by a previous run of THIS installer, as observed
# BEFORE stage 2 writes its own stamp? Stage 4 needs the answer to know whether the
# IIS objects it is about to reconcile are its own. $null means nothing has looked
# yet, which is the case when stage 4 is run on its own.
$script:PreexistingInstallStamp = $null
function Write-Log {
param([string] $Message, [string] $Level = 'INFO')
@@ -763,6 +768,12 @@ function Invoke-Stage2 {
if (-not (Test-Path $AppSource)) { Fail "application payload not found: $AppSource" }
Assert-BundleIntegrity
Assert-VenvMatchesWheelhouse
# Captured HERE, before the stamp below is written. Stage 4's guard against
# touching an installation this did not create tested the stamp directly, and
# stage 2 always runs first in a '-Stage all' install - so the stamp it had
# just written made every server look like one of ours and the guard never
# fired on the case it exists for.
$script:PreexistingInstallStamp = Test-Path (Join-Path $AppRoot '.installed-version')
# --- Python, all users --------------------------------------------------
# A per-user install lands in %LOCALAPPDATA%, which the IIS app-pool identity
@@ -1493,8 +1504,15 @@ has to come from the bundle either way.
# -MountAlias would delete a live mount with no prompt and no error, and the
# first sign would be the site 404ing.
#
# 'Ours' means there is a version stamp, which only this installer writes.
$weBuiltThis = Test-Path (Join-Path $AppRoot '.installed-version')
# 'Ours' means there was a version stamp BEFORE this run started - only this
# installer writes one. Stage 2 records that observation; testing the file
# here instead would read the stamp stage 2 has already written.
$weBuiltThis = if ($null -ne $script:PreexistingInstallStamp) {
$script:PreexistingInstallStamp
} else {
# Stage 4 run on its own, so stage 2 has not written anything this run.
Test-Path (Join-Path $AppRoot '.installed-version')
}
if (-not $WhatIfOnly) {
$doomed = @()
if ($MountAlias) {