Stop-WebAppPool only requests a stop - the w3wp.exe worker process can keep running for a few seconds afterwards, still holding the site's files open, so the previous fix (stopping both sites before the wipe) wasn't sufficient on its own. - Wait-AppPoolFullyStopped: polls app pool state until Stopped (30s timeout), then force-kills any lingering w3wp.exe for that pool - Remove-DirectoryContentsWithRetry: retries the wipe a few times with a short delay, since file handles can take a moment to release even after the worker process is confirmed gonemaster
| @@ -25,6 +25,53 @@ param( | |||||
| $ErrorActionPreference = 'Stop' | $ErrorActionPreference = 'Stop' | ||||
| Import-Module WebAdministration | Import-Module WebAdministration | ||||
| # Stop-WebAppPool only requests a stop - the w3wp.exe worker process can keep running for a | |||||
| # few seconds afterwards (finishing in-flight requests), still holding the site's files open. | |||||
| # Wait for the pool to actually report Stopped, then force-kill any worker process that's | |||||
| # still lingering past the timeout so the wipe below doesn't hit "Access is denied". | |||||
| function Wait-AppPoolFullyStopped { | |||||
| param([string]$PoolName, [int]$TimeoutSec = 30) | |||||
| if([string]::IsNullOrWhiteSpace($PoolName)){ return } | |||||
| $deadline = (Get-Date).AddSeconds($TimeoutSec) | |||||
| while((Get-Date) -lt $deadline){ | |||||
| if((Get-WebAppPoolState -Name $PoolName -ErrorAction SilentlyContinue).Value -eq 'Stopped'){ | |||||
| return | |||||
| } | |||||
| Start-Sleep -Milliseconds 500 | |||||
| } | |||||
| Write-Host "App pool $PoolName did not report Stopped within ${TimeoutSec}s - killing any lingering worker process" | |||||
| Get-CimInstance Win32_Process -Filter "Name = 'w3wp.exe'" -ErrorAction SilentlyContinue | | |||||
| Where-Object { $_.CommandLine -like ('*' + $PoolName + '*') } | | |||||
| ForEach-Object { | |||||
| try { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } catch { } | |||||
| } | |||||
| } | |||||
| # 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. | |||||
| function Remove-DirectoryContentsWithRetry { | |||||
| param([string]$Path, [int]$MaxAttempts = 5, [int]$DelayMs = 2000) | |||||
| for($attempt = 1; $attempt -le $MaxAttempts; $attempt++){ | |||||
| $failure = $null | |||||
| try { | |||||
| Remove-Item -Recurse -Force $Path -ErrorAction Stop | |||||
| return | |||||
| } catch { | |||||
| $failure = $_ | |||||
| if($attempt -lt $MaxAttempts){ | |||||
| Write-Host "Wipe attempt $attempt failed (files still locked?) - retrying in $($DelayMs)ms" | |||||
| Start-Sleep -Milliseconds $DelayMs | |||||
| } | |||||
| } | |||||
| } | |||||
| throw $failure | |||||
| } | |||||
| if(!(Get-Website -Name $SiteName -ErrorAction SilentlyContinue)){ | if(!(Get-Website -Name $SiteName -ErrorAction SilentlyContinue)){ | ||||
| throw "IIS site '$SiteName' does not exist yet. Create the site and app pool once manually (or via a one-time setup script) before running this deploy." | throw "IIS site '$SiteName' does not exist yet. Create the site and app pool once manually (or via a one-time setup script) before running this deploy." | ||||
| } | } | ||||
| @@ -35,6 +82,7 @@ if(!(Test-Path ('IIS:\AppPools\' + $AppPool))){ | |||||
| Write-Host "Stopping IIS site $SiteName and app pool $AppPool" | Write-Host "Stopping IIS site $SiteName and app pool $AppPool" | ||||
| try { Stop-Website -Name $SiteName } catch { } | try { Stop-Website -Name $SiteName } catch { } | ||||
| try { Stop-WebAppPool -Name $AppPool } catch { } | try { Stop-WebAppPool -Name $AppPool } catch { } | ||||
| Wait-AppPoolFullyStopped -PoolName $AppPool | |||||
| # The admin site's physical path may already point inside RemoteDir (e.g. RemoteDir\public-admin) | # The admin site's physical path may already point inside RemoteDir (e.g. RemoteDir\public-admin) | ||||
| # from a previous deploy - if it's left running, IIS holds those files open and the wipe below | # from a previous deploy - if it's left running, IIS holds those files open and the wipe below | ||||
| @@ -44,12 +92,13 @@ if(![string]::IsNullOrWhiteSpace($AdminSiteName) -and (Get-Website -Name $AdminS | |||||
| try { Stop-Website -Name $AdminSiteName } catch { } | try { Stop-Website -Name $AdminSiteName } catch { } | ||||
| if(![string]::IsNullOrWhiteSpace($AdminAppPool)){ | if(![string]::IsNullOrWhiteSpace($AdminAppPool)){ | ||||
| try { Stop-WebAppPool -Name $AdminAppPool } catch { } | try { Stop-WebAppPool -Name $AdminAppPool } catch { } | ||||
| Wait-AppPoolFullyStopped -PoolName $AdminAppPool | |||||
| } | } | ||||
| } | } | ||||
| if(Test-Path $RemoteDir){ | if(Test-Path $RemoteDir){ | ||||
| Write-Host "Wiping $RemoteDir" | Write-Host "Wiping $RemoteDir" | ||||
| Remove-Item -Recurse -Force (Join-Path $RemoteDir '*') -ErrorAction SilentlyContinue | |||||
| Remove-DirectoryContentsWithRetry -Path (Join-Path $RemoteDir '*') | |||||
| } else { | } else { | ||||
| New-Item -ItemType Directory -Force -Path $RemoteDir | Out-Null | New-Item -ItemType Directory -Force -Path $RemoteDir | Out-Null | ||||
| } | } | ||||
Powered by TurnKey Linux.