#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(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string] $VMPath, [string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe', [ValidateRange(1, 60)] [int] $GracefulTimeoutSeconds = 15 ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Continue' # Don't stop on errors — always try next step Import-Module (Join-Path $PSScriptRoot '_Common.psm1') -Force $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) -and $PSCmdlet.ShouldProcess($cloneDir, 'Remove partial clone directory')) { Remove-Item $cloneDir -Recurse -Force -ErrorAction SilentlyContinue } return } if (-not $PSCmdlet.ShouldProcess($VMPath, 'Stop and delete build VM')) { return } # ── Step 2: Graceful stop ───────────────────────────────────────────────────── Write-Host "[Remove-BuildVM] Sending soft stop (timeout: ${GracefulTimeoutSeconds}s)..." $stopSoft = Invoke-VmrunBounded -VmrunPath $VmrunPath ` -Operation stop -Arguments @($VMPath, 'soft') -TimeoutSeconds $GracefulTimeoutSeconds if ($stopSoft.TimedOut) { Write-Warning "[Remove-BuildVM] Soft stop timed out after ${GracefulTimeoutSeconds}s." } elseif ($stopSoft.ExitCode -eq 0) { Write-Host "[Remove-BuildVM] VM stopped gracefully." } # ── Step 3: Force stop if still running ────────────────────────────────────── if ($stopSoft.TimedOut -or $stopSoft.ExitCode -ne 0) { Write-Host "[Remove-BuildVM] Graceful stop did not succeed — forcing hard stop..." $stopHard = Invoke-VmrunBounded -VmrunPath $VmrunPath ` -Operation stop -Arguments @($VMPath, 'hard') -TimeoutSeconds 60 if ($stopHard.TimedOut) { Write-Warning "[Remove-BuildVM] Hard stop timed out after 60s." } } # 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 } $delResult = Invoke-VmrunBounded -VmrunPath $VmrunPath ` -Operation deleteVM -Arguments @($VMPath) -TimeoutSeconds 90 $deleteOutput = $delResult.Output $deleteExit = $delResult.ExitCode if ($delResult.TimedOut) { Write-Warning "[Remove-BuildVM] deleteVM timed out after 90s." $deleteExit = -1 } 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."