#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."