# 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 }