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
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:
"""Poll *transport*.is_ready() until True or *timeout* seconds elapse."""
deadline = time.monotonic() + timeout
@@ -532,8 +539,9 @@ def template_prepare_win(
try:
result = _run_with_heartbeat(transport, invoke, check=False)
exit_code = _parse_exit_marker(result.stdout)
if result.stdout.strip():
click.echo(result.stdout.strip())
display = _strip_exit_marker(result.stdout).strip()
if display:
click.echo(display)
if result.stderr.strip():
click.echo(result.stderr.strip(), err=True)
except TransportError:
@@ -658,8 +666,9 @@ Register-ScheduledTask -TaskName 'CI-StaticIp' `
)
cleanup_result = _run_with_heartbeat(transport, cleanup_invoke, check=False)
cleanup_ec = _parse_exit_marker(cleanup_result.stdout)
if cleanup_result.stdout.strip():
click.echo(cleanup_result.stdout.strip())
display = _strip_exit_marker(cleanup_result.stdout).strip()
if display:
click.echo(display)
if cleanup_result.stderr.strip():
click.echo(cleanup_result.stderr.strip(), err=True)
if cleanup_ec != 0:
+22 -37
View File
@@ -239,43 +239,15 @@ function Assert-Hash {
function Invoke-DiskCleanup {
Write-Step "Cleaning up disk"
$cleanKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches'
$sageset = 1
$cleanCategories = @(
'Active Setup Temp Folders',
'BranchCache',
'Downloaded Program Files',
'Internet Cache Files',
'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))."
# cleanmgr.exe requires an interactive window station — hangs in WinRM Session 0.
# Replicate the relevant categories with direct PowerShell cleanup instead.
Write-Host "Clearing memory dumps..."
Remove-Item 'C:\Windows\Minidump\*' -Force -Recurse -ErrorAction SilentlyContinue
Remove-Item 'C:\Windows\memory.dmp' -Force -ErrorAction SilentlyContinue
Write-Host "Clearing Windows Error Reporting files..."
Remove-Item 'C:\ProgramData\Microsoft\Windows\WER\*' -Force -Recurse -ErrorAction SilentlyContinue
Write-Host "Clearing Recycle Bin..."
Clear-RecycleBin -DriveLetter C -Force -ErrorAction SilentlyContinue
Write-Host "Stopping WU-adjacent services before cache clear..."
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' {
[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' {
[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' {
$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 }
@@ -1587,11 +1568,15 @@ if (Test-Path $vcpkgExe) {
$cloneArgs += '--depth'
$cloneArgs += '1'
}
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
& $gitBin.Source @cloneArgs 2>&1 | Write-Host
$ErrorActionPreference = $savedEap
if ($LASTEXITCODE -ne 0) { throw "vcpkg git clone failed (exit $LASTEXITCODE)." }
if ($VcpkgRef -ne '') {
Write-Host "Checking out vcpkg ref: $VcpkgRef"
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
& $gitBin.Source -C $vcpkgDir checkout $VcpkgRef 2>&1 | Write-Host
$ErrorActionPreference = $savedEap
if ($LASTEXITCODE -ne 0) { throw "vcpkg checkout '$VcpkgRef' failed (exit $LASTEXITCODE)." }
}
Write-Host "Bootstrapping vcpkg (-disableMetrics)..."
+23 -40
View File
@@ -239,47 +239,17 @@ function Assert-Hash {
function Invoke-DiskCleanup {
Write-Step "Cleaning up disk"
# 1. Windows Disk Cleanup (cleanmgr) — run with /sagerun:1 using preset flags
# First register the flags via /sageset:1 in the registry (silent, no UI)
$cleanKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches'
$sageset = 1
$cleanCategories = @(
'Active Setup Temp Folders',
'BranchCache',
'Downloaded Program Files',
'Internet Cache Files',
'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))."
# cleanmgr.exe requires an interactive window station — hangs in WinRM Session 0.
# Replicate the relevant categories with direct PowerShell cleanup instead.
Write-Host "Clearing memory dumps..."
Remove-Item 'C:\Windows\Minidump\*' -Force -Recurse -ErrorAction SilentlyContinue
Remove-Item 'C:\Windows\memory.dmp' -Force -ErrorAction SilentlyContinue
Write-Host "Clearing Windows Error Reporting files..."
Remove-Item 'C:\ProgramData\Microsoft\Windows\WER\*' -Force -Recurse -ErrorAction SilentlyContinue
Write-Host "Clearing Recycle Bin..."
Clear-RecycleBin -DriveLetter C -Force -ErrorAction SilentlyContinue
# 2. Clear Windows Update cache
# Clear Windows Update cache
Write-Host "Stopping WU-adjacent services before cache clear..."
foreach ($svc in 'wuauserv', 'UsoSvc', 'bits', 'dosvc', 'TrustedInstaller') {
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' {
[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' {
[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' {
$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 }
@@ -1601,11 +1580,15 @@ if (Test-Path $vcpkgExe) {
$cloneArgs += '--depth'
$cloneArgs += '1'
}
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
& $gitBin.Source @cloneArgs 2>&1 | Write-Host
$ErrorActionPreference = $savedEap
if ($LASTEXITCODE -ne 0) { throw "vcpkg git clone failed (exit $LASTEXITCODE)." }
if ($VcpkgRef -ne '') {
Write-Host "Checking out vcpkg ref: $VcpkgRef"
$savedEap = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
& $gitBin.Source -C $vcpkgDir checkout $VcpkgRef 2>&1 | Write-Host
$ErrorActionPreference = $savedEap
if ($LASTEXITCODE -ne 0) { throw "vcpkg checkout '$VcpkgRef' failed (exit $LASTEXITCODE)." }
}
Write-Host "Bootstrapping vcpkg (-disableMetrics)..."
+24 -16
View File
@@ -31,16 +31,24 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$vmtoolsRpc = 'C:\Program Files\VMware\VMware Tools\vmware-rpctool.exe'
if (-not (Test-Path $vmtoolsRpc -PathType Leaf)) { exit 0 }
# vmware-rpctool.exe was removed in recent VMware Tools versions; use vmtoolsd --cmd instead.
$vmtoolsd = 'C:\Program Files\VMware\VMware Tools\vmtoolsd.exe'
if (-not (Test-Path $vmtoolsd -PathType Leaf)) { exit 0 }
function Read-GuestInfo {
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'
$out = & $vmtoolsRpc "info-get guestinfo.$Key" 2>&1
cmd /c "`"$vmtoolsd`" --cmd `"info-get guestinfo.$Key`" > `"$tmp`" 2>&1"
$ErrorActionPreference = $savedEap
if ($LASTEXITCODE -eq 0 -and $out -notmatch 'No value found|^$') {
return ($out | Out-String).Trim()
$text = (Get-Content $tmp -ErrorAction SilentlyContinue | Out-String).Trim()
if ($text -and $text -notmatch 'No value found|Error') {
return $text
}
} finally {
Remove-Item $tmp -ErrorAction SilentlyContinue
}
return ''
}
@@ -75,22 +83,22 @@ if (-not $adapter) {
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
Get-NetRoute -InterfaceAlias $adapter.Name -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Remove-NetRoute -Confirm:$false -ErrorAction SilentlyContinue
$adapter | Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue
# Apply static IP.
$ipParams = @{
InterfaceAlias = $adapter.Name
IPAddress = $ip
PrefixLength = $prefix
ErrorAction = 'Stop'
}
# Apply static IP (no DefaultGateway here — add route separately to avoid conflicts).
New-NetIPAddress -InterfaceAlias $adapter.Name -IPAddress $ip `
-PrefixLength $prefix -ErrorAction Stop | Out-Null
# Add default route if gateway supplied.
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).
$dns = @()
@@ -101,7 +109,7 @@ Set-DnsClientServerAddress -InterfaceAlias $adapter.Name `
# Report IP back via guestinfo.ci-ip so the host also has it.
$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
exit 0