From 65447d765d17d46ae26acdfe335bd1b555ef6ff0 Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Mon, 24 Aug 2026 13:01:00 -0400 Subject: [PATCH] Fix deploy wipe/extract race that could break admin site deploys Remove-Item can report success while NTFS is still asynchronously finishing a large recursive delete, so Expand-Archive -Force could see leftover entries, queue them for removal itself, and lose the race against our own delete completing - failing with "Cannot find path ... because it does not exist" on the next deploy. --- scripts/deploy-iis-remote-apply.ps1 | 35 ++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/scripts/deploy-iis-remote-apply.ps1 b/scripts/deploy-iis-remote-apply.ps1 index db6d223..e6077df 100644 --- a/scripts/deploy-iis-remote-apply.ps1 +++ b/scripts/deploy-iis-remote-apply.ps1 @@ -53,23 +53,38 @@ function Wait-AppPoolFullyStopped { # Wipes are attempted a few times with a short pause - file handles (IIS or AV scanning # the just-stopped worker process's files) can take a moment to release even after the # worker process itself is confirmed gone. +# +# Remove-Item returning without an error is not proof the directory is actually empty yet - +# NTFS can finish a large recursive delete asynchronously, so a Get-ChildItem right after can +# still show remnants. If Expand-Archive -Force then runs against those remnants, it queues +# them for removal itself and can lose the race against our own delete finishing, failing with +# "Cannot find path ... because it does not exist". So after Remove-Item reports success, poll +# until the directory is verifiably empty before trusting the wipe is done. function Remove-DirectoryContentsWithRetry { - param([string]$Path, [int]$MaxAttempts = 5, [int]$DelayMs = 2000) + param([string]$DirPath, [int]$MaxAttempts = 5, [int]$DelayMs = 2000) for($attempt = 1; $attempt -le $MaxAttempts; $attempt++){ - $failure = $null + $problem = $null try { - Remove-Item -Recurse -Force $Path -ErrorAction Stop - return + Remove-Item -Recurse -Force (Join-Path $DirPath '*') -ErrorAction Stop } catch { - $failure = $_ - if($attempt -lt $MaxAttempts){ - Write-Host "Wipe attempt $attempt failed (files still locked?) - retrying in $($DelayMs)ms" - Start-Sleep -Milliseconds $DelayMs + $problem = "delete failed: $($_.Exception.Message)" + } + + if(!$problem){ + if(!(Get-ChildItem -Force -LiteralPath $DirPath -ErrorAction SilentlyContinue)){ + return } + $problem = 'residual files still present after delete (async NTFS delete still finishing?)' + } + + if($attempt -lt $MaxAttempts){ + Write-Host "Wipe attempt $attempt failed ($problem) - retrying in $($DelayMs)ms" + Start-Sleep -Milliseconds $DelayMs + } else { + throw "Wipe of $DirPath did not complete after $MaxAttempts attempts - $problem" } } - throw $failure } if(!(Get-Website -Name $SiteName -ErrorAction SilentlyContinue)){ @@ -106,7 +121,7 @@ if(![string]::IsNullOrWhiteSpace($AdminSiteName) -and (Get-Website -Name $AdminS if(Test-Path $RemoteDir){ Write-Host "Wiping $RemoteDir" - Remove-DirectoryContentsWithRetry -Path (Join-Path $RemoteDir '*') + Remove-DirectoryContentsWithRetry -DirPath $RemoteDir } else { New-Item -ItemType Directory -Force -Path $RemoteDir | Out-Null }