Parallel MSBuild fan-out (e.g. ns7zip x86/x64/x86-ansi) over pypsrp
Linux→Win fails with MSBuild MSB6003 / Win32 1816 'Not enough quota'
because two default guest limits are too tight for concurrent native
processes via WinRM:
- Plugin Microsoft.PowerShell MaxMemoryPerShellMB = 150 MB
(pypsrp uses the plugin endpoint, not the Shell endpoint that
Deploy was previously bumping)
- Non-interactive desktop heap SharedSection 3rd field = 768 KB
(Session 0 csrss; CreateProcess fails when 3+ cl.exe spawn)
Pre-migration Win→Win went through Invoke-Command on PSSession with
quotas inherited from the local process, so the limits never bit.
Changes:
- Deploy-WinBuild2025.ps1 / Deploy-WinBuild2022.ps1: also set the
plugin-level MaxMemoryPerShellMB=2048, bump MaxProcessesPerShell
25→100, patch SharedSection 3rd field 768→4096 (reboot needed for
csrss to pick up, applied by post-install shutdown).
- Install-CIToolchain-WinBuild2025/2022.ps1: add Assert-Step for
plugin quota, MaxProcessesPerShell≥100, SharedSection≥4096; fail
hard if any are under threshold.
- docs/WINDOWS-TEMPLATE-SETUP.md: new "WinRM quotas e desktop heap"
section explaining the diagnosis + one-shot patch for legacy
templates; updated Step 3 validation row + troubleshooting entry.
- AGENTS.md: error #13 with symptom, root cause, and pointer to docs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
24 KiB
Windows Template VM Setup
Procedura di provisioning della template VM Windows Server 2025 che alimenta i linked
clone usati per ogni job CI. Il template viene creato una volta; lo snapshot
BaseClean è la base immutabile per tutte le build successive.
Prerequisito: aver eseguito HOST-SETUP.md (F:\CI\ esiste,
credenziali archiviate, ISO Windows Server presente).
Architettura: tre script
| Script | Dove gira | Scopo |
|---|---|---|
template/Deploy-WinBuild2025.ps1 |
Host | Opzionale: crea VM e installa Windows Server 2025 unattended da ISO |
template/Prepare-WinBuild2025.ps1 |
Host | Orchestratore: copia + esegue Setup nel guest via WinRM (2025) |
template/Install-CIToolchain-WinBuild2025.ps1 |
Dentro VM | CI toolchain (user, .NET, Python, VS) + validazione hardening Deploy |
template/Deploy-WinBuild2022.ps1 |
Host | Opzionale: crea VM e installa Windows Server 2022 unattended da ISO |
template/Prepare-WinBuild2022.ps1 |
Host | Orchestratore: copia + esegue Setup nel guest via WinRM (2022) |
template/Install-CIToolchain-WinBuild2022.ps1 |
Dentro VM | CI toolchain per Windows Server 2022 (stesso flusso del 2025) |
template/Validate-DeployState.ps1 |
Host | Valida stato post-Deploy prima di eseguire Prepare (2025 e 2022) |
template/Validate-SetupState.ps1 |
Host | Valida stato post-Setup (Deploy + Setup) prima dello snapshot |
Confine Deploy / Setup (refactor §7, 2026-05-10):
Deploy-WinBuild2025.ps1si occupa dell'OS hardening (Firewall, WinRM, UAC, Defender, UX tweaks, autologin Administrator, WU GPO locks). Se usato, Setup valida questi come Assert-Step senza re-applicarli.Install-CIToolchain-WinBuild2025.ps1si occupa della CI customization (utenteci_build, cartelleC:\CI, toolchain .NET/Python/VS, Windows Update lifecycle, cleanup, final gate).
Prepare-WinBuild2025.ps1 apre una WinRM session, copia Install-CIToolchain-WinBuild2025.ps1 in
C:\CI\, lo esegue, raccoglie l'exit code, valida lo stato del guest.
Specifiche VM
| Parametro | Valore |
|---|---|
| OS | Windows Server 2025 Standard/Datacenter (Core o Desktop OK) |
| vCPU | 4 |
| RAM | 6144 MB (6 GB) |
| Disco | 80 GB thin provisioned |
| NIC | VMnet8 (NAT) — internet richiesto per WU + installer |
| VMX path | F:\CI\Templates\WinBuild2025\WinBuild2025.vmx |
| ISO | Windows Server 2025 (da F:\CI\ISO\) |
Fase A — Crea la VM e installa Windows
A.1 Crea VM in VMware Workstation
- File → New Virtual Machine → Custom
- Hardware: 4 vCPU, 6144 MB, 80 GB thin
- Network: VMnet8 (NAT)
- VMX:
F:\CI\Templates\WinBuild2025\WinBuild2025.vmx - CD/DVD: monta ISO Windows Server 2025
A.2 Installa Windows Server 2025
- Edition: Standard o Datacenter (Server Core o con Desktop)
- Activation: vedi A.4 (deve essere fatto prima dello snapshot)
A.3 Abilita WinRM dentro la VM
Dentro la VM, come Administrator:
winrm quickconfig -q
Enable-PSRemoting -Force -SkipNetworkProfileCheck
Set-Item WSMan:\localhost\Service\Auth\Basic $true -Force
# AllowUnencrypted left false — Deploy post-install.ps1 creates HTTPS/5986 listener
# Basic auth is secure over HTTPS; host connects on port 5986 with -SkipCACheck
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force
A.4 Attiva Windows Server 2025
IMPORTANTE: deve essere fatto prima dello snapshot. I linked clone ereditano l'attivazione.
slmgr /dli
Se non attivato, scegli il metodo:
- KMS (server in rete):
slmgr /skms <IP_KMS>→slmgr /ato - MAK/Retail key:
slmgr /ipk XXXXX-XXXXX-XXXXX-XXXXX-XXXXX→slmgr /ato - KMS pubblico (lab): vedi https://github.com/robin113x/Windows-Server-Activation/
Verifica: slmgr /xpr — deve mostrare attivazione permanente o scadenza lontana.
A.5 Annota IP DHCP
Dentro la VM:
ipconfig
Annota l'IP 192.168.79.x assegnato dal DHCP di VMnet8.
Fase B — Provisioning automatico dall'host
Dall'host (PowerShell elevato):
cd N:\Code\Workspace\Local-CI-CD-System\template
# Provisioning completo + registrazione credenziali automatica:
.\Prepare-WinBuild2025.ps1 -VMIPAddress 192.168.79.128 -StoreCredential
# Se WU già applicato in precedenza:
.\Prepare-WinBuild2025.ps1 -VMIPAddress 192.168.79.128 -SkipWindowsUpdate -StoreCredential
Cosa fa Prepare-WinBuild2025.ps1 — passo per passo
Ogni step ha validazione Assert-Step (throw on failure, [OK] su pass):
- Pre-flight: script dir presente,
Install-CIToolchain-WinBuild2025.ps1presente, IP ottetti 0-255, TCP/5986 reachable - Configura host WinRM client: aggiunge
$VMIPAddressaiTrustedHosts(noAllowUnencrypted— HTTPS non lo richiede)- Salva valore precedente, ripristinato nel
finally(host posture invariata)
- Salva valore precedente, ripristinato nel
- Validazione host WinRM:
TrustedHostsinclude IP - Test connettività:
Test-WSMan -Port 5986 -UseSSL -Authentication Basiccon credenziali admin - Apre PSSession
-UseSSL -Port 5986verso la VM - Crea
C:\CIin guest, valida creazione - Copia
Install-CIToolchain-WinBuild2025.ps1inC:\CI\Install-CIToolchain-WinBuild2025.ps1, valida size match - Esegue
Install-CIToolchain-WinBuild2025.ps1dentro la VM (vedi Fase C) - Gestisce reboot 3010: se WU richiede reboot, riavvia VM e suggerisce re-run con
-SkipWindowsUpdate - Post-setup remote validation (9 check via singola
Invoke-Command):- WinRM Running
- User
$BuildUsernameexists + admin - UAC
EnableLUA=0 - Firewall tutti profili Off
- .NET SDK installato
- Python
C:\Python\python.exepresente - MSBuild path presente
C:\CI\{build,output,scripts}presenti
- Restore host WinRM nel
finally(sempre eseguito, anche su failure)
Parametri principali
| Parametro | Default | Note |
|---|---|---|
-VMIPAddress |
(mandatory) | IP DHCP della VM su VMnet8 |
-AdminUsername |
Administrator | Account admin built-in nella VM |
-AdminPassword |
(prompt) | Se vuoto, prompt SecureString |
-BuildPassword |
(prompt) | Password per ci_build — mai hard-codare |
-BuildUsername |
ci_build | Account CI dentro la VM |
-DotNetSdkVersion |
10.0 | Channel .NET SDK |
-SkipWindowsUpdate |
(off) | Pass-through a Install-CIToolchain-WinBuild2025.ps1 |
-StoreCredential |
(off) | Registra BuildUsername/BuildPassword in Windows Credential Manager |
-CredentialTarget |
BuildVMGuest | Target Credential Manager (deve corrispondere a quanto usato da Invoke-CIJob) |
Fase C — Cosa fa Install-CIToolchain-WinBuild2025.ps1 (dentro la VM)
Eseguito automaticamente da Prepare in Fase B. Steps 1/2/3/4b/5b/5c sono
validation-only — verificano che Deploy abbia già applicato l'hardening OS.
Steps 4/5/6/6b/7-10 eseguono la CI customization effettiva.
Ogni step ha validazione Assert-Step (throw on failure).
Step 1 Firewall: validation only — Deploy disabled all profiles [Assert-Step]
Step 2 Defender: validation only — Deploy set DisableAntiSpyware GPO + RTP off [Assert-Step]
Step 3 WinRM: validation only — Deploy set Basic/Unencrypted/MaxMem/Timeout [Assert-Step]
Step 4 Build user account (ci_build, admin, no expire) ── crea utente CI
Step 4b UAC: validation only — Deploy set EnableLUA=0 + TokenFilterPolicy [Assert-Step]
Step 5 CI dirs: C:\CI\{build, output, scripts} ── C:\CI deve esistere prima di WU
Step 5b Explorer LaunchTo=1: validation only — Deploy set HKCU + Default hive [Assert-Step]
Step 5c CI UX hardening: validation only — Deploy set lock screen, SM policy/task,
DisableCAD, OOBE, telemetry, autologin Administrator [Assert-Step]
Step 6 Windows Update via Scheduled Task SYSTEM ── clears Deploy GPO locks, runs WU
Step 6b Windows Update permanently disabled (post-update lockdown)
Step 7 .NET SDK install (channel via dotnet-install.ps1)
Step 8 Python install (3.13.3, all-users, C:\Python)
Step 9 VS Build Tools 2026 via Scheduled Task SYSTEM
Step 10 Toolchain cross-check (dotnet, python, msbuild)
Cleanup cleanmgr /sagerun:1 + DISM /StartComponentCleanup /ResetBase
Final Pre-snapshot gate — 7 check cross-cutting + no installer leftover
Razionale dell'ordine
- Steps 1-3 (validazioni) — verificano prima le precondizioni Deploy (firewall off, Defender off, WinRM pronto) in modo che i failure emergano subito, non a metà toolchain.
- User + UAC (4/4b) — crea
ci_build; valida UAC off (set da Deploy); UAC off prima di dir/tool evita prompt di elevazione negli step 7-9. - CI dirs prima di WU (Step 5) — il worker WU scrive file temp in
C:\CI\(wu_worker.ps1,wu_result.txt, ecc.); la directory deve esistere prima. - UI tweaks (5b/5c) — validazioni registro rapide; raggruppate prima di WU (lento).
- WU (Step 6) — clears i GPO lock di Deploy (Step A), avvia COM API via SYSTEM task, poi Step 6b re-disabilita tutto → snapshot cattura WU permanentemente off.
- Toolchain (7-9) last — WU completato, nessun scan su payload installer.
Validazioni per step (sintesi)
| Step | Natura | Check chiave |
|---|---|---|
| 1 | Assert | Get-NetFirewallProfile -Profile Domain/Private/Public → Enabled=False |
| 2 | Assert | GPO DisableAntiSpyware=1 (set da Deploy) |
| 3 | Assert | WinRM Running+Automatic, AllowUnencrypted=false (HTTPS-only), Auth/Basic=true, Shell+Plugin MaxMem≥2048, MaxProcesses≥100, desktop heap SharedSection 3rd≥4096 |
| 4 | Operativo | User exists, enabled, PasswordNeverExpires, member of Administrators |
| 4b | Assert | EnableLUA=0, LocalAccountTokenFilterPolicy=1 |
| 5 | Operativo | Ogni dir Test-Path -PathType Container |
| 5b | Assert | HKCU LaunchTo=1 |
| 5c | Assert+Op | NoLockScreen=1, SM policy DoNotOpenAtLogon=1, SM task Disabled, DisableCAD=1, OOBE DisablePrivacyExperience=1, AllowTelemetry=0; AutoAdminLogon=1+DefaultUserName=Administrator re-affermati se assenti (Windows OOBE può resettarli al primo boot) |
| 6 | Operativo | ResultCode ∈ {0,2,3}, no reboot required (else exit 3010) |
| 6b | Operativo | wuauserv StartType=Disabled, NoAutoUpdate=1, DisableWindowsUpdateAccess=1 |
| 7 | Operativo | dotnet --version channel match, machine PATH contiene C:\dotnet |
| 8 | Operativo | C:\Python\python.exe esiste, version match esatto, PATH contiene C:\Python |
| 9 | Operativo | MSBuild.exe esiste, esegue, PATH, \VC\ dir presente |
| 10 | Operativo | dotnet+python+msbuild tutti risolvibili (throw) |
| Final | Cross-cut | WinRM, firewall, user+admin, UAC, WU disabled, tutti tool, no leftover installer in C:\CI |
Fase D — Snapshot
Solo se Prepare-WinBuild2025.ps1 exit 0 (tutti gli Assert-Step passati):
- Spegni la VM: Start → Shut down (graceful, non hard-stop)
- Take snapshot in VMware Workstation:
- VM → Snapshot → Take Snapshot
- Nome esatto:
BaseClean(case-sensitive, usato daNew-BuildVM.ps1)
- Lascia la VM spenta — non modificarla mai più dopo lo snapshot
- Verifica
runner/config.yaml:GITEA_CI_TEMPLATE_PATH=F:\CI\Templates\WinBuild2025\WinBuild2025.vmx
Validation scripts standalone
Due script per validare il guest in punti specifici del flusso, indipendentemente da Prepare. Utili per debug, re-verifica dopo modifiche manuali, e CI automatizzata.
Dopo Deploy — prima di Prepare
.\template\Validate-DeployState.ps1 -VMIPAddress 192.168.79.129
Controlla: Firewall off, Defender GPO, WinRM (Running/Automatic/AllowUnencrypted/Basic/MaxMem), UAC off, Explorer LaunchTo, NoLockScreen, Server Manager (policy+HKLM+task+HKCU), DisableCAD, OOBE, AllowTelemetry, AutoAdminLogon=1, WU GPO, wuauserv/UsoSvc=Manual.
Dopo Prepare — prima di snapshot
.\template\Validate-SetupState.ps1 -VMIPAddress 192.168.79.129
Tutti i check Deploy + ci_build (exists/enabled/admin/PasswordNeverExpires),
C:\CI\{build,output,scripts}, wuauserv/UsoSvc=Disabled, WU GPO,
.NET SDK channel match, Python version match, MSBuild present, no leftover installer files.
Exit 0 = tutto OK. Exit 1 = almeno un check fallito (output indica quale).
Fase E — Verifica finale
Dall'host:
# Test creazione clone manuale
cd N:\Code\Workspace\Local-CI-CD-System\scripts
.\New-BuildVM.ps1 -JobId 'smoke-test'
# Verifica WinRM raggiungibile sul clone
.\Wait-VMReady.ps1 -JobId 'smoke-test' -TimeoutSec 120
# Cleanup
.\Remove-BuildVM.ps1 -JobId 'smoke-test'
Se i 3 step exit 0 → template pronto per produzione.
Maintenance — refresh semestrale
Il template va rinfrescato ogni ~180 giorni (KMS lease) o quando serve aggiornare la toolchain (.NET, VS Build Tools, Python).
Procedura:
- Power on della template VM (NIC: VMnet8 NAT)
- Riattiva:
slmgr /ato - Re-run
Prepare-WinBuild2025.ps1per applicare update toolchain (passare flag opportuni) - Shut down + nuovo snapshot versionato:
BaseClean_<yyyyMMdd>(TODO §2.5) - Aggiorna
GITEA_CI_SNAPSHOT_NAMEenv var inrunner/config.yaml - Tieni i 2 snapshot più recenti, cancella vecchi dopo 1 settimana di uso pulito
WinRM quotas e desktop heap (build paralleli)
I template Windows escono di default da Windows con quote dimensionate per uso
interattivo. Da Linux/pypsrp queste quote si rivelano strette appena il build
guest fa fan-out di processi nativi (compilatori, linker) in parallelo. Tre
configurazioni che il template deve applicare prima dello snapshot
BaseClean (gestite automaticamente da Deploy-WinBuild2025.ps1 / 2022):
| Setting | Default | Valore template | Motivo |
|---|---|---|---|
WSMan:\localhost\Shell\MaxMemoryPerShellMB |
150 | 2048 | Limite globale memoria per shell WinRM (winrs) |
WSMan:\localhost\Plugin\Microsoft.PowerShell\Quotas\MaxMemoryPerShellMB |
150 | 2048 | pypsrp da Linux usa l'endpoint plugin Microsoft.PowerShell, non lo Shell. Senza questo, builds paralleli falliscono con MSB6003 / Not enough quota anche se Shell è a 2048 |
WSMan:\localhost\Shell\MaxProcessesPerShell |
25 | 100 | MSBuild + cl + link + mt + altri tool si moltiplicano in fan-out parallelo |
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems\Windows SharedSection |
…,768 |
…,4096 |
Heap del desktop non-interattivo (Session 0). CreateProcess fallisce con ERROR_NOT_ENOUGH_QUOTA (1816) quando 3+ cl.exe partono insieme. Richiede reboot per essere letto da csrss.exe. |
Importante: la modifica SharedSection è una scrittura di registry che il
kernel legge solo al boot (csrss.exe). Lo shutdown /s finale di
post-install.ps1 fa sì che il prossimo avvio del template (per ulteriore
manutenzione, o l'invocazione di Install-CIToolchain) parta con il valore
corretto. Il BaseClean snapshot va preso dopo un boot completo per essere
certi che il valore sia attivo.
Perché pre-migrazione (Win host) funzionava: il path Windows→Windows usava
Invoke-Command su PSSession con la stessa identità del processo locale (act_runner
come LocalSystem), ereditando spesso quote più generose tramite GPO e default
del client locale. Il path Linux→Windows (pypsrp) attinge ai default puri
dell'endpoint plugin Microsoft.PowerShell del guest — se il template non li ha
bumpati, sono 150 MB / 25 processi / 768 KB heap.
Validazione: Install-CIToolchain-WinBuild2025.ps1 Step 3 controlla tutti e
quattro i valori e fallisce hard se anche solo uno è sotto soglia. Per
template già esistenti, applicare il blocco PowerShell sotto, reboot, shutdown,
nuovo snapshot BaseClean:
# Una tantum su template legacy
Set-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB 2048 -Force
Set-Item 'WSMan:\localhost\Plugin\Microsoft.PowerShell\Quotas\MaxMemoryPerShellMB' 2048 -Force
Set-Item WSMan:\localhost\Shell\MaxProcessesPerShell 100 -Force
$k = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems'
$cur = (Get-ItemProperty -Path $k -Name Windows).Windows
$new = $cur -replace 'SharedSection=(\d+),(\d+),\d+', 'SharedSection=$1,$2,4096'
Set-ItemProperty -Path $k -Name Windows -Value $new
Restart-Service WinRM -Force
Restart-Computer -Force # necessario per il desktop heap
Troubleshooting
| Sintomo | Diagnosi / Fix |
|---|---|
Cannot reach $VMIPAddress via WinRM |
VM non avviata, WinRM non abilitato, IP errato — verifica con ipconfig |
Install-CIToolchain-WinBuild2025.ps1 exited with code 3010 |
Reboot richiesto post-WU. Re-run con -SkipWindowsUpdate |
[Defender] Validation failed: GPO key |
Defender riattivato da Windows Update — disabilita via gpedit + reboot |
VS Build Tools installation timed out |
Network slow o cache corrotta. Re-run con cache pulita (C:\ProgramData\Microsoft\VisualStudio\Packages) |
Python version match fail |
$PythonVersion ≠ versione installata. Aggiorna param o reinstalla |
Post-setup MSBuild not found |
VS install fallita silenziosa. Check C:\CI\vs_log.txt |
| Pre-snapshot Final check fail | Stato VM rotto — non prendere snapshot. Re-run o ricomincia da zero |
MSBuild error MSB6003: CL.exe could not be run + Win32Exception (0x80004005): Not enough quota su build paralleli |
Quote WinRM/desktop-heap non bumpate nel template (vedi §"WinRM quotas e desktop heap" sotto). Tipico quando il build interno fa fan-out (es. 3× cl.exe paralleli) via pypsrp Linux→Win |
Backlog correlato (TODO.md)
- §1.1 [P0] Migrare WinRM da HTTP/Basic a HTTPS/5986 con cert self-signed nel template
- §1.3 [P0] Pinning hash SHA256 degli installer (Python, dotnet-install.ps1, vs_buildtools.exe)
- §1.6 [P2] Documentare threat model per Defender/Firewall/UAC tutti off
- §2.5 [P1] Snapshot versionato
BaseClean_<yyyyMMdd>(rollback in caso di refresh rotto) - §2.6 [P2] Backup automatico VMDK template pre-snapshot
- In-VM Git Clone — installare Git for Windows + 7-Zip nel template per ottimizzare flusso