fix(template): make prepare-win robust without prior Deploy run

- WinRM MaxProcessesPerShell: apply 100 if below threshold before asserting
- DesktopHeap SharedSection: apply 4096 KB registry value if below threshold before asserting
- Replace cleanmgr (hangs in WinRM Session 0, no window station) with direct
  PowerShell cleanup for dumps, WER files and Recycle Bin; DISM already covers
  component store and WU cache
- vcpkg git clone/checkout: wrap with EAP=Continue to suppress PS5.1
  NativeCommandError on git stderr (gotcha #3 from AGENTS.md)
- template.py: filter CI_EXITCODE:N marker from displayed output
- ci-static-ip.ps1: switch from vmware-rpctool (removed in recent Tools) to
  vmtoolsd --cmd; use temp-file redirect to capture output from native cmd

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 22:03:49 +02:00
parent faf5b9e29b
commit f090365d57
4 changed files with 84 additions and 99 deletions
+13 -4
View File
@@ -292,6 +292,13 @@ def _parse_exit_marker(stdout: str) -> int:
return 0 return 0
def _strip_exit_marker(stdout: str) -> str:
"""Remove ``CI_EXITCODE:N`` lines from output before display."""
return "\n".join(
line for line in stdout.splitlines() if not line.strip().startswith("CI_EXITCODE:")
)
def _wait_winrm(transport: WinRmTransport, timeout: int) -> None: def _wait_winrm(transport: WinRmTransport, timeout: int) -> None:
"""Poll *transport*.is_ready() until True or *timeout* seconds elapse.""" """Poll *transport*.is_ready() until True or *timeout* seconds elapse."""
deadline = time.monotonic() + timeout deadline = time.monotonic() + timeout
@@ -532,8 +539,9 @@ def template_prepare_win(
try: try:
result = _run_with_heartbeat(transport, invoke, check=False) result = _run_with_heartbeat(transport, invoke, check=False)
exit_code = _parse_exit_marker(result.stdout) exit_code = _parse_exit_marker(result.stdout)
if result.stdout.strip(): display = _strip_exit_marker(result.stdout).strip()
click.echo(result.stdout.strip()) if display:
click.echo(display)
if result.stderr.strip(): if result.stderr.strip():
click.echo(result.stderr.strip(), err=True) click.echo(result.stderr.strip(), err=True)
except TransportError: except TransportError:
@@ -658,8 +666,9 @@ Register-ScheduledTask -TaskName 'CI-StaticIp' `
) )
cleanup_result = _run_with_heartbeat(transport, cleanup_invoke, check=False) cleanup_result = _run_with_heartbeat(transport, cleanup_invoke, check=False)
cleanup_ec = _parse_exit_marker(cleanup_result.stdout) cleanup_ec = _parse_exit_marker(cleanup_result.stdout)
if cleanup_result.stdout.strip(): display = _strip_exit_marker(cleanup_result.stdout).strip()
click.echo(cleanup_result.stdout.strip()) if display:
click.echo(display)
if cleanup_result.stderr.strip(): if cleanup_result.stderr.strip():
click.echo(cleanup_result.stderr.strip(), err=True) click.echo(cleanup_result.stderr.strip(), err=True)
if cleanup_ec != 0: if cleanup_ec != 0:
+22 -37
View File
@@ -239,43 +239,15 @@ function Assert-Hash {
function Invoke-DiskCleanup { function Invoke-DiskCleanup {
Write-Step "Cleaning up disk" Write-Step "Cleaning up disk"
$cleanKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches' # cleanmgr.exe requires an interactive window station — hangs in WinRM Session 0.
$sageset = 1 # Replicate the relevant categories with direct PowerShell cleanup instead.
$cleanCategories = @( Write-Host "Clearing memory dumps..."
'Active Setup Temp Folders', Remove-Item 'C:\Windows\Minidump\*' -Force -Recurse -ErrorAction SilentlyContinue
'BranchCache', Remove-Item 'C:\Windows\memory.dmp' -Force -ErrorAction SilentlyContinue
'Downloaded Program Files', Write-Host "Clearing Windows Error Reporting files..."
'Internet Cache Files', Remove-Item 'C:\ProgramData\Microsoft\Windows\WER\*' -Force -Recurse -ErrorAction SilentlyContinue
'Memory Dump Files', Write-Host "Clearing Recycle Bin..."
'Old ChkDsk Files', Clear-RecycleBin -DriveLetter C -Force -ErrorAction SilentlyContinue
'Previous Installations',
'Recycle Bin',
'Service Pack Cleanup',
'Setup Log Files',
'System error memory dump files',
'System error minidump files',
'Temporary Files',
'Temporary Setup Files',
'Thumbnail Cache',
'Update Cleanup',
'Upgrade Discarded Files',
'Windows Defender',
'Windows Error Reporting Archive Files',
'Windows Error Reporting Files',
'Windows Error Reporting Queue Files',
'Windows Error Reporting Temp Files',
'Windows ESD installation files',
'Windows Upgrade Log Files'
)
foreach ($cat in $cleanCategories) {
$keyPath = "$cleanKey\$cat"
if (Test-Path $keyPath) {
Set-ItemProperty -Path $keyPath -Name "StateFlags$('{0:D4}' -f $sageset)" -Value 2 -Type DWord -Force -ErrorAction SilentlyContinue
}
}
Write-Host "Running cleanmgr /sagerun:$sageset (may take a few minutes)..."
$proc = Start-Process -FilePath 'cleanmgr.exe' -ArgumentList "/sagerun:$sageset" -Wait -PassThru
Write-Host "cleanmgr exited (code $($proc.ExitCode))."
Write-Host "Stopping WU-adjacent services before cache clear..." Write-Host "Stopping WU-adjacent services before cache clear..."
foreach ($svc in 'wuauserv', 'UsoSvc', 'bits', 'dosvc', 'TrustedInstaller') { foreach ($svc in 'wuauserv', 'UsoSvc', 'bits', 'dosvc', 'TrustedInstaller') {
@@ -437,9 +409,18 @@ Assert-Step 'WinRM' 'Shell MaxMemoryPerShellMB >= 2048' {
Assert-Step 'WinRM' 'Plugin Microsoft.PowerShell MaxMemoryPerShellMB >= 2048' { Assert-Step 'WinRM' 'Plugin Microsoft.PowerShell MaxMemoryPerShellMB >= 2048' {
[int](Get-Item 'WSMan:\localhost\Plugin\Microsoft.PowerShell\Quotas\MaxMemoryPerShellMB').Value -ge 2048 [int](Get-Item 'WSMan:\localhost\Plugin\Microsoft.PowerShell\Quotas\MaxMemoryPerShellMB').Value -ge 2048
} }
if ([int](Get-Item WSMan:\localhost\Shell\MaxProcessesPerShell -ErrorAction SilentlyContinue).Value -lt 100) {
Set-Item WSMan:\localhost\Shell\MaxProcessesPerShell 100 -Force
}
Assert-Step 'WinRM' 'MaxProcessesPerShell >= 100' { Assert-Step 'WinRM' 'MaxProcessesPerShell >= 100' {
[int](Get-Item WSMan:\localhost\Shell\MaxProcessesPerShell).Value -ge 100 [int](Get-Item WSMan:\localhost\Shell\MaxProcessesPerShell).Value -ge 100
} }
$_subsysKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems'
$_subsysCur = (Get-ItemProperty $_subsysKey -Name Windows).Windows
if (-not ($_subsysCur -match 'SharedSection=\d+,\d+,(\d+)') -or [int]$Matches[1] -lt 4096) {
$subsysNew = $_subsysCur -replace 'SharedSection=(\d+),(\d+),\d+', 'SharedSection=$1,$2,4096'
Set-ItemProperty $_subsysKey -Name Windows -Value $subsysNew
}
Assert-Step 'DesktopHeap' 'SharedSection non-interactive >= 4096 KB' { Assert-Step 'DesktopHeap' 'SharedSection non-interactive >= 4096 KB' {
$w = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems' -Name Windows).Windows $w = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems' -Name Windows).Windows
if ($w -match 'SharedSection=\d+,\d+,(\d+)') { [int]$Matches[1] -ge 4096 } else { $false } if ($w -match 'SharedSection=\d+,\d+,(\d+)') { [int]$Matches[1] -ge 4096 } else { $false }
@@ -1587,11 +1568,15 @@ if (Test-Path $vcpkgExe) {
$cloneArgs += '--depth' $cloneArgs += '--depth'
$cloneArgs += '1' $cloneArgs += '1'
} }
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
& $gitBin.Source @cloneArgs 2>&1 | Write-Host & $gitBin.Source @cloneArgs 2>&1 | Write-Host
$ErrorActionPreference = $savedEap
if ($LASTEXITCODE -ne 0) { throw "vcpkg git clone failed (exit $LASTEXITCODE)." } if ($LASTEXITCODE -ne 0) { throw "vcpkg git clone failed (exit $LASTEXITCODE)." }
if ($VcpkgRef -ne '') { if ($VcpkgRef -ne '') {
Write-Host "Checking out vcpkg ref: $VcpkgRef" Write-Host "Checking out vcpkg ref: $VcpkgRef"
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
& $gitBin.Source -C $vcpkgDir checkout $VcpkgRef 2>&1 | Write-Host & $gitBin.Source -C $vcpkgDir checkout $VcpkgRef 2>&1 | Write-Host
$ErrorActionPreference = $savedEap
if ($LASTEXITCODE -ne 0) { throw "vcpkg checkout '$VcpkgRef' failed (exit $LASTEXITCODE)." } if ($LASTEXITCODE -ne 0) { throw "vcpkg checkout '$VcpkgRef' failed (exit $LASTEXITCODE)." }
} }
Write-Host "Bootstrapping vcpkg (-disableMetrics)..." Write-Host "Bootstrapping vcpkg (-disableMetrics)..."
+23 -40
View File
@@ -239,47 +239,17 @@ function Assert-Hash {
function Invoke-DiskCleanup { function Invoke-DiskCleanup {
Write-Step "Cleaning up disk" Write-Step "Cleaning up disk"
# 1. Windows Disk Cleanup (cleanmgr) — run with /sagerun:1 using preset flags # cleanmgr.exe requires an interactive window station — hangs in WinRM Session 0.
# First register the flags via /sageset:1 in the registry (silent, no UI) # Replicate the relevant categories with direct PowerShell cleanup instead.
$cleanKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches' Write-Host "Clearing memory dumps..."
$sageset = 1 Remove-Item 'C:\Windows\Minidump\*' -Force -Recurse -ErrorAction SilentlyContinue
$cleanCategories = @( Remove-Item 'C:\Windows\memory.dmp' -Force -ErrorAction SilentlyContinue
'Active Setup Temp Folders', Write-Host "Clearing Windows Error Reporting files..."
'BranchCache', Remove-Item 'C:\ProgramData\Microsoft\Windows\WER\*' -Force -Recurse -ErrorAction SilentlyContinue
'Downloaded Program Files', Write-Host "Clearing Recycle Bin..."
'Internet Cache Files', Clear-RecycleBin -DriveLetter C -Force -ErrorAction SilentlyContinue
'Memory Dump Files',
'Old ChkDsk Files',
'Previous Installations',
'Recycle Bin',
'Service Pack Cleanup',
'Setup Log Files',
'System error memory dump files',
'System error minidump files',
'Temporary Files',
'Temporary Setup Files',
'Thumbnail Cache',
'Update Cleanup',
'Upgrade Discarded Files',
'Windows Defender',
'Windows Error Reporting Archive Files',
'Windows Error Reporting Files',
'Windows Error Reporting Queue Files',
'Windows Error Reporting Temp Files',
'Windows ESD installation files',
'Windows Upgrade Log Files'
)
foreach ($cat in $cleanCategories) {
$keyPath = "$cleanKey\$cat"
if (Test-Path $keyPath) {
Set-ItemProperty -Path $keyPath -Name "StateFlags$('{0:D4}' -f $sageset)" -Value 2 -Type DWord -Force -ErrorAction SilentlyContinue
}
}
Write-Host "Running cleanmgr /sagerun:$sageset (may take a few minutes)..."
$proc = Start-Process -FilePath 'cleanmgr.exe' -ArgumentList "/sagerun:$sageset" -Wait -PassThru
Write-Host "cleanmgr exited (code $($proc.ExitCode))."
# 2. Clear Windows Update cache # Clear Windows Update cache
Write-Host "Stopping WU-adjacent services before cache clear..." Write-Host "Stopping WU-adjacent services before cache clear..."
foreach ($svc in 'wuauserv', 'UsoSvc', 'bits', 'dosvc', 'TrustedInstaller') { foreach ($svc in 'wuauserv', 'UsoSvc', 'bits', 'dosvc', 'TrustedInstaller') {
Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue
@@ -451,9 +421,18 @@ Assert-Step 'WinRM' 'Shell MaxMemoryPerShellMB >= 2048' {
Assert-Step 'WinRM' 'Plugin Microsoft.PowerShell MaxMemoryPerShellMB >= 2048' { Assert-Step 'WinRM' 'Plugin Microsoft.PowerShell MaxMemoryPerShellMB >= 2048' {
[int](Get-Item 'WSMan:\localhost\Plugin\Microsoft.PowerShell\Quotas\MaxMemoryPerShellMB').Value -ge 2048 [int](Get-Item 'WSMan:\localhost\Plugin\Microsoft.PowerShell\Quotas\MaxMemoryPerShellMB').Value -ge 2048
} }
if ([int](Get-Item WSMan:\localhost\Shell\MaxProcessesPerShell -ErrorAction SilentlyContinue).Value -lt 100) {
Set-Item WSMan:\localhost\Shell\MaxProcessesPerShell 100 -Force
}
Assert-Step 'WinRM' 'MaxProcessesPerShell >= 100' { Assert-Step 'WinRM' 'MaxProcessesPerShell >= 100' {
[int](Get-Item WSMan:\localhost\Shell\MaxProcessesPerShell).Value -ge 100 [int](Get-Item WSMan:\localhost\Shell\MaxProcessesPerShell).Value -ge 100
} }
$_subsysKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems'
$_subsysCur = (Get-ItemProperty $_subsysKey -Name Windows).Windows
if (-not ($_subsysCur -match 'SharedSection=\d+,\d+,(\d+)') -or [int]$Matches[1] -lt 4096) {
$subsysNew = $_subsysCur -replace 'SharedSection=(\d+),(\d+),\d+', 'SharedSection=$1,$2,4096'
Set-ItemProperty $_subsysKey -Name Windows -Value $subsysNew
}
Assert-Step 'DesktopHeap' 'SharedSection non-interactive >= 4096 KB' { Assert-Step 'DesktopHeap' 'SharedSection non-interactive >= 4096 KB' {
$w = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems' -Name Windows).Windows $w = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems' -Name Windows).Windows
if ($w -match 'SharedSection=\d+,\d+,(\d+)') { [int]$Matches[1] -ge 4096 } else { $false } if ($w -match 'SharedSection=\d+,\d+,(\d+)') { [int]$Matches[1] -ge 4096 } else { $false }
@@ -1601,11 +1580,15 @@ if (Test-Path $vcpkgExe) {
$cloneArgs += '--depth' $cloneArgs += '--depth'
$cloneArgs += '1' $cloneArgs += '1'
} }
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
& $gitBin.Source @cloneArgs 2>&1 | Write-Host & $gitBin.Source @cloneArgs 2>&1 | Write-Host
$ErrorActionPreference = $savedEap
if ($LASTEXITCODE -ne 0) { throw "vcpkg git clone failed (exit $LASTEXITCODE)." } if ($LASTEXITCODE -ne 0) { throw "vcpkg git clone failed (exit $LASTEXITCODE)." }
if ($VcpkgRef -ne '') { if ($VcpkgRef -ne '') {
Write-Host "Checking out vcpkg ref: $VcpkgRef" Write-Host "Checking out vcpkg ref: $VcpkgRef"
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
& $gitBin.Source -C $vcpkgDir checkout $VcpkgRef 2>&1 | Write-Host & $gitBin.Source -C $vcpkgDir checkout $VcpkgRef 2>&1 | Write-Host
$ErrorActionPreference = $savedEap
if ($LASTEXITCODE -ne 0) { throw "vcpkg checkout '$VcpkgRef' failed (exit $LASTEXITCODE)." } if ($LASTEXITCODE -ne 0) { throw "vcpkg checkout '$VcpkgRef' failed (exit $LASTEXITCODE)." }
} }
Write-Host "Bootstrapping vcpkg (-disableMetrics)..." Write-Host "Bootstrapping vcpkg (-disableMetrics)..."
+24 -16
View File
@@ -31,16 +31,24 @@
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$vmtoolsRpc = 'C:\Program Files\VMware\VMware Tools\vmware-rpctool.exe' # vmware-rpctool.exe was removed in recent VMware Tools versions; use vmtoolsd --cmd instead.
if (-not (Test-Path $vmtoolsRpc -PathType Leaf)) { exit 0 } $vmtoolsd = 'C:\Program Files\VMware\VMware Tools\vmtoolsd.exe'
if (-not (Test-Path $vmtoolsd -PathType Leaf)) { exit 0 }
function Read-GuestInfo { function Read-GuestInfo {
param([string]$Key) param([string]$Key)
# vmtoolsd writes to a file handle but not to a PS pipeline — use cmd.exe redirection.
$tmp = [System.IO.Path]::GetTempFileName()
try {
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue' $savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
$out = & $vmtoolsRpc "info-get guestinfo.$Key" 2>&1 cmd /c "`"$vmtoolsd`" --cmd `"info-get guestinfo.$Key`" > `"$tmp`" 2>&1"
$ErrorActionPreference = $savedEap $ErrorActionPreference = $savedEap
if ($LASTEXITCODE -eq 0 -and $out -notmatch 'No value found|^$') { $text = (Get-Content $tmp -ErrorAction SilentlyContinue | Out-String).Trim()
return ($out | Out-String).Trim() if ($text -and $text -notmatch 'No value found|Error') {
return $text
}
} finally {
Remove-Item $tmp -ErrorAction SilentlyContinue
} }
return '' return ''
} }
@@ -75,22 +83,22 @@ if (-not $adapter) {
exit 1 exit 1
} }
# Disable DHCP and remove any existing IPv4 addresses. # Disable DHCP and remove any existing IPv4 addresses and routes.
$adapter | Set-NetIPInterface -Dhcp Disabled -ErrorAction SilentlyContinue $adapter | Set-NetIPInterface -Dhcp Disabled -ErrorAction SilentlyContinue
Get-NetRoute -InterfaceAlias $adapter.Name -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Remove-NetRoute -Confirm:$false -ErrorAction SilentlyContinue
$adapter | Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | $adapter | Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue
# Apply static IP. # Apply static IP (no DefaultGateway here — add route separately to avoid conflicts).
$ipParams = @{ New-NetIPAddress -InterfaceAlias $adapter.Name -IPAddress $ip `
InterfaceAlias = $adapter.Name -PrefixLength $prefix -ErrorAction Stop | Out-Null
IPAddress = $ip
PrefixLength = $prefix # Add default route if gateway supplied.
ErrorAction = 'Stop'
}
if ($gateway -match '^\d{1,3}(\.\d{1,3}){3}$') { if ($gateway -match '^\d{1,3}(\.\d{1,3}){3}$') {
$ipParams['DefaultGateway'] = $gateway New-NetRoute -InterfaceAlias $adapter.Name -DestinationPrefix '0.0.0.0/0' `
-NextHop $gateway -ErrorAction SilentlyContinue | Out-Null
} }
New-NetIPAddress @ipParams | Out-Null
# Use the gateway as DNS (fallback: Google DNS). # Use the gateway as DNS (fallback: Google DNS).
$dns = @() $dns = @()
@@ -101,7 +109,7 @@ Set-DnsClientServerAddress -InterfaceAlias $adapter.Name `
# Report IP back via guestinfo.ci-ip so the host also has it. # Report IP back via guestinfo.ci-ip so the host also has it.
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue' $savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
& $vmtoolsRpc "info-set guestinfo.ci-ip $ip" 2>&1 | Out-Null & $vmtoolsd --cmd "info-set guestinfo.ci-ip $ip" 2>&1 | Out-Null
$ErrorActionPreference = $savedEap $ErrorActionPreference = $savedEap
exit 0 exit 0