d2b20284d1
After hard stop, vmware-vmx.exe briefly holds file locks causing deleteVM to fail with "Insufficient permissions". Replace the fixed 3s sleep with a vmrun-list poll loop (up to 20s) that waits until the VMX is no longer registered as running, then retry deleteVM up to 3x with 0/3/6s backoff before falling back to directory removal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
122 lines
4.5 KiB
PowerShell
122 lines
4.5 KiB
PowerShell
#Requires -Version 5.1
|
||
<#
|
||
.SYNOPSIS
|
||
Stops and permanently deletes an ephemeral build VM.
|
||
|
||
.DESCRIPTION
|
||
Performs a graceful shutdown (soft stop) with a timeout fallback to
|
||
a hard stop, then uses vmrun deleteVM to remove all VM files.
|
||
Finally, removes the clone directory from disk.
|
||
|
||
This script is designed to be called from a try/finally block so
|
||
it always runs, even on build failure.
|
||
|
||
.PARAMETER VMPath
|
||
Full path to the clone VM's .vmx file.
|
||
|
||
.PARAMETER VmrunPath
|
||
Full path to vmrun.exe.
|
||
Default: C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe
|
||
|
||
.PARAMETER GracefulTimeoutSeconds
|
||
Seconds to wait for graceful shutdown before forcing power-off.
|
||
Default: 15
|
||
|
||
.EXAMPLE
|
||
.\Remove-BuildVM.ps1 -VMPath "F:\CI\BuildVMs\Clone_run-42_20260508_143022\Clone_run-42_20260508_143022.vmx"
|
||
#>
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory)]
|
||
[string] $VMPath,
|
||
|
||
[string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe',
|
||
|
||
[ValidateRange(5, 60)]
|
||
[int] $GracefulTimeoutSeconds = 15
|
||
)
|
||
|
||
Set-StrictMode -Version Latest
|
||
$ErrorActionPreference = 'Continue' # Don't stop on errors — always try next step
|
||
|
||
$cloneDir = Split-Path $VMPath -Parent
|
||
|
||
Write-Host "[Remove-BuildVM] Destroying VM: $VMPath"
|
||
|
||
# ── Step 1: Check if VM exists at all ────────────────────────────────────────
|
||
if (-not (Test-Path $VMPath -PathType Leaf)) {
|
||
Write-Warning "[Remove-BuildVM] VMX file not found — nothing to remove: $VMPath"
|
||
# Still attempt to remove the directory in case of partial clone
|
||
if (Test-Path $cloneDir) {
|
||
Remove-Item $cloneDir -Recurse -Force -ErrorAction SilentlyContinue
|
||
}
|
||
return
|
||
}
|
||
|
||
# ── Step 2: Graceful stop ─────────────────────────────────────────────────────
|
||
Write-Host "[Remove-BuildVM] Sending soft stop..."
|
||
& $VmrunPath -T ws stop $VMPath soft 2>&1 | Out-Null
|
||
|
||
$deadline = (Get-Date).AddSeconds($GracefulTimeoutSeconds)
|
||
while ((Get-Date) -lt $deadline) {
|
||
Start-Sleep -Seconds 2
|
||
$stateOutput = & $VmrunPath -T ws getState $VMPath 2>&1
|
||
if ($stateOutput -notmatch 'running') {
|
||
Write-Host "[Remove-BuildVM] VM stopped gracefully."
|
||
break
|
||
}
|
||
}
|
||
|
||
# ── Step 3: Force stop if still running ──────────────────────────────────────
|
||
$stateOutput = & $VmrunPath -T ws getState $VMPath 2>&1
|
||
if ($stateOutput -match 'running') {
|
||
Write-Host "[Remove-BuildVM] Graceful stop timed out — forcing hard stop..."
|
||
& $VmrunPath -T ws stop $VMPath hard 2>&1 | Out-Null
|
||
}
|
||
|
||
# Poll vmrun list until VMX is gone — vmware-vmx.exe may hold file locks briefly
|
||
# after stop. Waiting for the entry to disappear ensures deleteVM won't race the
|
||
# process exit.
|
||
$vmxNorm = $VMPath.Trim().ToLower()
|
||
$releaseDeadline = (Get-Date).AddSeconds(20)
|
||
while ((Get-Date) -lt $releaseDeadline) {
|
||
$listedVMs = & $VmrunPath -T ws list 2>&1 |
|
||
Where-Object { $_ -notmatch '^Total running' } |
|
||
ForEach-Object { "$_".Trim().ToLower() }
|
||
if ($listedVMs -notcontains $vmxNorm) { break }
|
||
Start-Sleep -Seconds 2
|
||
}
|
||
|
||
# ── Step 4: Delete VM via vmrun — retry up to 3× with backoff ────────────────
|
||
Write-Host "[Remove-BuildVM] Deleting VM..."
|
||
$deleteOutput = ''
|
||
$deleteExit = -1
|
||
foreach ($delay in @(0, 3, 6)) {
|
||
if ($delay -gt 0) {
|
||
Write-Host "[Remove-BuildVM] Retrying deleteVM in ${delay}s..."
|
||
Start-Sleep -Seconds $delay
|
||
}
|
||
$deleteOutput = & $VmrunPath -T ws deleteVM $VMPath 2>&1
|
||
$deleteExit = $LASTEXITCODE
|
||
if ($deleteExit -eq 0) { break }
|
||
}
|
||
|
||
if ($deleteExit -ne 0) {
|
||
Write-Warning "[Remove-BuildVM] vmrun deleteVM failed after retries (exit $deleteExit): $deleteOutput"
|
||
Write-Warning "[Remove-BuildVM] Will attempt manual directory removal."
|
||
}
|
||
|
||
# ── Step 5: Remove clone directory (catches any leftover files) ───────────────
|
||
if (Test-Path $cloneDir) {
|
||
Write-Host "[Remove-BuildVM] Removing clone directory: $cloneDir"
|
||
Remove-Item $cloneDir -Recurse -Force -ErrorAction SilentlyContinue
|
||
if (Test-Path $cloneDir) {
|
||
Write-Warning "[Remove-BuildVM] Clone directory could not be fully removed: $cloneDir"
|
||
Write-Warning " Manual cleanup required."
|
||
} else {
|
||
Write-Host "[Remove-BuildVM] Clone directory removed."
|
||
}
|
||
}
|
||
|
||
Write-Host "[Remove-BuildVM] VM destruction complete."
|