Files
DeployCenter/DeployCenter.ps1
T

1154 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"
$script:LogRoot = Join-Path $script:DeployRoot "Logs"
# 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 -LogRoot $script:LogRoot -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 }