1154 lines
55 KiB
PowerShell
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 }
|