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