Files
SilverMetal/windows/collector/Collector.ps1
sysadmin fce4b77bd6
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 4m8s
fix(collector): launch via Setup\CmdLine (was bypassed) + WinPE diagnostics
The boot.wim Setup\CmdLine override (legacy-Setup forcing) is authoritative over
winpeshl.ini, so it launched setup.exe directly and the collector never ran -- the
VM went straight to the old sm-bootstrap unattended install. Repoint Setup\CmdLine
at the collector (cmd /c X:\sm\Start-Collector.cmd); the collector still launches the
legacy X:\sources\setup.exe itself. Add wpeinit + an on-screen banner, and write any
collector/WinForms-load failure to X:\sm\collector-error.txt shown on the console
before falling back, so we can diagnose WinForms-in-WinPE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:14:08 +01:00

259 lines
11 KiB
PowerShell

#Requires -Version 5.1
# SilverMetal WinPE pre-config collector (WinForms UI). Runs in WinPE under the
# ADK WinPE-NetFx + WinPE-PowerShell optional components. ASCII body only (WinPE
# PowerShell 5.1 mis-parses smart quotes / em-dashes). On Finish it generates the
# answer file and launches legacy Setup; on Cancel / error it exits 1 so the
# wrapping Start-Collector.cmd falls back to the default autounattend.xml.
try {
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
# Validation helpers + answer-file generator live next to this script under X:\sm\.
. (Join-Path $PSScriptRoot 'Test-SmInput.ps1')
. (Join-Path $PSScriptRoot 'New-SmAnswerFile.ps1')
# --- palette ---------------------------------------------------------------
$colBg = [Drawing.Color]::FromArgb(18, 18, 22)
$colPanel = [Drawing.Color]::FromArgb(28, 28, 34)
$colText = [Drawing.Color]::FromArgb(232, 232, 238)
$colMuted = [Drawing.Color]::FromArgb(150, 150, 162)
$colAccent = [Drawing.Color]::FromArgb(96, 200, 170)
$colError = [Drawing.Color]::FromArgb(236, 110, 110)
$colField = [Drawing.Color]::FromArgb(40, 40, 48)
$fontBase = New-Object Drawing.Font('Segoe UI', 11)
$fontTitle = New-Object Drawing.Font('Segoe UI Semibold', 26, [Drawing.FontStyle]::Bold)
$fontLabel = New-Object Drawing.Font('Segoe UI', 10)
# --- form ------------------------------------------------------------------
$form = New-Object Windows.Forms.Form
$form.Text = 'SilverMetal Setup'
$form.WindowState = 'Maximized'
$form.FormBorderStyle = 'None'
$form.BackColor = $colBg
$form.ForeColor = $colText
$form.Font = $fontBase
$form.KeyPreview = $true
# Helpers to build consistent controls.
function New-Label([string]$text, [int]$x, [int]$y, [int]$w = 220) {
$l = New-Object Windows.Forms.Label
$l.Text = $text; $l.AutoSize = $false
$l.Location = New-Object Drawing.Point($x, $y)
$l.Size = New-Object Drawing.Size($w, 24)
$l.ForeColor = $colMuted; $l.Font = $fontLabel
$form.Controls.Add($l); $l
}
function New-Field([int]$x, [int]$y, [int]$w = 360, [bool]$mask = $false) {
$t = New-Object Windows.Forms.TextBox
$t.Location = New-Object Drawing.Point($x, $y)
$t.Size = New-Object Drawing.Size($w, 28)
$t.BackColor = $colField; $t.ForeColor = $colText
$t.BorderStyle = 'FixedSingle'; $t.Font = $fontBase
if ($mask) { $t.UseSystemPasswordChar = $true }
$form.Controls.Add($t); $t
}
# Title + subtitle (left rail).
$title = New-Object Windows.Forms.Label
$title.Text = 'SilverMetal'
$title.Font = $fontTitle; $title.ForeColor = $colAccent
$title.AutoSize = $true
$title.Location = New-Object Drawing.Point(80, 70)
$form.Controls.Add($title)
$subtitle = New-Object Windows.Forms.Label
$subtitle.Text = 'Pre-install configuration. Set your account, machine and security options before Windows installs.'
$subtitle.Font = $fontLabel; $subtitle.ForeColor = $colMuted
$subtitle.AutoSize = $false
$subtitle.Location = New-Object Drawing.Point(82, 120)
$subtitle.Size = New-Object Drawing.Size(640, 24)
$form.Controls.Add($subtitle)
# --- left column fields ----------------------------------------------------
$colX = 80; $fldX = 80; $y = 180; $rowH = 64
New-Label 'Display name' $colX $y | Out-Null
$txtDisplay = New-Field $fldX ($y + 26)
$y += $rowH
New-Label 'Username' $colX $y | Out-Null
$txtUser = New-Field $fldX ($y + 26)
$y += $rowH
New-Label 'Password' $colX $y | Out-Null
$txtPass = New-Field $fldX ($y + 26) 360 $true
$y += $rowH
New-Label 'Confirm password' $colX $y | Out-Null
$txtPassC = New-Field $fldX ($y + 26) 360 $true
$y += $rowH
New-Label 'Computer name' $colX $y | Out-Null
$txtComputer = New-Field $fldX ($y + 26)
$y += $rowH
# --- right column: flavour + BitLocker -------------------------------------
$rX = 540; $rY = 180
$grpFlavour = New-Object Windows.Forms.GroupBox
$grpFlavour.Text = 'Flavour'
$grpFlavour.ForeColor = $colMuted
$grpFlavour.Location = New-Object Drawing.Point($rX, $rY)
$grpFlavour.Size = New-Object Drawing.Size(360, 220)
$form.Controls.Add($grpFlavour)
# 'essentials' is the always-on baseline role (no flavour manifest), not a selectable
# flavour -- the four below are the real flavours; essentials apps ship with every one.
$flavours = @('developer', 'journalist', 'daily-driver', 'privacy-max')
$radioFlavours = @()
$fy = 32
foreach ($f in $flavours) {
$rb = New-Object Windows.Forms.RadioButton
$rb.Text = $f; $rb.ForeColor = $colText; $rb.Font = $fontBase
$rb.Location = New-Object Drawing.Point(20, $fy)
$rb.Size = New-Object Drawing.Size(320, 28)
$rb.Tag = $f
if ($f -eq 'daily-driver') { $rb.Checked = $true }
$grpFlavour.Controls.Add($rb)
$radioFlavours += $rb
$fy += 36
}
$chkBitLocker = New-Object Windows.Forms.CheckBox
$chkBitLocker.Text = 'Enable BitLocker (TPM + PIN)'
$chkBitLocker.ForeColor = $colText; $chkBitLocker.Font = $fontBase
$chkBitLocker.AutoSize = $true
$chkBitLocker.Location = New-Object Drawing.Point($rX, ($rY + 240))
$form.Controls.Add($chkBitLocker)
$lblPin = New-Label 'BitLocker PIN' $rX ($rY + 280) | Out-Null
$lblPin = $form.Controls[$form.Controls.Count - 1]
$txtPin = New-Field ($rX) ($rY + 306) 360 $true
$lblPinC = New-Label 'Confirm PIN' $rX ($rY + 348) | Out-Null
$lblPinC = $form.Controls[$form.Controls.Count - 1]
$txtPinC = New-Field ($rX) ($rY + 374) 360 $true
# PIN fields disabled until BitLocker is checked.
$setPinEnabled = {
$on = $chkBitLocker.Checked
$txtPin.Enabled = $on; $txtPinC.Enabled = $on
$fg = if ($on) { $colMuted } else { [Drawing.Color]::FromArgb(90, 90, 98) }
$lblPin.ForeColor = $fg; $lblPinC.ForeColor = $fg
}
$chkBitLocker.Add_CheckedChanged($setPinEnabled)
& $setPinEnabled
# --- status line -----------------------------------------------------------
$lblStatus = New-Object Windows.Forms.Label
$lblStatus.Text = ''
$lblStatus.ForeColor = $colError; $lblStatus.Font = $fontBase
$lblStatus.AutoSize = $false
$lblStatus.Location = New-Object Drawing.Point(80, ($y + 8))
$lblStatus.Size = New-Object Drawing.Size(820, 28)
$form.Controls.Add($lblStatus)
# --- buttons ---------------------------------------------------------------
function Style-Button($b, $primary) {
$b.FlatStyle = 'Flat'; $b.Font = $fontBase
$b.Size = New-Object Drawing.Size(150, 40)
$b.FlatAppearance.BorderSize = 1
if ($primary) {
$b.BackColor = $colAccent; $b.ForeColor = $colBg
$b.FlatAppearance.BorderColor = $colAccent
} else {
$b.BackColor = $colPanel; $b.ForeColor = $colText
$b.FlatAppearance.BorderColor = $colMuted
}
}
$btnDefaults = New-Object Windows.Forms.Button
$btnDefaults.Text = 'Use defaults'
Style-Button $btnDefaults $false
$btnDefaults.Location = New-Object Drawing.Point(80, ($y + 48))
$form.Controls.Add($btnDefaults)
$btnCancel = New-Object Windows.Forms.Button
$btnCancel.Text = 'Cancel'
Style-Button $btnCancel $false
$btnCancel.Location = New-Object Drawing.Point(580, ($y + 48))
$form.Controls.Add($btnCancel)
$btnFinish = New-Object Windows.Forms.Button
$btnFinish.Text = 'Finish'
Style-Button $btnFinish $true
$btnFinish.Location = New-Object Drawing.Point(750, ($y + 48))
$form.Controls.Add($btnFinish)
# --- behaviour -------------------------------------------------------------
$btnDefaults.Add_Click({
$txtDisplay.Text = 'SilverMetal User'
$txtUser.Text = 'silver'
$txtComputer.Text = 'SILVER-PC'
# Leave passwords blank on purpose: the user must set them.
$txtPass.Text = ''; $txtPassC.Text = ''
foreach ($rb in $radioFlavours) { $rb.Checked = ($rb.Tag -eq 'daily-driver') }
$chkBitLocker.Checked = $false
$txtPin.Text = ''; $txtPinC.Text = ''
$lblStatus.ForeColor = $colMuted
$lblStatus.Text = 'Defaults applied. Set a password to continue.'
})
$btnCancel.Add_Click({ [Environment]::Exit(1) })
$form.Add_KeyDown({ if ($_.KeyCode -eq 'Escape') { [Environment]::Exit(1) } })
$btnFinish.Add_Click({
$lblStatus.ForeColor = $colError
$rUser = Test-SmUsername $txtUser.Text
if (-not $rUser.Ok) { $lblStatus.Text = $rUser.Message; return }
$rPass = Test-SmPassword $txtPass.Text $txtPassC.Text
if (-not $rPass.Ok) { $lblStatus.Text = $rPass.Message; return }
$rComp = Test-SmComputerName $txtComputer.Text
if (-not $rComp.Ok) { $lblStatus.Text = $rComp.Message; return }
$blEnabled = $chkBitLocker.Checked
$pin = ''
if ($blEnabled) {
$rPin = Test-SmPin $txtPin.Text $txtPinC.Text
if (-not $rPin.Ok) { $lblStatus.Text = $rPin.Message; return }
$pin = $txtPin.Text
}
$flavour = 'daily-driver'
foreach ($rb in $radioFlavours) { if ($rb.Checked) { $flavour = [string]$rb.Tag } }
$display = if ([string]::IsNullOrWhiteSpace($txtDisplay.Text)) { $txtUser.Text } else { $txtDisplay.Text }
$lblStatus.ForeColor = $colAccent
$lblStatus.Text = 'Generating answer file and starting Windows Setup...'
$form.Refresh()
$xml = New-SmAnswerFile -DisplayName $display -Username $txtUser.Text -Password $txtPass.Text `
-ComputerName $txtComputer.Text -Flavour $flavour `
-BitLockerEnable $blEnabled -BitLockerPin $pin
Set-Content -Path 'X:\sm\unattend.generated.xml' -Value $xml -Encoding UTF8
try { [void][xml](Get-Content 'X:\sm\unattend.generated.xml' -Raw) }
catch { [Environment]::Exit(1) } # bad XML -> fall back to default answer file
$setup = if (Test-Path 'X:\sources\setup.exe') { 'X:\sources\setup.exe' } else { 'X:\setup.exe' }
Start-Process -FilePath $setup -ArgumentList '/unattend:X:\sm\unattend.generated.xml' -Wait
[Environment]::Exit(0)
})
[void]$form.ShowDialog()
# If the form closes without Finish/Cancel handling exiting, treat as cancel.
[Environment]::Exit(1)
}
catch {
# Any failure (e.g. WinForms can't load in WinPE) -> log it where Start-Collector.cmd
# can show it on the console, then exit 1 so the wrapper falls back to the default answer file.
try { ($_ | Out-String) | Set-Content 'X:\sm\collector-error.txt' -Encoding ASCII } catch {}
Write-Host ('Collector error: ' + ($_ | Out-String))
[Environment]::Exit(1)
}