Files
DeployCenter/DeployCenter.ps1
benoit 5a121718d4 Le log est désormais stocké dans %temp%
DeployCenter peut être stocké dans un partage en lecture seule
Désactivation de l'hibernation via l'unattend
Désactivation de la mise en veille lorsque le pc est sur secteur
L'installation unattend empêche bitlocker de s'activer (pas de clé stockée en lieu sûr hors domaine et azure en automatique). Cela n'empêche pas de l'activer en connaissance de cause par la suite.
2026-10-19 00:11:09 +02:00

1153 lines
55 KiB
PowerShell

# DeployCenter - WinPE interface
# Compatible with Windows PowerShell 5.1
param(
# Show DeployCenter in a resizable window for debugging.
[switch]$Windowed,
[switch]$Vnc,
# Encode the password followed by the Windows SIM Password marker for DeployCenter.ini.
[switch]$EncodeUnattendPassword,
# Non-interactive mode requires deployment values and explicit erase confirmation.
[switch]$NonInteractive,
[int]$DiskNumber,
[string]$ImagePath,
[int]$EditionIndex,
[string]$PartitionProfileId,
[string]$UnattendProfileId,
[string]$HostName,
[int]$CSizeGB,
[switch]$ConfirmDiskErase,
[switch]$RestartWhenComplete,
[ValidateSet('Sources', 'Drivers')]
[string[]]$CopyLocally = @()
)
$script:Windowed = [bool]$Windowed
$script:NonInteractive = [bool]$NonInteractive
$script:AutoInstallParameters = @{
DiskNumber = $DiskNumber
ImagePath = $ImagePath
EditionIndex = $EditionIndex
PartitionProfileId = $PartitionProfileId
UnattendProfileId = $UnattendProfileId
HostName = $HostName
CSizeGB = $CSizeGB
ConfirmDiskErase = [bool]$ConfirmDiskErase
RestartWhenComplete = [bool]$RestartWhenComplete
CopyLocally = @($CopyLocally)
BoundParameters = @($PSBoundParameters.Keys)
}
$script:DeploymentAttempted = $false
$script:ExitCode = 0
$ErrorActionPreference = "Stop"
$localizationModulePath = Join-Path $PSScriptRoot 'Modules\Localization.psm1'
if (-not (Test-Path -LiteralPath $localizationModulePath -PathType Leaf)) { throw "Localization module not found: $localizationModulePath" }
Import-Module $localizationModulePath -Force
$script:Locale = Initialize-DeployCenterLocalization -CultureName ([Globalization.CultureInfo]::CurrentUICulture.Name)
if ($EncodeUnattendPassword) {
$securePassword = Read-Host -Prompt (Get-DeployCenterText -Text 'Local account password') -AsSecureString
$passwordPointer = [IntPtr]::Zero
try {
$passwordPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword)
$plainPassword = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($passwordPointer)
# PlainText=false for LocalAccount/Password requires the password followed by the "Password" suffix.
$passwordBytes = [Text.Encoding]::Unicode.GetBytes($plainPassword + 'Password')
[Console]::Out.WriteLine([Convert]::ToBase64String($passwordBytes))
} finally {
if ($passwordPointer -ne [IntPtr]::Zero) {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordPointer)
}
if ($null -ne $passwordBytes) { [Array]::Clear($passwordBytes, 0, $passwordBytes.Length) }
$plainPassword = $null
$securePassword = $null
}
return
}
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
Import-Module (Join-Path $PSScriptRoot "Modules\ThemeManager.psm1") -Force
$uiModulePath = Join-Path $PSScriptRoot 'Modules\UI.psm1'
if (-not (Test-Path -LiteralPath $uiModulePath -PathType Leaf)) { throw "UI module not found: $uiModulePath" }
# UI workflows use the main session state, so this file is loaded into that scope.
. ([scriptblock]::Create([System.IO.File]::ReadAllText($uiModulePath)))
Import-Module (Join-Path $PSScriptRoot "Modules\Configuration.psm1") -Force
Import-Module (Join-Path $PSScriptRoot "Modules\HardwareInventory.psm1") -Force
Import-Module (Join-Path $PSScriptRoot "Modules\DriverManagement.psm1") -Force
# GroupBox is drawn by DeployCenter so its background and border follow the themes.
if (-not ([System.Management.Automation.PSTypeName]'DeployCenter.ThemedGroupBox').Type) {
Add-Type -TypeDefinition @'
using System;
using System.Drawing;
using System.Windows.Forms;
namespace DeployCenter {
public class ThemedGroupBox : GroupBox {
public Color PanelColor { get; set; }
public Color BorderColor { get; set; }
public Color TextColor { get; set; }
public ThemedGroupBox() {
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
PanelColor = SystemColors.Control;
BorderColor = SystemColors.ControlDark;
TextColor = SystemColors.ControlText;
}
protected override void OnPaint(PaintEventArgs e) {
Graphics g = e.Graphics;
g.Clear(PanelColor);
Size textSize = TextRenderer.MeasureText(Text, Font);
int textLeft = 10;
int lineY = Math.Max(8, (int)Math.Ceiling(textSize.Height / 2.0));
using (Pen pen = new Pen(BorderColor)) {
g.DrawLine(pen, 0, lineY, textLeft - 2, lineY);
int afterText = textLeft + textSize.Width + 4;
if (afterText < Width - 2) g.DrawLine(pen, afterText, lineY, Width - 2, lineY);
g.DrawLine(pen, 0, lineY, 0, Height - 2);
g.DrawLine(pen, 0, Height - 2, Width - 2, Height - 2);
g.DrawLine(pen, Width - 2, lineY, Width - 2, Height - 2);
}
TextRenderer.DrawText(g, Text, Font, new Point(textLeft, 0), TextColor,
PanelColor, TextFormatFlags.NoPadding | TextFormatFlags.SingleLine);
}
}
}
'@ -ReferencedAssemblies System.Windows.Forms,System.Drawing
}
# ============================================================
# PATHS
# ============================================================
$script:DeployRoot = $PSScriptRoot
# Root of the drive hosting DeployCenter (e.g. Y:\); do not hard-code a drive letter.
$script:DeployVolumeRoot = $null
try {
$deployQualifier = Split-Path -Path $script:DeployRoot -Qualifier -ErrorAction Stop
if (-not [string]::IsNullOrWhiteSpace($deployQualifier)) {
$script:DeployVolumeRoot = $deployQualifier.TrimEnd('\') + '\'
}
} catch {
$script:DeployVolumeRoot = $null
}
$script:AssetsRoot = Join-Path $script:DeployRoot "Assets"
$script:LogoPath = Join-Path $script:AssetsRoot "logo.png"
$script:LogoDarkPath = Join-Path $script:AssetsRoot "logo-dark.png"
# Network detection is centralized in Modules\\Network.psm1.
$networkModulePath = Join-Path $PSScriptRoot "Modules\\Network.psm1"
if (-not (Test-Path -LiteralPath $networkModulePath -PathType Leaf)) {
throw "Network module was not found: $networkModulePath"
}
. ([scriptblock]::Create([System.IO.File]::ReadAllText($networkModulePath)))
$script:DeployIPv4 = Get-DeployNetworkIPv4
# Logging is centralized in Modules\\Logging.psm1.
$loggingModulePath = Join-Path $PSScriptRoot "Modules\\Logging.psm1"
if (-not (Test-Path -LiteralPath $loggingModulePath -PathType Leaf)) {
throw "Logging module not found: $loggingModulePath"
}
. ([scriptblock]::Create([System.IO.File]::ReadAllText($loggingModulePath)))
Initialize-DeployCenterLogging -IPv4 $script:DeployIPv4
# Keep the main script scope so Write-Log remains available to WinForms callbacks.
$script:SourceLogAction = {
param($Message, $Level, [bool]$SystemOutput = $false)
if ([string]::IsNullOrWhiteSpace([string]$Level)) { $Level = 'INFO' }
if (-not $SystemOutput) { $Message = ConvertTo-DeployCenterLocalizedLogMessage -Message ([string]$Message) }
Write-Log -Message $Message -Level $Level
}
# Windows source and image discovery.
$windowsSourcesModulePath = Join-Path $script:DeployRoot 'Modules\WindowsSources.psm1'
if (-not (Test-Path -LiteralPath $windowsSourcesModulePath -PathType Leaf)) {
throw "Windows Sources module not found: $windowsSourcesModulePath"
}
Import-Module $windowsSourcesModulePath -Force
# Partitioning profiles are PSD1 data files validated by their module.
$partitioningModulePath = Join-Path $script:DeployRoot 'Modules\Partitioning.psm1'
if (-not (Test-Path -LiteralPath $partitioningModulePath -PathType Leaf)) {
throw "Partitioning module not found: $partitioningModulePath"
}
Import-Module $partitioningModulePath -Force
$deploymentModulePath = Join-Path $script:DeployRoot 'Modules\Deployment.psm1'
if (-not (Test-Path -LiteralPath $deploymentModulePath -PathType Leaf)) {
throw "Deployment module was not found: $deploymentModulePath"
}
Import-Module $deploymentModulePath -Force
$unattendModulePath = Join-Path $script:DeployRoot 'Modules\Unattend.psm1'
if (-not (Test-Path -LiteralPath $unattendModulePath -PathType Leaf)) {
throw "Unattend module not found: $unattendModulePath"
}
Import-Module $unattendModulePath -Force
$script:PartitionProfilesRoot = Join-Path $script:DeployRoot 'Partitioning\Profiles'
$script:PartitionProfiles = @()
foreach ($profileName in (Get-DeployCenterPartitionProfileNames -ProfilesRoot $script:PartitionProfilesRoot)) {
try {
$script:PartitionProfiles += Import-DeployCenterPartitionProfile -ProfilesRoot $script:PartitionProfilesRoot -ProfileName $profileName
} catch {
& $script:SourceLogAction "Partitioning profile '$profileName' was skipped: $($_.Exception.Message)" 'WARNING'
}
}
if ($script:PartitionProfiles.Count -eq 0) {
throw "No valid partitioning profile was found in $script:PartitionProfilesRoot"
}
$script:SelectedPartitionProfileIndex = 0
for ($profileIndex = 0; $profileIndex -lt $script:PartitionProfiles.Count; $profileIndex++) {
if ($script:PartitionProfiles[$profileIndex].Default -eq $true) {
$script:SelectedPartitionProfileIndex = $profileIndex
break
}
}
$script:SelectedPartitionProfile = $script:PartitionProfiles[$script:SelectedPartitionProfileIndex]
$script:SelectedPartitionSizing = $null
$script:SelectedDiskIndex = $null
$script:SelectedWindowsEdition = $null
$script:RestartAfterDeployment = $false
$script:RefreshingDiskList = $false
$script:ChangingPartitionProfileSelection = $false
# Session inventory and portable UltraVNC: all files stay in the temporary folder.
$script:InventoryDir = Join-Path $env:TEMP 'DeployCenter'
$script:InventoryFile = Join-Path $script:InventoryDir 'DeployCenter_Inventory.json'
$script:VncPassword = $null
$script:VncDirectory = Join-Path $script:InventoryDir ("uvnc_{0}" -f $PID)
$script:VncProcess = $null
$script:VncStarted = $false
$script:HardwareInventory = $null
# ============================================================
# EXTERNAL THEMES (Themes\*.psd1)
# Themes are data files; no PowerShell code is executed from them.
# ============================================================
$script:ThemesRoot = Join-Path $script:DeployRoot "Themes"
if (-not (Test-Path -LiteralPath $script:ThemesRoot -PathType Container)) {
New-Item -Path $script:ThemesRoot -ItemType Directory -Force | Out-Null
}
# ============================================================
# PREFERENCES (DeployCenter.ini is read-only; it is never modified automatically)
# ============================================================
$script:ConfigPath = Join-Path $script:DeployRoot "DeployCenter.ini"
# Default themes: create them only when missing to make customization easier.
$defaultThemeFiles = @{
Dark = @"
@{
Background = "#171A21"
Panel = "#222731"
PanelAlt = "#1C2029"
Text = "#F1F5F9"
Secondary = "#9CA9BC"
Accent = "#3B82F6"
Success = "#4ADE80"
Border = "#414A59"
Danger = "#EF4444"
Button = @{
Background = "#303744"; Foreground = "#F1F5F9"; Border = "#414A59"
HoverBackground = "#3B4555"; PressedBackground = "#485365"
DisabledBackground = "#292E38"; DisabledForeground = "#7F8999"
}
PrimaryButton = @{
Background = "#3B82F6"; Foreground = "#FFFFFF"; Border = "#3B82F6"
HoverBackground = "#2563EB"; PressedBackground = "#1D4ED8"
DisabledBackground = "#344A6A"; DisabledForeground = "#9AAAC1"
}
DangerButton = @{
Background = "#EF4444"; Foreground = "#FFFFFF"; Border = "#EF4444"
HoverBackground = "#DC2626"; PressedBackground = "#B91C1C"
DisabledBackground = "#59383B"; DisabledForeground = "#BBA0A2"
}
}
"@
Light = @"
@{
Background = "#F5F7FA"
Panel = "#FFFFFF"
PanelAlt = "#F8FAFC"
Text = "#1E293B"
Secondary = "#475569"
Accent = "#2563EB"
Success = "#16A34A"
Border = "#CBD5E1"
Danger = "#DC2626"
Button = @{
Background = "#E8EDF3"; Foreground = "#1E293B"; Border = "#CBD5E1"
HoverBackground = "#DCE4EE"; PressedBackground = "#CBD5E1"
DisabledBackground = "#F1F3F6"; DisabledForeground = "#94A3B8"
}
PrimaryButton = @{
Background = "#2563EB"; Foreground = "#FFFFFF"; Border = "#2563EB"
HoverBackground = "#1D4ED8"; PressedBackground = "#1E40AF"
DisabledBackground = "#C7D8F8"; DisabledForeground = "#F8FAFC"
}
DangerButton = @{
Background = "#DC2626"; Foreground = "#FFFFFF"; Border = "#DC2626"
HoverBackground = "#B91C1C"; PressedBackground = "#991B1B"
DisabledBackground = "#F2C5C5"; DisabledForeground = "#FFF7F7"
}
}
"@
}
foreach ($themeFileName in $defaultThemeFiles.Keys) {
$themeFilePath = Join-Path $script:ThemesRoot ($themeFileName + '.psd1')
if (-not (Test-Path -LiteralPath $themeFilePath -PathType Leaf)) {
Set-Content -LiteralPath $themeFilePath -Value $defaultThemeFiles[$themeFileName] -Encoding UTF8
}
}
$script:Config = Read-DeployCenterConfig -ConfigPath $script:ConfigPath
$configuredDriverRoot = [string]$script:Config.DriverRootPath
if ([string]::IsNullOrWhiteSpace($configuredDriverRoot)) {
$script:DriverRoot = Join-Path (Split-Path -Path $script:DeployRoot -Parent) 'Drivers'
} else {
$configuredDriverRoot = $configuredDriverRoot.Replace('%DEPLOY_VOLUME%', [string]$script:DeployVolumeRoot).Replace('%DEPLOY_ROOT%', [string]$script:DeployRoot)
$configuredDriverRoot = [Environment]::ExpandEnvironmentVariables($configuredDriverRoot)
if ([System.IO.Path]::IsPathRooted($configuredDriverRoot)) { $script:DriverRoot = [System.IO.Path]::GetFullPath($configuredDriverRoot) }
else { $script:DriverRoot = [System.IO.Path]::GetFullPath((Join-Path $script:DeployRoot $configuredDriverRoot)) }
}
$script:DriverPackSelection = $null
$script:UnattendProfilesRoot = Join-Path $script:DeployRoot 'Unattend\Profiles'
$script:UnattendProfiles = @()
foreach ($unattendProfileName in (Get-DeployCenterUnattendProfileNames -ProfilesRoot $script:UnattendProfilesRoot)) {
try {
$script:UnattendProfiles += Import-DeployCenterUnattendProfile `
-ProfilesRoot $script:UnattendProfilesRoot -ProfileName $unattendProfileName
} catch {
& $script:SourceLogAction "Unattend profile '$unattendProfileName' was skipped: $($_.Exception.Message)" 'WARNING'
}
}
$duplicateUnattendNames = @($script:UnattendProfiles | Group-Object -Property Name | Where-Object Count -gt 1)
foreach ($duplicateName in $duplicateUnattendNames) {
& $script:SourceLogAction "Unattend profiles were skipped because their display name is duplicated: $($duplicateName.Name)." 'WARNING'
$script:UnattendProfiles = @($script:UnattendProfiles | Where-Object { [string]$_.Name -ne [string]$duplicateName.Name })
}
$script:UnattendOobeChoice = 'Use OOBE'
$script:AutoInstallOobeId = 'OOBE'
foreach ($partitionProfile in $script:PartitionProfiles) {
foreach ($partition in $partitionProfile.Partitions) {
if ([string]$partition.Size.Policy -ne 'ConfiguredMB') { continue }
$configKey = [string]$partition.Size.ConfigKey
if (-not $script:Config.ContainsKey($configKey)) {
throw "DeployCenter.ini parameter '$configKey' required by profile '$($partitionProfile.Name)' is missing from the configuration."
}
$configuredSizeMB = [int]$script:Config[$configKey]
if ($configuredSizeMB -le 0) {
throw "Parameter DeployCenter.ini '$configKey' must be strictement positif."
}
$partition.Size.Value = $configuredSizeMB
$partition.Size.PreviewMB = $configuredSizeMB
}
}
$availableThemes = @(Get-DeployCenterThemeNames -ThemesRoot $script:ThemesRoot)
if ($availableThemes.Count -eq 0) { throw "No valid theme was found in $script:ThemesRoot" }
if ($availableThemes -notcontains $script:Config.Theme) { $script:Config.Theme = "Light" }
$script:CurrentTheme = $script:Config.Theme
try {
$script:Theme = Import-DeployCenterTheme -ThemesRoot $script:ThemesRoot -ThemeName $script:CurrentTheme
} catch {
$script:CurrentTheme = "Light"
$script:Config.Theme = "Light"
$script:Theme = Import-DeployCenterTheme -ThemesRoot $script:ThemesRoot -ThemeName "Light"
}
# ============================================================
# FONTS
# ============================================================
$script:FontRegular = [System.Drawing.Font]::new("Segoe UI", [single]10, [System.Drawing.FontStyle]::Regular)
$script:FontSmall = [System.Drawing.Font]::new("Segoe UI", [single]9, [System.Drawing.FontStyle]::Regular)
$script:FontTitle = [System.Drawing.Font]::new("Segoe UI", [single]23, [System.Drawing.FontStyle]::Bold)
$script:FontSubtitle = [System.Drawing.Font]::new("Segoe UI", [single]10, [System.Drawing.FontStyle]::Regular)
$script:FontGroup = [System.Drawing.Font]::new("Segoe UI", [single]11, [System.Drawing.FontStyle]::Bold)
# ============================================================
# NETWORK, ULTRAVNC, AND SESSION INVENTORY
# ============================================================
# The IPv4 address was detected by Modules\\Network.psm1 before log initialization.
# VNC is disabled by default and starts only when the -Vnc switch is supplied.
$script:EnableVnc = [bool]$Vnc
if ($script:EnableVnc) {
try {
# VNC code runs in the main scope only when -Vnc is supplied.
$vncModulePath = Join-Path $PSScriptRoot "Modules\VNC.psm1"
if (-not (Test-Path -LiteralPath $vncModulePath -PathType Leaf)) {
throw "VNC module not found: $vncModulePath"
}
$vncCode = [System.IO.File]::ReadAllText($vncModulePath)
. ([scriptblock]::Create($vncCode))
Start-DeployCenterVnc
} catch {
& $script:SourceLogAction "Unable to start VNC: $($_.Exception.Message)" 'ERROR'
}
} else {
& $script:SourceLogAction "UltraVNC was not started: -vnc parameter was not supplied."
}
# ============================================================
# CREATE CONTROLS
# ============================================================
# ============================================================
# MAIN WINDOW
# ============================================================
$script:form = New-Object System.Windows.Forms.Form
$script:form.Text = "DeployCenter"
$script:form.BackColor = $script:Theme.Background
if ($script:Windowed) {
# Debug mode: standard, movable, resizable window.
$script:form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
$script:form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::Sizable
$script:form.WindowState = [System.Windows.Forms.FormWindowState]::Normal
$script:form.TopMost = $false
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
$debugWidth = [Math]::Min(1200, [Math]::Max(800, $screen.Width - 80))
$debugHeight = [Math]::Min(850, [Math]::Max(600, $screen.Height - 80))
$script:form.Size = New-Object System.Drawing.Size($debugWidth, $debugHeight)
} else {
# Normal mode: fullscreen, borderless, and kept on top.
$script:form.StartPosition = "Manual"
$script:form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None
$script:form.WindowState = [System.Windows.Forms.FormWindowState]::Maximized
$script:form.TopMost = $true
$script:form.Bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
}
$script:form.ForeColor = $script:Theme.Text
$script:form.Font = $script:FontRegular
$script:form.KeyPreview = $true
$script:form.MinimumSize = New-Object System.Drawing.Size(800, 600)
$screenWidth = $script:form.ClientSize.Width
$screenHeight = $script:form.ClientSize.Height
$margin = 30
$usableWidth = $screenWidth - (2 * $margin)
# ============================================================
# HEADER AND OPTIONAL LOGO
# ============================================================
$initialLogoImage = $null
# Only two files are used: logo.png (light theme and fallback) and logo-dark.png.
# In dark mode, logo-dark.png takes priority; logo.png is used if it is missing.
# In light mode, logo.png is used.
$initialLogoPath = Get-DeployCenterLogoPathForTheme -ThemeName $script:CurrentTheme `
-LogoPath $script:LogoPath -LogoDarkPath $script:LogoDarkPath
if ($null -ne $initialLogoPath) {
try {
$initialLogoImage = [System.Drawing.Image]::FromFile($initialLogoPath)
}
catch {
$initialLogoImage = $null
}
}
$mainHeader = Add-DeployCenterApplicationHeader -TargetForm $script:form -Theme $script:Theme `
-TitleFont $script:FontTitle -SubtitleFont $script:FontSubtitle -ThemeFont $script:FontRegular `
-LogoImage $initialLogoImage -ThemeNames $availableThemes -SelectedTheme $script:CurrentTheme
$script:pictureLogo = $mainHeader.Logo
$script:lblTitle = $mainHeader.Title
$script:lblSubtitle = $mainHeader.Subtitle
$script:cmbTheme = $mainHeader.ThemeSelector
# ============================================================
# PC INFORMATION
# ============================================================
$script:groupPC = New-GroupBox -Text "PC information" `
-X $margin -Y 100 -Width $usableWidth -Height 90 -Theme $script:Theme -Font $script:FontGroup
$script:lblManufacturer = New-Label -Text "Manufacturer: -" `
-X 18 -Y 25 -Width 330 -Height 24 -Theme $script:Theme -Font $script:FontRegular
$script:lblModel = New-Label -Text "Model: -" `
-X 360 -Y 25 -Width 400 -Height 24 -Theme $script:Theme -Font $script:FontRegular
$script:lblSerial = New-Label -Text "Serial number: -" `
-X 18 -Y 52 -Width 330 -Height 24 -Theme $script:Theme -Font $script:FontRegular
$script:lblFirmware = New-Label -Text "Firmware: -" `
-X 360 -Y 52 -Width 220 -Height 24 -Theme $script:Theme -Font $script:FontRegular
$script:lblArchitecture = New-Label -Text "Architecture: -" `
-X 600 -Y 52 -Width 220 -Height 24 -Theme $script:Theme -Font $script:FontRegular
$script:groupPC.Controls.AddRange(@(
$script:lblManufacturer,
$script:lblModel,
$script:lblSerial,
$script:lblFirmware,
$script:lblArchitecture
))
$script:form.Controls.Add($script:groupPC)
# ============================================================
# DEPLOYMENT PREPARATION
# ============================================================
$script:groupDisks = New-GroupBox -Text "Deployment preparation" `
-X $margin -Y 205 -Width $usableWidth -Height 270 -Theme $script:Theme -Font $script:FontGroup
$deploymentControls = New-DeployCenterDeploymentControls `
-Parent $script:groupDisks `
-ProfileNames @($script:PartitionProfiles | ForEach-Object { [string]$_.Name }) `
-Theme $script:Theme `
-Font $script:FontSmall
$script:cmbDisk = $deploymentControls.DiskComboBox
$script:btnChooseWindows = $deploymentControls.ChooseSystemButton
$script:lblSelectedWindows = $deploymentControls.SelectedSystemLabel
$script:lblDriverStatus = $deploymentControls.DriverStatusLabel
$script:DriverStatusToolTip = $deploymentControls.DriverToolTip
$script:lblPartitionProfile = $deploymentControls.PartitionProfileLabel
$script:cmbPartitionProfile = $deploymentControls.PartitionProfileComboBox
$script:cmbUnattendProfile = $deploymentControls.UnattendProfileComboBox
$script:DeploymentControls = $deploymentControls
$script:lblHostName = $deploymentControls.HostNameLabel
$script:txtHostName = $deploymentControls.HostNameTextBox
$script:cmbPartitionProfile.SelectedIndex = $script:SelectedPartitionProfileIndex
$script:form.Controls.Add($script:groupDisks)
function Update-DeployCenterDriverStatus {
if (-not $script:lblDriverStatus) { return }
$editionName = if ($script:SelectedWindowsEdition) { [string]$script:SelectedWindowsEdition.Name } else { $null }
$script:DriverPackSelection = $null
if (-not $script:HardwareInventory -or -not $editionName) {
$statusText = "{0} ({1})" -f `
(Get-DeployCenterText -Text "Drivers searched under: $script:DriverRoot"), `
(Get-DeployCenterText -Text 'Select a Windows edition to check')
} else {
try {
$script:DriverPackSelection = Find-DeployCenterDriverPack `
-DriverRoot $script:DriverRoot `
-Manufacturer ([string]$script:HardwareInventory.Manufacturer) `
-Model ([string]$script:HardwareInventory.Model) `
-SystemSKU ([string]$script:HardwareInventory.SystemSKU) `
-EditionName $editionName
if ($script:DriverPackSelection.Found) {
$statusText = "Drivers found: $($script:DriverPackSelection.SearchPath) ($($script:DriverPackSelection.InfCount) INF)."
} else {
$statusText = "No drivers found ($($script:DriverPackSelection.Status)) : $($script:DriverPackSelection.SearchPath)."
}
} catch {
$statusText = "Driver search failed: $($_.Exception.Message)"
}
}
$script:lblDriverStatus.Text = ConvertTo-DeployCenterLocalizedLogMessage -Message $statusText
$script:lblDriverStatus.ForeColor = if ($script:DriverPackSelection -and $script:DriverPackSelection.Found) { $script:Theme.Success } else { $script:Theme.Secondary }
if ($script:DriverStatusToolTip) { $script:DriverStatusToolTip.SetToolTip($script:lblDriverStatus, $statusText) }
}
Update-DeployCenterDriverStatus
# ============================================================
# LOG IN A SEPARATE WINDOW
# ============================================================
$logWindow = New-DeployCenterLogWindow -Theme $script:Theme -Font $script:FontRegular
$script:logForm = $logWindow.Form
$script:txtLog = $logWindow.TextBox
$buttonHeight = 38
# In fullscreen mode, move the bottom buttons closer without overlapping the footer.
# In windowed mode, keep the current position.
$buttonY = if ($script:Windowed) {
$screenHeight - 54 - $buttonHeight
} else {
$screenHeight - 42 - $buttonHeight
}
# LOG, REFRESH, AND CLOSE BUTTONS
# ============================================================
$buttonWidth = 140
$buttonGap = 12
$closeX = $screenWidth - $margin - $buttonWidth
$deployX = $closeX - $buttonGap - $buttonWidth
$refreshX = $deployX - $buttonGap - $buttonWidth
$script:btnLog = New-Button -Text "Log" `
-X $margin -Y $buttonY -Width $buttonWidth -Height $buttonHeight `
-Theme $script:Theme -Font $script:FontRegular `
-Role "Button"
$script:btnRefresh = New-Button -Text "Refresh" `
-X $refreshX -Y $buttonY -Width $buttonWidth -Height $buttonHeight `
-Theme $script:Theme -Font $script:FontRegular `
-Role "PrimaryButton"
$script:btnDeploy = New-Button -Text "Install" `
-X $deployX -Y $buttonY -Width $buttonWidth -Height $buttonHeight `
-Theme $script:Theme -Font $script:FontRegular `
-Role "PrimaryButton"
$script:btnClose = New-Button -Text "Close" `
-X $closeX -Y $buttonY -Width $buttonWidth -Height $buttonHeight `
-Theme $script:Theme -Font $script:FontRegular `
-Role "DangerButton"
$script:form.Controls.Add($script:btnLog)
$script:form.Controls.Add($script:btnRefresh)
$script:form.Controls.Add($script:btnDeploy)
$script:form.Controls.Add($script:btnClose)
Add-SessionFooter -TargetForm $script:form -Theme $script:Theme -Font $script:FontSmall `
-IPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -ShowPassword $true
# ============================================================
# APPLY THE THEME TO ALL CONTROLS
# ============================================================
# ============================================================
# DETECT WINDOWS SOURCES ON AN SMB SHARE
# ============================================================
# ============================================================
# HARDWARE INVENTORY
# ============================================================
# ============================================================
# RESPONSIVE LAYOUT
# Main panels follow the window size; margins and control heights remain fixed.
# ============================================================
# ============================================================
# EVENTS
# ============================================================
function Show-DeployCenterValidationError {
param([Parameter(Mandatory=$true)][string]$Message, [string]$Title = 'Windows deployment')
if ($script:NonInteractive) {
& $script:SourceLogAction $Message 'ERROR'
$script:ExitCode = 1
if (-not $script:form.IsDisposed) { $script:form.Close() }
return
}
Show-DeployCenterMessage -Message $Message -Title $Title -Icon Warning -Owner $script:form -Theme $script:Theme -Font $script:FontRegular
}
function Initialize-DeployCenterAutomatedInstall {
$parameters = $script:AutoInstallParameters
$allowedDeploymentParameters = @('DiskNumber','ImagePath','EditionIndex','PartitionProfileId','UnattendProfileId','HostName','CSizeGB','ConfirmDiskErase','RestartWhenComplete')
$providedDeploymentParameters = @($parameters.BoundParameters | Where-Object { $_ -in $allowedDeploymentParameters })
if (-not $script:NonInteractive) {
if ($providedDeploymentParameters.Count -gt 0) {
throw 'Command-line deployment parameters require -NonInteractive.'
}
return
}
foreach ($requiredParameter in @('DiskNumber','ImagePath','EditionIndex','PartitionProfileId','UnattendProfileId')) {
if ($requiredParameter -notin $parameters.BoundParameters) { throw "Parameter -$requiredParameter is required with -NonInteractive." }
}
if (-not $parameters.ConfirmDiskErase) { throw 'Non-interactive mode requires -ConfirmDiskErase because the disk will be erased completely.' }
if ([int]$parameters.DiskNumber -lt 0) { throw 'DiskNumber must be a non-negative integer.' }
if ([int]$parameters.EditionIndex -le 0) { throw 'EditionIndex must be greater than zero.' }
$diskInfo = Get-SelectedDeployDiskInfo -HardwareInfo $script:HardwareInventory -DiskIndex ([int]$parameters.DiskNumber)
if ($null -eq $diskInfo) { throw "Disk $($parameters.DiskNumber) does not exist in the hardware inventory." }
$matchingPartitionProfiles = @($script:PartitionProfiles | Where-Object { [string]$_.Id -eq [string]$parameters.PartitionProfileId })
if ($matchingPartitionProfiles.Count -ne 1) { throw "Partitioning profile not found or ambiguous: $($parameters.PartitionProfileId)." }
$partitionProfile = $matchingPartitionProfiles[0]
$resolvedImage = $null
$editionTransferred = $false
try {
$resolvedImage = Resolve-InstallImagePath -SelectedPath ([string]$parameters.ImagePath) -ExistingImage $script:SelectedWindowsEdition
$editionResult = Get-WindowsImageEditions -ImagePath ([string]$resolvedImage.ImagePath) -LogAction $script:SourceLogAction
$matchingEditions = @($editionResult.Editions | Where-Object { [int]$_.Index -eq [int]$parameters.EditionIndex })
if ($matchingEditions.Count -ne 1) { throw "Edition index $($parameters.EditionIndex) does not exist in the specified image." }
$chosenEdition = $matchingEditions[0]
$selectedImage = [pscustomobject]@{
SourcePath = [string]$resolvedImage.SourcePath
ImagePath = [string]$resolvedImage.ImagePath
MountedImage = $resolvedImage.MountedImage
Edition = $chosenEdition
Index = [int]$chosenEdition.Index
Name = [string]$chosenEdition.Name
}
$unattendId = [string]$parameters.UnattendProfileId
$unattendProfile = $null
if (-not [string]::Equals($unattendId, $script:AutoInstallOobeId, [StringComparison]::OrdinalIgnoreCase)) {
$matchingUnattendProfiles = @($script:UnattendProfiles | Where-Object { [string]$_.Id -eq $unattendId })
if ($matchingUnattendProfiles.Count -ne 1) { throw "Unattend profile not found or ambiguous: $unattendId." }
$unattendProfile = $matchingUnattendProfiles[0]
$target = if ([string]$chosenEdition.Name -match '(?i)server') { 'Server' } else { 'Workstation' }
if ([string]$unattendProfile.Target -ne $target) {
throw "The Unattend profile '$($unattendProfile.Name)' targets $($unattendProfile.Target), but the edition '$($chosenEdition.Name)' is type $target."
}
}
$hostName = [string]$parameters.HostName
if ($null -ne $unattendProfile -and $unattendProfile.ComputerName) {
if ('HostName' -notin $parameters.BoundParameters -or
$hostName -notmatch '^(?=.*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,13}[A-Za-z0-9])?$') {
throw "The selected Unattend profile requires a valid computer name of 1 to 15 characters."
}
Test-DeployCenterUnattendConfiguration -Profile $unattendProfile -Configuration $script:Config -HostName $hostName
}
$sizeInputs = @($partitionProfile.Partitions | Where-Object { [string]$_.Size.Policy -eq 'UserInputGB' })
$sizingState = $null
if ($sizeInputs.Count -gt 0) {
if ($sizeInputs.Count -ne 1 -or [string]$sizeInputs[0].Role -ne 'Windows') {
throw "Non-interactive mode supports only one user-defined size input for C: in profile '$($partitionProfile.Id)'."
}
if ('CSizeGB' -notin $parameters.BoundParameters) { throw "The profile '$($partitionProfile.Id)' requires parameter -CSizeGB." }
$capacity = Get-DeployCenterPartitionSizingCapacity -Profile $partitionProfile -DiskSizeBytes ([long]$diskInfo.SizeBytes)
$remainder = @($partitionProfile.Partitions | Where-Object { [string]$_.Size.Policy -eq 'RemainderAfterInputs' })[0]
$minimumGB = [double]$sizeInputs[0].Size.MinimumGB
$maximumGB = [math]::Floor($capacity.UsableGB - [double]$remainder.Size.MinimumGB)
if ([int]$parameters.CSizeGB -lt $minimumGB -or [int]$parameters.CSizeGB -gt $maximumGB) {
throw "CSizeGB must be between $minimumGB et $maximumGB GB for this disk and profile."
}
$selection = @{
Inputs = @{ Windows = [double]$parameters.CSizeGB }
RemainderRole = [string]$remainder.Role
RemainderGB = [math]::Round(($capacity.UsableGB - [double]$parameters.CSizeGB), 1)
AvailableGB = [double]$capacity.UsableGB
}
$sizingState = New-DeployCenterPartitionSizingState -Profile $partitionProfile -DiskInfo $diskInfo -Selection $selection
} elseif ('CSizeGB' -in $parameters.BoundParameters) {
throw "The profile '$($partitionProfile.Id)' does not define a size input for C:; -CSizeGB is not applicable."
}
$script:ChangingPartitionProfileSelection = $true
try {
for ($i = 0; $i -lt $script:cmbDisk.Items.Count; $i++) {
if ([int]$script:cmbDisk.Items[$i].Tag -eq [int]$parameters.DiskNumber) { $script:cmbDisk.SelectedIndex = $i; break }
}
$profileIndex = -1
for ($i = 0; $i -lt $script:PartitionProfiles.Count; $i++) {
if ([string]$script:PartitionProfiles[$i].Id -eq [string]$partitionProfile.Id) { $profileIndex = $i; break }
}
if ($profileIndex -lt 0) { throw "Partitioning profile '$($partitionProfile.Id)' cannot be displayed." }
$script:cmbPartitionProfile.SelectedIndex = $profileIndex
$script:SelectedPartitionProfileIndex = $profileIndex
$script:SelectedPartitionProfile = $partitionProfile
$script:SelectedPartitionSizing = $sizingState
} finally {
$script:ChangingPartitionProfileSelection = $false
}
$script:SelectedWindowsEdition = $selectedImage
Update-DeployCenterDriverStatus
$editionTransferred = $true
$sourceDescription = if ($selectedImage.ImagePath) { $selectedImage.ImagePath } else { $selectedImage.SourcePath }
$script:lblSelectedWindows.Text = "{0} (index {1}) — {2}" -f $selectedImage.Name, $selectedImage.Index, $sourceDescription
$script:lblSelectedWindows.ForeColor = $script:Theme.Success
$script:cmbPartitionProfile.Enabled = $true
Set-DeployCenterUnattendProfileChoices -ComboBox $script:cmbUnattendProfile `
-Profiles $script:UnattendProfiles -EditionName ([string]$selectedImage.Name) -OobeChoice $script:UnattendOobeChoice
$choice = if ($null -eq $unattendProfile) {
Get-DeployCenterText -Text $script:UnattendOobeChoice
} else {
Get-DeployCenterText -Text ([string]$unattendProfile.Name)
}
$choiceIndex = $script:cmbUnattendProfile.Items.IndexOf($choice)
if ($choiceIndex -lt 0) { throw "The Unattend profile '$choice' is not available for the selected edition." }
$script:cmbUnattendProfile.SelectedIndex = $choiceIndex
if ($unattendProfile -and $unattendProfile.ComputerName) { $script:txtHostName.Text = $hostName }
& $script:SourceLogAction ("Non-interactive mode prepared: disk {0}, image {1}, index {2}, partitioning profile {3}, Unattend profile {4}." -f $diskInfo.Index, $selectedImage.SourcePath, $selectedImage.Index, $partitionProfile.Id, $unattendId) 'INFO'
} finally {
if (-not $editionTransferred -and $resolvedImage -and $resolvedImage.MountedImage) {
try { [void](Dismount-DeployCenterInstallImage -Image $resolvedImage) } catch { }
}
}
}
$script:btnChooseWindows.Add_Click({
Show-WindowsSourcesDialog
Update-DeployCenterDriverStatus
if ($script:SelectedWindowsEdition) {
$edition = $script:SelectedWindowsEdition
$sourceDescription = if ($edition.ImagePath) { [string]$edition.ImagePath } else { [string]$edition.SourcePath }
$script:lblSelectedWindows.Text = "{0} (index {1}) — {2}" -f $edition.Name, $edition.Index, $sourceDescription
$script:lblSelectedWindows.ForeColor = $script:Theme.Success
$script:lblSelectedWindows.Name = 'lblSelectedWindows'
$script:cmbPartitionProfile.Enabled = $true
Set-DeployCenterUnattendProfileChoices -ComboBox $script:cmbUnattendProfile `
-Profiles $script:UnattendProfiles -EditionName ([string]$edition.Name) `
-OobeChoice $script:UnattendOobeChoice
}
})
$script:cmbUnattendProfile.Add_SelectedIndexChanged({
$showHostName = $script:cmbUnattendProfile.SelectedItem -and
[string]$script:cmbUnattendProfile.SelectedItem -ne (Get-DeployCenterText -Text $script:UnattendOobeChoice)
Set-DeployCenterUnattendHostNameVisibility -Controls $script:DeploymentControls -ShowHostName ([bool]$showHostName)
})
$script:btnLog.Add_Click({
if ($script:logForm.IsDisposed) { return }
if (-not $script:logForm.Visible) { $script:logForm.Show($script:form) }
$script:logForm.BringToFront()
})
$script:btnRefresh.Add_Click({
Update-HardwareView
})
$script:btnDeploy.Add_Click({
$diskIndex = $null
if ($null -ne $script:cmbDisk.SelectedItem) { $diskIndex = [int]$script:cmbDisk.SelectedItem.Tag }
$diskInfo = if ($null -ne $diskIndex) { Get-SelectedDeployDiskInfo -HardwareInfo $script:HardwareInventory -DiskIndex $diskIndex } else { $null }
$unattendChoice = [string]$script:cmbUnattendProfile.SelectedItem
$unattendProfile = $null
$hostName = $null
if ($unattendChoice -and $unattendChoice -ne (Get-DeployCenterText -Text $script:UnattendOobeChoice)) {
$matchingProfiles = @($script:UnattendProfiles | Where-Object { (Get-DeployCenterText -Text ([string]$_.Name)) -eq $unattendChoice })
if ($matchingProfiles.Count -ne 1) {
Show-DeployCenterValidationError -Message 'The selected Unattend profile was not found or is ambiguous.' -Title 'Unattend profile'
return
}
$unattendProfile = $matchingProfiles[0]
$hostName = [string]$script:txtHostName.Text.Trim()
}
if ($null -eq $diskInfo) {
Show-DeployCenterValidationError -Message 'Select the destination disk.'
return
}
if ($null -eq $script:SelectedWindowsEdition) {
Show-DeployCenterValidationError -Message 'Select a Windows edition to install.'
return
}
if ($null -ne $unattendProfile -and $unattendProfile.ComputerName -and $hostName -notmatch '^(?=.*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,13}[A-Za-z0-9])?$') {
Show-DeployCenterValidationError -Message "The computer name must be 1 to 15 characters, start and end with a letter or digit, and contain only letters, digits, or hyphens." -Title "Invalid computer name"
return
}
if ($null -ne $unattendProfile) {
try {
Test-DeployCenterUnattendConfiguration -Profile $unattendProfile `
-Configuration $script:Config -HostName $hostName
} catch {
Show-DeployCenterValidationError -Message $_.Exception.Message -Title 'Invalid Unattend configuration'
return
}
}
if ($script:SelectedPartitionSizing -and [int]$script:SelectedPartitionSizing.DiskIndex -ne [int]$diskInfo.Index) {
Show-DeployCenterValidationError -Message 'The partition sizes belong to a different disk. Select the partitioning profile again.' -Title 'Partitioning'
return
}
$sizeDescription = Get-DeployCenterText -Text 'Sizes defined by the profile'
if ($script:SelectedPartitionSizing -and $script:SelectedPartitionSizing.Inputs.Count -gt 0) {
$sizeParts = @()
foreach ($role in $script:SelectedPartitionSizing.Inputs.Keys) {
$sizeParts += ("{0}: {1} {2}" -f $role, $script:SelectedPartitionSizing.Inputs[$role], (Get-DeployCenterText -Text 'GB'))
}
$sizeParts += ("{0} {1:N1} {2}" -f (Get-DeployCenterText -Text 'Remaining space:'), $script:SelectedPartitionSizing.RemainderGB, (Get-DeployCenterText -Text 'GB'))
$sizeDescription = $sizeParts -join ', '
}
$unattendDescription = if ($null -ne $unattendProfile) {
"$(Get-DeployCenterText -Text 'Unattend profile:') $($unattendProfile.Name)`r`n$(Get-DeployCenterText -Text 'Computer name:') $hostName"
} else {
"$(Get-DeployCenterText -Text 'Initial configuration:') $(Get-DeployCenterText -Text 'Use OOBE')"
}
if ($script:NonInteractive) {
if (-not $script:AutoInstallParameters.ConfirmDiskErase) {
Show-DeployCenterValidationError -Message "Disk erase was declined: -ConfirmDiskErase is required."
return
}
$confirmation = $true
} else {
$confirmation = Show-DeployCenterConfirmation `
-Title "Confirm disk erasure" `
-Message ("{0} {1} — {2} ({3:N1} {4})`r`n{5} {6}`r`n{7} {8}`r`n{9} {10}`r`n{11}`r`n{12}" -f `
(Get-DeployCenterText -Text 'Disk'), $diskInfo.Index, $diskInfo.Model, $diskInfo.SizeGB, (Get-DeployCenterText -Text 'GB'), `
(Get-DeployCenterText -Text 'Partitioning profile:'), $script:SelectedPartitionProfile.Name, `
(Get-DeployCenterText -Text 'Edition:'), $script:SelectedWindowsEdition.Name, `
(Get-DeployCenterText -Text 'Sizes:'), $sizeDescription, $unattendDescription, `
(Get-DeployCenterText -Text 'All contents of the disk will be erased. This operation cannot be undone.')) `
-Owner $script:form -Theme $script:Theme -Font $script:FontRegular
}
if (-not $confirmation) { return }
$deploymentSteps = @('Erasing and partitioning the disk…')
if ('Sources' -in $script:AutoInstallParameters.CopyLocally) {
$deploymentSteps += 'Copying the Windows image locally…'
}
if ($script:DriverPackSelection -and $script:DriverPackSelection.Found -and
'Drivers' -in $script:AutoInstallParameters.CopyLocally) {
$deploymentSteps += 'Copying drivers locally…'
}
$deploymentSteps += @('Applying Windows to ', 'Installing hardware drivers…', 'Checking WinRE partition capacity…')
if ($null -ne $unattendProfile) { $deploymentSteps += 'Preparing the Windows answer file…' }
$deploymentSteps += @(
'Configuring UEFI boot…',
'Configuring the WinRE recovery partition…',
'Copying winre.wim…',
'Registering the WinRE image…',
'Enabling WinRE…',
'Finalizing partitions…',
'Cleaning up temporary files…'
)
if (-not ($script:DriverPackSelection -and $script:DriverPackSelection.Found)) {
$deploymentSteps = @($deploymentSteps | Where-Object { $_ -ne 'Installing hardware drivers…' })
}
$progress = New-DeployCenterDeploymentProgress `
-Theme $script:Theme -Font $script:FontRegular -FooterFont $script:FontSmall `
-TitleFont $script:FontTitle -SubtitleFont $script:FontSubtitle `
-Steps $deploymentSteps -Owner $script:form -LogoImage $(if ($script:pictureLogo) { $script:pictureLogo.Image } else { $null }) -Windowed:$script:Windowed `
-IPv4 $script:DeployIPv4 -VncPassword $script:VncPassword
$script:DeploymentAttempted = $true
$script:form.Enabled = $false
try {
$progress.Form.Show($script:form)
[System.Windows.Forms.Application]::DoEvents()
$deploymentLogAction = $script:SourceLogAction
$deploymentProgressMessage = Get-DeployCenterText -Text 'Preparing deployment…'
$deploymentProgressAction = {
param($Message, $Percent)
if ($null -ne $Message) {
$deploymentProgressMessage = Get-DeployCenterText -Text ([string]$Message)
if ($deploymentLogAction -is [scriptblock]) {
& $deploymentLogAction ([string]$Message) 'INFO'
}
}
& $progress.UpdateAction $Message $Percent $deploymentProgressMessage
}.GetNewClosure()
$deploymentResult = Invoke-DeployCenterWindowsDeployment `
-DiskInfo $diskInfo `
-Profile $script:SelectedPartitionProfile `
-SizingState $script:SelectedPartitionSizing `
-Edition $script:SelectedWindowsEdition `
-HostName $hostName `
-IdentifierLabel (Get-DeployCenterText -Text 'identifier') `
-UnattendProfile $unattendProfile `
-UnattendConfiguration $script:Config `
-Firmware $script:HardwareInventory.Firmware `
-DriverPath $(if ($script:DriverPackSelection -and $script:DriverPackSelection.Found) { [string]$script:DriverPackSelection.DriverPath } else { $null }) `
-DriverInfCount $(if ($script:DriverPackSelection -and $script:DriverPackSelection.Found) { [int]$script:DriverPackSelection.InfCount } else { 0 }) `
-CopyLocally $script:AutoInstallParameters.CopyLocally `
-TemporaryRoot $script:InventoryDir `
-LogAction $script:SourceLogAction `
-ProgressAction $deploymentProgressAction
Complete-DeployCenterDeploymentProgress -Progress $progress
$completionMessage = 'Windows was applied successfully. Deployment is ready.'
$restartByDefault = $true
if ($deploymentResult -and @($deploymentResult.WinRERecommendationWarnings).Count -gt 0) {
$winreWarnings = @($deploymentResult.WinRERecommendationWarnings) -join '; '
$completionMessage += "`r`n`r`nWinRE reminder: Microsoft recommendations are not all met: $winreWarnings."
$restartByDefault = $false
}
if ($script:NonInteractive) {
$restartAfterDeployment = [bool]$script:AutoInstallParameters.RestartWhenComplete
if ($restartAfterDeployment -and -not $restartByDefault) {
$restartAfterDeployment = $false
& $script:SourceLogAction 'Automatic restart was canceled because the WinRE recommendations are not met.' 'WARNING'
}
& $script:SourceLogAction $completionMessage 'SUCCESS'
$progress.Form.Close()
} else {
$restartAfterDeployment = Show-DeployCenterDeploymentCompletion `
-Progress $progress `
-Message $completionMessage `
-RestartByDefault $restartByDefault `
-Owner $script:form
}
$script:form.Enabled = $true
Copy-DeployCenterLog -DestinationRoot 'C:\'
if ($restartAfterDeployment) {
$script:RestartAfterDeployment = $true
$script:form.Close()
}
if ($script:NonInteractive) { $script:form.Close() }
} catch {
if (-not $progress.Form.IsDisposed) { Fail-DeployCenterDeploymentProgress -Progress $progress }
$script:form.Enabled = $true
& $script:SourceLogAction "Deployment failed: $($_.Exception.Message)" 'ERROR'
Copy-DeployCenterLog -DestinationRoot 'C:\'
if ($script:NonInteractive) {
$script:ExitCode = 1
$script:form.Close()
} else {
Show-DeployCenterMessage -Message $_.Exception.Message -Title 'Deployment failed' -Icon Error -Owner $script:form -Theme $script:Theme -Font $script:FontRegular
}
if (-not $progress.Form.IsDisposed) { $progress.Form.Close() }
} finally {
if (-not $progress.Form.IsDisposed) { $progress.Form.Dispose() }
$script:form.Enabled = $true
if (-not $script:form.IsDisposed -and $script:form.Visible) { $script:form.Activate() }
}
})
$script:btnClose.Add_Click({
$script:form.Close()
})
$script:cmbTheme.Add_SelectedIndexChanged({
if ($script:cmbTheme.SelectedItem) {
Set-DeployCenterTheme -ThemeName ([string]$script:cmbTheme.SelectedItem)
}
})
$script:cmbDisk.Add_SizeChanged({
Set-DiskSelectorWidth
})
$script:cmbPartitionProfile.Add_SelectedIndexChanged({
if ($script:ChangingPartitionProfileSelection) { return }
$selectedIndex = $script:cmbPartitionProfile.SelectedIndex
if ($selectedIndex -ge 0 -and $selectedIndex -lt $script:PartitionProfiles.Count) {
$candidateProfile = $script:PartitionProfiles[$selectedIndex]
$sizeInputs = @($candidateProfile.Partitions | Where-Object { $_.Size.Policy -eq 'UserInputGB' })
if ($sizeInputs.Count -gt 0) {
$selectedDiskTag = if ($null -ne $script:cmbDisk.SelectedItem) { [int]$script:cmbDisk.SelectedItem.Tag } else { $null }
$diskInfo = if ($null -ne $selectedDiskTag) { Get-SelectedDeployDiskInfo -HardwareInfo $script:HardwareInventory -DiskIndex $selectedDiskTag } else { $null }
$initialValues = @{}
if ($script:SelectedPartitionSizing -and $script:SelectedPartitionSizing.ProfileId -eq $candidateProfile.Id) {
$initialValues = $script:SelectedPartitionSizing.Inputs
}
if ($null -eq $diskInfo) {
Show-DeployCenterMessage -Message "Select a physical disk first." -Title 'Partitioning' -Icon Information -Owner $script:form -Theme $script:Theme -Font $script:FontRegular
}
if ($null -eq $diskInfo -or -not (Set-PartitionProfileSizingSelection -Profile $candidateProfile -DiskInfo $diskInfo -InitialValues $initialValues)) {
$script:ChangingPartitionProfileSelection = $true
try { $script:cmbPartitionProfile.SelectedIndex = $script:SelectedPartitionProfileIndex }
finally { $script:ChangingPartitionProfileSelection = $false }
return
}
} else {
$script:SelectedPartitionSizing = $null
}
$script:SelectedPartitionProfileIndex = $selectedIndex
$script:SelectedPartitionProfile = $candidateProfile
& $script:SourceLogAction "Selected partitioning profile: $($candidateProfile.Name)" 'INFO'
if ($script:SelectedPartitionSizing) {
foreach ($role in $script:SelectedPartitionSizing.Inputs.Keys) {
& $script:SourceLogAction "Entered size for $role`: $($script:SelectedPartitionSizing.Inputs[$role]) GB; remaining space: $($script:SelectedPartitionSizing.RemainderGB) GB" 'INFO'
}
}
}
})
$script:cmbDisk.Add_SelectedIndexChanged({
if ($script:RefreshingDiskList -or $script:ChangingPartitionProfileSelection) { return }
$selectedDiskTag = if ($null -ne $script:cmbDisk.SelectedItem) { [int]$script:cmbDisk.SelectedItem.Tag } else { $null }
$diskInfo = if ($null -ne $selectedDiskTag) { Get-SelectedDeployDiskInfo -HardwareInfo $script:HardwareInventory -DiskIndex $selectedDiskTag } else { $null }
if ($null -eq $diskInfo) { $script:SelectedDiskIndex = $null; return }
$profileSizeInputs = @($script:SelectedPartitionProfile.Partitions | Where-Object { $_.Size.Policy -eq 'UserInputGB' })
if ($profileSizeInputs.Count -gt 0) {
$initialValues = if ($script:SelectedPartitionSizing -and $script:SelectedPartitionSizing.ProfileId -eq $script:SelectedPartitionProfile.Id) {
$script:SelectedPartitionSizing.Inputs
} else { @{} }
if (-not (Set-PartitionProfileSizingSelection -Profile $script:SelectedPartitionProfile -DiskInfo $diskInfo -InitialValues $initialValues)) {
$script:RefreshingDiskList = $true
try {
for ($i = 0; $i -lt $script:cmbDisk.Items.Count; $i++) {
if ([string]$script:cmbDisk.Items[$i].Tag -eq [string]$script:SelectedDiskIndex) { $script:cmbDisk.SelectedIndex = $i; break }
}
} finally { $script:RefreshingDiskList = $false }
return
}
}
$script:SelectedDiskIndex = [string]$diskInfo.Index
})
$script:form.Add_SizeChanged({
Update-DeployCenterLayout
})
$script:form.Add_KeyDown({
param($sender, $eventArgs)
if ($eventArgs.KeyCode -eq [System.Windows.Forms.Keys]::Escape) {
$script:form.Close()
}
elseif ($eventArgs.KeyCode -eq [System.Windows.Forms.Keys]::F5) {
Update-HardwareView
}
})
$script:form.Add_Shown({
# Use the real maximized client width to position the theme button.
Update-DeployCenterLayout
$script:cmbTheme.BringToFront()
& $script:SourceLogAction "DeployCenter started."
& $script:SourceLogAction "Folder: $script:DeployRoot"
& $script:SourceLogAction "Log: $script:LogFile"
Update-HardwareView
if ($script:NonInteractive) {
try {
Initialize-DeployCenterAutomatedInstall
$script:btnDeploy.PerformClick()
} catch {
& $script:SourceLogAction "Unable to prepare non-interactive mode: $($_.Exception.Message)" 'ERROR'
$script:ExitCode = 1
$script:form.Close()
}
}
})
$script:form.Add_FormClosed({
if ($null -ne $script:pictureLogo -and $null -ne $script:pictureLogo.Image) {
$script:pictureLogo.Image.Dispose()
}
$script:FontRegular.Dispose()
$script:FontSmall.Dispose()
$script:FontTitle.Dispose()
$script:FontSubtitle.Dispose()
$script:FontGroup.Dispose()
})
# ============================================================
# STARTUP
# ============================================================
try {
[void]$script:form.ShowDialog()
}
finally {
if ($script:SelectedWindowsEdition -and $script:SelectedWindowsEdition.MountedImage) {
try { [void](Dismount-DeployCenterInstallImage -Image $script:SelectedWindowsEdition) } catch {
& $script:SourceLogAction "Unable to dismount the ISO on close: $($_.Exception.Message)" 'WARNING'
}
}
if ($script:EnableVnc -and (Get-Command Stop-DeployCenterVnc -ErrorAction SilentlyContinue)) {
Stop-DeployCenterVnc
}
$script:form.Dispose()
if ($script:DeploymentAttempted) {
Copy-DeployCenterLog -DestinationRoot 'C:\'
}
if ($script:RestartAfterDeployment) {
try {
Restart-DeployCenterWinPE -LogAction $script:SourceLogAction
} catch {
& $script:SourceLogAction "Failed to request restart: $($_.Exception.Message)" 'ERROR'
}
}
}
if ($script:NonInteractive) { exit [int]$script:ExitCode }