1818 lines
79 KiB
PowerShell
1818 lines
79 KiB
PowerShell
# DeployCenter - Interface WinPE
|
||
# Compatible Windows PowerShell 5.1
|
||
|
||
param(
|
||
# Affiche DeployCenter dans une fenetre redimensionnable pour le debug.
|
||
[switch]$Windowed,
|
||
[switch]$Vnc
|
||
)
|
||
|
||
$script:Windowed = [bool]$Windowed
|
||
$ErrorActionPreference = "Stop"
|
||
|
||
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
|
||
Import-Module (Join-Path $PSScriptRoot "Modules\UI.psm1") -Force
|
||
|
||
# GroupBox dessine par DeployCenter pour que le fond et le cadre suivent les 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
|
||
}
|
||
|
||
# ============================================================
|
||
# CHEMINS
|
||
# ============================================================
|
||
|
||
$script:DeployRoot = $PSScriptRoot
|
||
# Racine du lecteur qui heberge DeployCenter (ex. Y:\), sans lecteur code en dur.
|
||
$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:HardwareScript = Join-Path $script:DeployRoot "HardwareInfo.ps1"
|
||
$script:AssetsRoot = Join-Path $script:DeployRoot "Assets"
|
||
$script:LogoPath = Join-Path $script:AssetsRoot "logo.png"
|
||
$script:LogoDarkPath = Join-Path $script:AssetsRoot "logo-dark.png"
|
||
$script:LogRoot = Join-Path $script:DeployRoot "Logs"
|
||
|
||
# Détection réseau centralisée dans Modules\\Network.psm1.
|
||
$networkModulePath = Join-Path $PSScriptRoot "Modules\\Network.psm1"
|
||
if (-not (Test-Path -LiteralPath $networkModulePath -PathType Leaf)) {
|
||
throw "Module réseau introuvable : $networkModulePath"
|
||
}
|
||
. ([scriptblock]::Create([System.IO.File]::ReadAllText($networkModulePath)))
|
||
$script:DeployIPv4 = Get-DeployNetworkIPv4
|
||
|
||
# La gestion des journaux est centralisee dans Modules\\Logging.psm1.
|
||
$loggingModulePath = Join-Path $PSScriptRoot "Modules\\Logging.psm1"
|
||
if (-not (Test-Path -LiteralPath $loggingModulePath -PathType Leaf)) {
|
||
throw "Module de journalisation introuvable : $loggingModulePath"
|
||
}
|
||
. ([scriptblock]::Create([System.IO.File]::ReadAllText($loggingModulePath)))
|
||
Initialize-DeployCenterLogging -LogRoot $script:LogRoot -IPv4 $script:DeployIPv4
|
||
$script:HardwareJson = Join-Path $env:TEMP "HardwareInfo.json"
|
||
|
||
# Inventaire de session et UltraVNC portable : tout reste dans le dossier temporaire.
|
||
$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
|
||
|
||
# ============================================================
|
||
# THEMES EXTERNES (Themes\*.psd1)
|
||
# Les thèmes sont des fichiers de données ; aucun code PowerShell n'est exécuté.
|
||
# ============================================================
|
||
|
||
$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 en lecture seule : aucune ecriture automatique)
|
||
# ============================================================
|
||
|
||
$script:ConfigPath = Join-Path $script:DeployRoot "DeployCenter.ini"
|
||
$script:Config = @{
|
||
Theme = "Light"
|
||
SourcePaths = "%DEPLOY_VOLUME%"
|
||
VncPassword = ""
|
||
}
|
||
|
||
function Read-DeployCenterConfig {
|
||
if (-not (Test-Path -LiteralPath $script:ConfigPath -PathType Leaf)) { return }
|
||
$section = ""
|
||
foreach ($line in (Get-Content -LiteralPath $script:ConfigPath -ErrorAction SilentlyContinue)) {
|
||
$trimmed = $line.Trim()
|
||
if (-not $trimmed -or $trimmed.StartsWith(";") -or $trimmed.StartsWith("#")) { continue }
|
||
if ($trimmed -match '^\[(.+)\]$') { $section = $matches[1].Trim(); continue }
|
||
if ($trimmed -match '^([^=]+)=(.*)$') {
|
||
$key = $matches[1].Trim()
|
||
$value = $matches[2].Trim()
|
||
if ($section -ieq "General" -and $key -ieq "Theme") {
|
||
if ($value -match '^[A-Za-z0-9_-]+$' -and (Test-Path -LiteralPath (Join-Path $script:ThemesRoot ($value + ".psd1")) -PathType Leaf)) {
|
||
$script:Config.Theme = $value
|
||
}
|
||
}
|
||
elseif ($section -ieq "Sources" -and $key -ieq "Paths") {
|
||
$script:Config.SourcePaths = $value
|
||
}
|
||
elseif ($section -ieq "UltraVNC" -and $key -ieq "Password") {
|
||
# Mot de passe facultatif pour les tests ; s'il est renseigné, il remplace la génération aléatoire.
|
||
$script:Config.VncPassword = $value
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
# Themes par defaut : crees uniquement si absents, afin de faciliter les personnalisations.
|
||
$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
|
||
}
|
||
}
|
||
|
||
Read-DeployCenterConfig
|
||
$availableThemes = @(Get-DeployCenterThemeNames -ThemesRoot $script:ThemesRoot)
|
||
if ($availableThemes.Count -eq 0) { throw "Aucun thème valide trouvé dans $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"
|
||
}
|
||
# ============================================================
|
||
# POLICES
|
||
# ============================================================
|
||
|
||
$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)
|
||
|
||
# ============================================================
|
||
# RESEAU, ULTRAVNC ET INVENTAIRE DE SESSION
|
||
# ============================================================
|
||
|
||
function Write-DeployInventory {
|
||
param($HardwareInfo = $null)
|
||
try {
|
||
if (-not (Test-Path -LiteralPath $script:InventoryDir -PathType Container)) {
|
||
New-Item -Path $script:InventoryDir -ItemType Directory -Force | Out-Null
|
||
}
|
||
if ($null -ne $HardwareInfo) { $script:HardwareInventory = $HardwareInfo }
|
||
$inventory = [ordered]@{
|
||
GeneratedAt = (Get-Date).ToString('o')
|
||
DeploymentIPv4 = $script:DeployIPv4
|
||
VncPassword = $script:VncPassword
|
||
Hardware = $script:HardwareInventory
|
||
}
|
||
$json = $inventory | ConvertTo-Json -Depth 8
|
||
Set-Content -LiteralPath $script:InventoryFile -Value $json -Encoding UTF8
|
||
} catch {
|
||
Write-Log "Impossible d'écrire l'inventaire temporaire : $($_.Exception.Message)" 'WARNING'
|
||
}
|
||
}
|
||
|
||
|
||
function Set-DeployCenterFormSizing {
|
||
param(
|
||
[Parameter(Mandatory=$true)][System.Windows.Forms.Form]$Form,
|
||
[switch]$Resizable
|
||
)
|
||
if ($Resizable) {
|
||
$Form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::Sizable
|
||
$Form.MaximizeBox = $true
|
||
} else {
|
||
$Form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
|
||
$Form.MaximizeBox = $false
|
||
}
|
||
}
|
||
|
||
function Add-SessionFooter {
|
||
param(
|
||
[Parameter(Mandatory=$true)][System.Windows.Forms.Form]$TargetForm,
|
||
[bool]$ShowFooter = $true,
|
||
[bool]$ShowPassword = $true
|
||
)
|
||
if (-not $ShowFooter) { return }
|
||
|
||
$footer = New-Object System.Windows.Forms.Label
|
||
$footer.Name = 'DeployCenterSessionFooter'
|
||
$footer.Text = if ($script:DeployIPv4) {
|
||
"IP de déploiement : $($script:DeployIPv4)"
|
||
} else {
|
||
'IP de déploiement indisponible'
|
||
}
|
||
if ($ShowPassword -and -not [string]::IsNullOrWhiteSpace([string]$script:VncPassword)) {
|
||
$footer.Text += " | Mot de passe VNC : $($script:VncPassword)"
|
||
}
|
||
$footer.Height = 26
|
||
$footer.Dock = [System.Windows.Forms.DockStyle]::Bottom
|
||
$footer.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
|
||
$footer.Padding = New-Object System.Windows.Forms.Padding(12, 0, 0, 0)
|
||
$footer.BackColor = $script:Theme.PanelAlt
|
||
$footer.ForeColor = $script:Theme.Text
|
||
$footer.Font = $script:FontSmall
|
||
$TargetForm.Controls.Add($footer)
|
||
$footer.BringToFront()
|
||
}
|
||
|
||
# L'adresse IPv4 a été détectée par Modules\\Network.psm1 avant l'initialisation du journal.
|
||
|
||
# VNC est désactivé par défaut ; il démarre uniquement avec le paramètre -vnc.
|
||
$script:EnableVnc = [bool]$Vnc
|
||
if ($script:EnableVnc) {
|
||
try {
|
||
# Le code VNC est exécuté dans le scope principal uniquement avec -vnc.
|
||
$vncModulePath = Join-Path $PSScriptRoot "Modules\VNC.psm1"
|
||
if (-not (Test-Path -LiteralPath $vncModulePath -PathType Leaf)) {
|
||
throw "Module VNC introuvable : $vncModulePath"
|
||
}
|
||
$vncCode = [System.IO.File]::ReadAllText($vncModulePath)
|
||
. ([scriptblock]::Create($vncCode))
|
||
Start-DeployCenterVnc
|
||
} catch {
|
||
Write-Log "Impossible de démarrer VNC : $($_.Exception.Message)" 'ERROR'
|
||
}
|
||
} else {
|
||
Write-Log "UltraVNC non démarré : paramètre -vnc absent."
|
||
}
|
||
|
||
# ============================================================
|
||
# CREATION DES CONTROLES
|
||
# ============================================================
|
||
|
||
function New-Label {
|
||
param(
|
||
[string]$Text,
|
||
[int]$X,
|
||
[int]$Y,
|
||
[int]$Width = 180,
|
||
[int]$Height = 24,
|
||
$Color = $null,
|
||
$Font = $null
|
||
)
|
||
|
||
$label = New-Object System.Windows.Forms.Label
|
||
$label.Text = $Text
|
||
$label.Location = New-Object System.Drawing.Point($X, $Y)
|
||
$label.Size = New-Object System.Drawing.Size($Width, $Height)
|
||
$label.AutoEllipsis = $true
|
||
$label.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
|
||
$label.BackColor = [System.Drawing.Color]::Transparent
|
||
|
||
if ($null -eq $Color) {
|
||
$label.ForeColor = $script:Theme.Text
|
||
}
|
||
else {
|
||
$label.ForeColor = $Color
|
||
}
|
||
|
||
if ($null -eq $Font) {
|
||
$label.Font = $script:FontRegular
|
||
}
|
||
else {
|
||
$label.Font = $Font
|
||
}
|
||
|
||
return $label
|
||
}
|
||
|
||
function New-GroupBox {
|
||
param(
|
||
[string]$Text,
|
||
[int]$X,
|
||
[int]$Y,
|
||
[int]$Width,
|
||
[int]$Height
|
||
)
|
||
|
||
$group = New-Object DeployCenter.ThemedGroupBox
|
||
$group.Text = $Text
|
||
$group.Location = New-Object System.Drawing.Point($X, $Y)
|
||
$group.Size = New-Object System.Drawing.Size($Width, $Height)
|
||
$group.ForeColor = $script:Theme.Text
|
||
$group.BackColor = $script:Theme.Panel
|
||
$group.PanelColor = $script:Theme.Panel
|
||
$group.BorderColor = $script:Theme.Border
|
||
$group.TextColor = $script:Theme.Text
|
||
$group.Font = $script:FontGroup
|
||
return $group
|
||
}
|
||
|
||
function New-Button {
|
||
param(
|
||
[string]$Text,
|
||
[int]$X,
|
||
[int]$Y,
|
||
[int]$Width,
|
||
[int]$Height,
|
||
[ValidateSet("Button", "PrimaryButton", "DangerButton")][string]$Role = "Button"
|
||
)
|
||
|
||
$button = New-Object System.Windows.Forms.Button
|
||
$button.Text = $Text
|
||
$button.Location = New-Object System.Drawing.Point($X, $Y)
|
||
$button.Size = New-Object System.Drawing.Size($Width, $Height)
|
||
$button.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
|
||
$button.FlatAppearance.BorderSize = 1
|
||
$button.Tag = $Role
|
||
Set-DeployCenterButtonStyle -Button $button -Theme $script:Theme -Role $Role
|
||
$button.Font = $script:FontRegular
|
||
$button.Cursor = [System.Windows.Forms.Cursors]::Hand
|
||
$button.UseVisualStyleBackColor = $false
|
||
$button.AutoSize = $false
|
||
$button.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
|
||
return $button
|
||
}
|
||
|
||
# ============================================================
|
||
# FENETRE PRINCIPALE
|
||
# ============================================================
|
||
|
||
$script:form = New-Object System.Windows.Forms.Form
|
||
$script:form.Text = "DeployCenter"
|
||
$script:form.BackColor = $script:Theme.Background
|
||
if ($script:Windowed) {
|
||
# Mode debug : fenetre classique, deplacable et reductible.
|
||
$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 {
|
||
# Mode normal : plein ecran, sans bordure et au premier plan.
|
||
$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)
|
||
|
||
# ============================================================
|
||
# EN-TETE ET LOGO FACULTATIF
|
||
# ============================================================
|
||
|
||
$script:pictureLogo = $null
|
||
$titleX = 30
|
||
|
||
# Deux fichiers seulement : logo.png (clair et solution de repli) et logo-dark.png.
|
||
# En mode sombre, logo-dark.png est prioritaire ; s'il manque, logo.png est utilise.
|
||
# En mode clair, logo.png est utilise.
|
||
function Get-LogoPathForTheme {
|
||
param([string]$ThemeName)
|
||
|
||
if ($ThemeName -eq "Dark" -and (Test-Path $script:LogoDarkPath)) {
|
||
return $script:LogoDarkPath
|
||
}
|
||
if (Test-Path $script:LogoPath) {
|
||
return $script:LogoPath
|
||
}
|
||
# Si logo.png manque, garder logo-dark.png comme dernier recours.
|
||
if (Test-Path $script:LogoDarkPath) {
|
||
return $script:LogoDarkPath
|
||
}
|
||
return $null
|
||
}
|
||
|
||
function Update-DeployCenterLogo {
|
||
param([string]$ThemeName)
|
||
|
||
if ($null -eq $script:pictureLogo) { return }
|
||
$logoPath = Get-LogoPathForTheme -ThemeName $ThemeName
|
||
if ([string]::IsNullOrWhiteSpace($logoPath)) { return }
|
||
|
||
try {
|
||
$newImage = [System.Drawing.Image]::FromFile($logoPath)
|
||
$oldImage = $script:pictureLogo.Image
|
||
$script:pictureLogo.Image = $newImage
|
||
if ($null -ne $oldImage) { $oldImage.Dispose() }
|
||
$script:pictureLogo.Visible = $true
|
||
}
|
||
catch {
|
||
# En cas d'image absente ou invalide, conserver le logo deja affiche.
|
||
}
|
||
}
|
||
|
||
$initialLogoPath = Get-LogoPathForTheme -ThemeName $script:CurrentTheme
|
||
if ($null -ne $initialLogoPath) {
|
||
try {
|
||
$script:pictureLogo = New-Object System.Windows.Forms.PictureBox
|
||
$script:pictureLogo.Location = New-Object System.Drawing.Point(30, 20)
|
||
$script:pictureLogo.Size = New-Object System.Drawing.Size(64, 64)
|
||
$script:pictureLogo.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom
|
||
$script:pictureLogo.BackColor = [System.Drawing.Color]::Transparent
|
||
$script:pictureLogo.Image = [System.Drawing.Image]::FromFile($initialLogoPath)
|
||
$script:form.Controls.Add($script:pictureLogo)
|
||
$titleX = 110
|
||
}
|
||
catch {
|
||
$script:pictureLogo = $null
|
||
$titleX = 30
|
||
}
|
||
}
|
||
|
||
$script:lblTitle = New-Label -Text "DeployCenter" `
|
||
-X $titleX -Y 17 -Width 600 -Height 42 `
|
||
-Color $script:Theme.Text -Font $script:FontTitle
|
||
|
||
$script:lblSubtitle = New-Label `
|
||
-Text "Environnement de déploiement Windows" `
|
||
-X ($titleX + 4) -Y 61 -Width 650 -Height 24 `
|
||
-Color $script:Theme.Secondary -Font $script:FontSubtitle
|
||
|
||
$script:form.Controls.Add($script:lblTitle)
|
||
$script:form.Controls.Add($script:lblSubtitle)
|
||
|
||
# Menu de selection du theme ; les noms correspondent aux fichiers Themes\<Nom>.psd1
|
||
$script:cmbTheme = New-Object System.Windows.Forms.ComboBox
|
||
$script:cmbTheme.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
|
||
# OwnerDraw permet de colorer aussi la liste deroulante, que WinForms laisse sinon au theme systeme.
|
||
$script:cmbTheme.DrawMode = [System.Windows.Forms.DrawMode]::OwnerDrawFixed
|
||
$script:cmbTheme.ItemHeight = 24
|
||
$script:cmbTheme.Font = $script:FontRegular
|
||
$script:cmbTheme.Width = 190
|
||
$script:cmbTheme.Height = 32
|
||
$script:cmbTheme.Location = New-Object System.Drawing.Point(0, 29)
|
||
$script:cmbTheme.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Right
|
||
foreach ($themeName in (Get-DeployCenterThemeNames -ThemesRoot $script:ThemesRoot)) { [void]$script:cmbTheme.Items.Add($themeName) }
|
||
$script:cmbTheme.SelectedItem = $script:CurrentTheme
|
||
$script:cmbTheme.BackColor = $script:Theme.Panel
|
||
$script:cmbTheme.ForeColor = $script:Theme.Text
|
||
$script:cmbTheme.Add_DrawItem({
|
||
param($sender, $e)
|
||
if ($e.Index -lt 0) {
|
||
$itemText = [string]$sender.Text
|
||
} else {
|
||
$itemText = [string]$sender.Items[$e.Index]
|
||
}
|
||
$isSelected = (($e.State -band [System.Windows.Forms.DrawItemState]::Selected) -ne 0)
|
||
if ($isSelected) {
|
||
$backgroundColor = $script:Theme.Accent
|
||
$foregroundColor = [System.Drawing.Color]::White
|
||
} else {
|
||
$backgroundColor = $script:Theme.Panel
|
||
$foregroundColor = $script:Theme.Text
|
||
}
|
||
$backgroundBrush = New-Object System.Drawing.SolidBrush($backgroundColor)
|
||
try { $e.Graphics.FillRectangle($backgroundBrush, $e.Bounds) }
|
||
finally { $backgroundBrush.Dispose() }
|
||
if (-not [string]::IsNullOrEmpty($itemText)) {
|
||
$textBounds = $e.Bounds
|
||
$textBounds.X += 4
|
||
[System.Windows.Forms.TextRenderer]::DrawText(
|
||
$e.Graphics, $itemText, $sender.Font, $textBounds, $foregroundColor,
|
||
([System.Windows.Forms.TextFormatFlags]::Left -bor [System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor [System.Windows.Forms.TextFormatFlags]::EndEllipsis)
|
||
)
|
||
}
|
||
$e.DrawFocusRectangle()
|
||
})
|
||
$script:form.Controls.Add($script:cmbTheme)
|
||
$script:cmbTheme.BringToFront()
|
||
|
||
# ============================================================
|
||
# INFORMATIONS DU PC
|
||
# ============================================================
|
||
|
||
$script:groupPC = New-GroupBox -Text "Informations du PC" `
|
||
-X $margin -Y 100 -Width $usableWidth -Height 105
|
||
|
||
$script:lblManufacturer = New-Label -Text "Fabricant : -" `
|
||
-X 18 -Y 25 -Width 330 -Height 24
|
||
$script:lblModel = New-Label -Text "Modèle : -" `
|
||
-X 360 -Y 25 -Width 400 -Height 24
|
||
$script:lblSerial = New-Label -Text "Numéro de série : -" `
|
||
-X 18 -Y 52 -Width 330 -Height 24
|
||
$script:lblFirmware = New-Label -Text "Firmware : -" `
|
||
-X 360 -Y 52 -Width 220 -Height 24
|
||
$script:lblArchitecture = New-Label -Text "Architecture : -" `
|
||
-X 600 -Y 52 -Width 220 -Height 24
|
||
$script:lblWinPE = New-Label -Text "Environnement : -" `
|
||
-X 18 -Y 78 -Width 350 -Height 22 `
|
||
-Color $script:Theme.Secondary -Font $script:FontSmall
|
||
|
||
$script:groupPC.Controls.AddRange(@(
|
||
$script:lblManufacturer,
|
||
$script:lblModel,
|
||
$script:lblSerial,
|
||
$script:lblFirmware,
|
||
$script:lblArchitecture,
|
||
$script:lblWinPE
|
||
))
|
||
$script:form.Controls.Add($script:groupPC)
|
||
|
||
# ============================================================
|
||
# DISQUE PHYSIQUE - SELECTEUR COMPACT
|
||
# ============================================================
|
||
|
||
$script:groupDisks = New-GroupBox -Text "Disque physique" `
|
||
-X $margin -Y 220 -Width $usableWidth -Height 78
|
||
|
||
$script:cmbDisk = New-Object System.Windows.Forms.ComboBox
|
||
$script:cmbDisk.Name = "cmbDisk"
|
||
$script:cmbDisk.Location = New-Object System.Drawing.Point(12, 30)
|
||
$script:cmbDisk.Size = New-Object System.Drawing.Size(($usableWidth - 24), 28)
|
||
$script:cmbDisk.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
|
||
$script:cmbDisk.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
|
||
$script:cmbDisk.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
|
||
$script:cmbDisk.BackColor = $script:Theme.PanelAlt
|
||
$script:cmbDisk.ForeColor = $script:Theme.Text
|
||
$script:cmbDisk.Font = $script:FontSmall
|
||
$script:cmbDisk.IntegralHeight = $false
|
||
$script:cmbDisk.MaxDropDownItems = 12
|
||
$script:groupDisks.Controls.Add($script:cmbDisk)
|
||
$script:form.Controls.Add($script:groupDisks)
|
||
|
||
function Set-DiskSelectorWidth {
|
||
if ($null -eq $script:cmbDisk -or $null -eq $script:groupDisks) { return }
|
||
$script:cmbDisk.Location = New-Object System.Drawing.Point(12, 30)
|
||
$script:cmbDisk.Size = New-Object System.Drawing.Size([Math]::Max(250, $script:groupDisks.ClientSize.Width - 24), 28)
|
||
}
|
||
|
||
# ============================================================
|
||
# JOURNAL
|
||
# ============================================================
|
||
|
||
$logY = $script:groupDisks.Bottom + 15
|
||
$buttonHeight = 38
|
||
# En plein ecran, rapprocher les boutons du bas sans empiéter sur le pied de page.
|
||
# En mode fenetre, conserver la position actuelle.
|
||
$buttonY = if ($script:Windowed) {
|
||
$screenHeight - 54 - $buttonHeight
|
||
} else {
|
||
$screenHeight - 42 - $buttonHeight
|
||
}
|
||
$logHeight = [math]::Max(100, $buttonY - $logY - 15)
|
||
|
||
$script:groupLog = New-GroupBox -Text "Journal" `
|
||
-X $margin -Y $logY -Width $usableWidth -Height $logHeight
|
||
|
||
$script:txtLog = New-Object System.Windows.Forms.TextBox
|
||
$script:txtLog.Location = New-Object System.Drawing.Point(12, 28)
|
||
$script:txtLog.Size = New-Object System.Drawing.Size(
|
||
($usableWidth - 28), ($logHeight - 44)
|
||
)
|
||
$script:txtLog.Multiline = $true
|
||
$script:txtLog.ReadOnly = $true
|
||
$script:txtLog.WordWrap = $false
|
||
$script:txtLog.ScrollBars = [System.Windows.Forms.ScrollBars]::Both
|
||
$script:txtLog.BackColor = $script:Theme.PanelAlt
|
||
$script:txtLog.ForeColor = $script:Theme.Text
|
||
$script:txtLog.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle
|
||
$script:txtLog.Font = [System.Drawing.Font]::new("Consolas", [single]9, [System.Drawing.FontStyle]::Regular)
|
||
$script:groupLog.Controls.Add($script:txtLog)
|
||
$script:form.Controls.Add($script:groupLog)
|
||
|
||
# ============================================================
|
||
# BOUTONS ACTUALISER ET FERMER
|
||
# ============================================================
|
||
|
||
$buttonWidth = 140
|
||
$buttonGap = 12
|
||
$closeX = $screenWidth - $margin - $buttonWidth
|
||
$refreshX = $closeX - $buttonGap - $buttonWidth
|
||
$sourcesX = $refreshX - $buttonGap - 170
|
||
|
||
$script:btnSources = New-Button -Text "Sources Windows" `
|
||
-X $sourcesX -Y $buttonY -Width 170 -Height $buttonHeight `
|
||
-Role "Button"
|
||
$script:btnSources.ForeColor = $script:Theme.Text
|
||
|
||
$script:btnRefresh = New-Button -Text "Actualiser" `
|
||
-X $refreshX -Y $buttonY -Width $buttonWidth -Height $buttonHeight `
|
||
-Role "PrimaryButton"
|
||
|
||
$script:btnClose = New-Button -Text "Fermer" `
|
||
-X $closeX -Y $buttonY -Width $buttonWidth -Height $buttonHeight `
|
||
-Role "DangerButton"
|
||
|
||
$script:form.Controls.Add($script:btnSources)
|
||
$script:form.Controls.Add($script:btnRefresh)
|
||
$script:form.Controls.Add($script:btnClose)
|
||
Add-SessionFooter -TargetForm $script:form -ShowPassword $true
|
||
|
||
# ============================================================
|
||
# APPLICATION DU THEME A TOUS LES CONTROLES
|
||
# ============================================================
|
||
|
||
function Set-ControlTheme {
|
||
param([System.Windows.Forms.Control]$Control)
|
||
|
||
# Le formulaire principal doit lui aussi changer de fond.
|
||
if ($Control -is [System.Windows.Forms.Form]) {
|
||
$Control.BackColor = $script:Theme.Background
|
||
$Control.ForeColor = $script:Theme.Text
|
||
}
|
||
elseif ($Control -is [DeployCenter.ThemedGroupBox]) {
|
||
$Control.BackColor = $script:Theme.Panel
|
||
$Control.PanelColor = $script:Theme.Panel
|
||
$Control.BorderColor = $script:Theme.Border
|
||
$Control.TextColor = $script:Theme.Text
|
||
$Control.ForeColor = $script:Theme.Text
|
||
$Control.Invalidate()
|
||
}
|
||
elseif ($Control -is [System.Windows.Forms.Label]) {
|
||
$Control.ForeColor = $script:Theme.Text
|
||
# Le pied de page VNC suit lui aussi la palette du thème actif.
|
||
if ($Control.Name -eq 'DeployCenterSessionFooter') {
|
||
$Control.BackColor = $script:Theme.PanelAlt
|
||
$Control.ForeColor = $script:Theme.Secondary
|
||
$Control.Invalidate()
|
||
}
|
||
}
|
||
elseif ($Control -is [System.Windows.Forms.ListView]) {
|
||
$Control.BackColor = $script:Theme.PanelAlt
|
||
$Control.ForeColor = $script:Theme.Text
|
||
}
|
||
elseif ($Control -is [System.Windows.Forms.TextBox]) {
|
||
$Control.BackColor = $script:Theme.PanelAlt
|
||
$Control.ForeColor = $script:Theme.Text
|
||
}
|
||
|
||
foreach ($child in $Control.Controls) {
|
||
Set-ControlTheme -Control $child
|
||
}
|
||
}
|
||
|
||
function Set-DeployCenterTheme {
|
||
param([Parameter(Mandatory=$true)][string]$ThemeName)
|
||
|
||
$availableThemes = @(Get-DeployCenterThemeNames -ThemesRoot $script:ThemesRoot)
|
||
if ($availableThemes -notcontains $ThemeName) { return }
|
||
try {
|
||
$script:Theme = Import-DeployCenterTheme -ThemesRoot $script:ThemesRoot -ThemeName $ThemeName
|
||
} catch {
|
||
Write-Log ("Impossible de charger le theme '$ThemeName' : " + $_.Exception.Message) "ERROR"
|
||
return
|
||
}
|
||
$script:CurrentTheme = $ThemeName
|
||
$script:cmbTheme.BackColor = $script:Theme.Panel
|
||
$script:cmbTheme.ForeColor = $script:Theme.Text
|
||
$script:cmbTheme.Invalidate()
|
||
$script:cmbTheme.Refresh()
|
||
|
||
Set-ControlTheme -Control $script:form
|
||
|
||
# Met a jour le logo si une variante propre au theme existe.
|
||
Update-DeployCenterLogo -ThemeName $script:CurrentTheme
|
||
|
||
$script:lblTitle.ForeColor = $script:Theme.Text
|
||
$script:lblSubtitle.ForeColor = $script:Theme.Secondary
|
||
|
||
if ($script:lblWinPE.Text -like "*WinPE*") {
|
||
$script:lblWinPE.ForeColor = $script:Theme.Success
|
||
}
|
||
else {
|
||
$script:lblWinPE.ForeColor = $script:Theme.Secondary
|
||
}
|
||
|
||
Set-DeployCenterButtonStyle -Button $script:btnSources -Theme $script:Theme -Role 'Button'
|
||
Set-DeployCenterButtonStyle -Button $script:btnRefresh -Theme $script:Theme -Role 'PrimaryButton'
|
||
Set-DeployCenterButtonStyle -Button $script:btnClose -Theme $script:Theme -Role 'DangerButton'
|
||
# Actualise explicitement le pied de page principal lors du changement de thème.
|
||
foreach ($footer in $script:form.Controls.Find('DeployCenterSessionFooter', $true)) {
|
||
$footer.BackColor = $script:Theme.PanelAlt
|
||
$footer.ForeColor = $script:Theme.Secondary
|
||
$footer.Invalidate()
|
||
}
|
||
$script:cmbTheme.BackColor = $script:Theme.Panel
|
||
$script:cmbTheme.ForeColor = $script:Theme.Text
|
||
$script:cmbTheme.Invalidate()
|
||
$script:groupPC.Invalidate()
|
||
$script:groupDisks.Invalidate()
|
||
$script:groupLog.Invalidate()
|
||
$script:cmbDisk.BackColor = $script:Theme.PanelAlt
|
||
$script:cmbDisk.ForeColor = $script:Theme.Text
|
||
$script:cmbDisk.Invalidate()
|
||
# Le bouton reste au-dessus des autres controles de l'en-tete.
|
||
$script:cmbTheme.BringToFront()
|
||
|
||
# Forcer le rafraichissement apres le changement de palette.
|
||
$script:form.SuspendLayout()
|
||
try {
|
||
foreach ($control in $script:form.Controls) {
|
||
$control.Invalidate($true)
|
||
}
|
||
$script:form.Invalidate($true)
|
||
$script:form.Refresh()
|
||
}
|
||
finally {
|
||
$script:form.ResumeLayout($true)
|
||
}
|
||
}
|
||
|
||
# ============================================================
|
||
# DETECTION DES SOURCES WINDOWS SUR PARTAGE SMB
|
||
# ============================================================
|
||
|
||
function Find-WindowsSources {
|
||
param([Parameter(Mandatory=$true)][string]$RootPath)
|
||
|
||
$results = New-Object System.Collections.Generic.List[object]
|
||
$roots = @($RootPath -split ';' | ForEach-Object {
|
||
$candidate = $_.Trim().Trim('"')
|
||
if ($candidate -notmatch '^[A-Za-z]:\\$') { $candidate = $candidate.TrimEnd('\') }
|
||
$candidate
|
||
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
|
||
if ($roots.Count -eq 0) {
|
||
throw 'Indique au moins un chemin local et/ou un partage SMB, separes par un point-virgule.'
|
||
}
|
||
|
||
foreach ($root in $roots) {
|
||
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
|
||
Write-Log "Source inaccessible, ignoree : $root" 'WARNING'
|
||
continue
|
||
}
|
||
|
||
Write-Log "Recherche des sources Windows dans : $root"
|
||
# Parcours en lecture seule. Les erreurs d'acces aux sous-dossiers sont ignorees.
|
||
# Afficher les ISO et les fichiers WIM/ESD autonomes.
|
||
# Exception : masquer boot.wim uniquement lorsqu'il fait partie d'un
|
||
# support Windows extrait dont le même dossier Sources contient install.wim/esd.
|
||
$allFiles = @(Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue)
|
||
$files = New-Object System.Collections.Generic.List[object]
|
||
foreach ($file in $allFiles) {
|
||
if ($file.Extension -ieq '.iso') {
|
||
[void]$files.Add($file)
|
||
continue
|
||
}
|
||
|
||
if ($file.Extension -ine '.wim' -and $file.Extension -ine '.esd') {
|
||
continue
|
||
}
|
||
|
||
$isBootWimInExtractedMedia = $false
|
||
if ($file.Name -ieq 'boot.wim' -and $file.Directory.Name -ieq 'Sources') {
|
||
$installWim = Join-Path $file.Directory.FullName 'install.wim'
|
||
$installEsd = Join-Path $file.Directory.FullName 'install.esd'
|
||
if ((Test-Path -LiteralPath $installWim -PathType Leaf) -or
|
||
(Test-Path -LiteralPath $installEsd -PathType Leaf)) {
|
||
$isBootWimInExtractedMedia = $true
|
||
}
|
||
}
|
||
|
||
if (-not $isBootWimInExtractedMedia) {
|
||
[void]$files.Add($file)
|
||
}
|
||
}
|
||
|
||
foreach ($file in $files) {
|
||
$kind = switch ($file.Extension.ToLowerInvariant()) {
|
||
'.iso' { 'Image ISO' }
|
||
'.wim' { 'Image WIM Windows' }
|
||
'.esd' { 'Image ESD Windows' }
|
||
}
|
||
$detail = if ($file.Name -ieq 'install.wim' -or $file.Name -ieq 'install.esd') {
|
||
"Image d’installation dans un support extrait"
|
||
} elseif ($file.Extension -ieq '.iso') {
|
||
"Support d’installation ISO"
|
||
} else {
|
||
"Image WIM/ESD autonome"
|
||
}
|
||
$results.Add([pscustomobject]@{
|
||
Type = $kind; Nom = $file.Name; Chemin = $file.FullName
|
||
Taille = ('{0:N2} Go' -f ($file.Length / 1GB)); Detail = $detail
|
||
})
|
||
}
|
||
}
|
||
|
||
if (-not ($roots | Where-Object { Test-Path -LiteralPath $_ -PathType Container })) {
|
||
throw "Aucun des chemins n'est accessible : $($roots -join '; ')`nVerifie que le lecteur local et le partage SMB sont accessibles."
|
||
}
|
||
Write-Log ("Detection terminee : {0} element(s) trouve(s)." -f $results.Count) 'SUCCESS'
|
||
return @($results | Sort-Object Type, Nom, Chemin -Unique)
|
||
}
|
||
|
||
function Get-WindowsImageEditions {
|
||
param([Parameter(Mandatory=$true)][string]$ImagePath)
|
||
|
||
if (-not (Test-Path -LiteralPath $ImagePath -PathType Leaf)) {
|
||
throw "Fichier image introuvable : $ImagePath"
|
||
}
|
||
|
||
$normalized = @()
|
||
$getWindowsImage = Get-Command Get-WindowsImage -ErrorAction SilentlyContinue
|
||
if ($getWindowsImage) {
|
||
try {
|
||
$images = @(Get-WindowsImage -ImagePath $ImagePath -ErrorAction Stop)
|
||
foreach ($image in $images) {
|
||
$indexValue = $null
|
||
foreach ($propertyName in @('ImageIndex', 'Index')) {
|
||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||
$indexValue = [int]$image.$propertyName
|
||
break
|
||
}
|
||
}
|
||
if ($null -eq $indexValue) { continue }
|
||
|
||
$nameValue = $null
|
||
foreach ($propertyName in @('ImageName', 'Name', 'ImageDescription')) {
|
||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||
$nameValue = [string]$image.$propertyName
|
||
break
|
||
}
|
||
}
|
||
$archValue = $null
|
||
foreach ($propertyName in @('Architecture', 'ImageArchitecture')) {
|
||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||
$archValue = $image.$propertyName
|
||
break
|
||
}
|
||
}
|
||
$versionValue = $null
|
||
foreach ($propertyName in @('Version', 'ImageVersion')) {
|
||
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
|
||
$versionValue = [string]$image.$propertyName
|
||
break
|
||
}
|
||
}
|
||
$normalized += [pscustomobject]@{
|
||
Index = $indexValue
|
||
Name = $nameValue
|
||
Description = if ($image.PSObject.Properties['ImageDescription']) { $image.ImageDescription } else { $null }
|
||
Architecture = $archValue
|
||
Version = $versionValue
|
||
Edition = if ($image.PSObject.Properties['EditionId']) { $image.EditionId } else { $null }
|
||
}
|
||
}
|
||
} catch {
|
||
Write-Log "Get-WindowsImage indisponible pour $ImagePath : $($_.Exception.Message)" 'WARNING'
|
||
}
|
||
}
|
||
|
||
$dism = Join-Path $env:WINDIR 'System32\dism.exe'
|
||
if (-not (Test-Path -LiteralPath $dism)) { $dism = 'dism.exe' }
|
||
|
||
# Si le module PowerShell n'a pas renvoyé les index/noms, les récupérer avec DISM.
|
||
if ($normalized.Count -eq 0) {
|
||
$output = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" 2>&1
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "DISM n'a pas pu lire l'image : $ImagePath`n$($output -join "`n")"
|
||
}
|
||
|
||
$current = $null
|
||
foreach ($line in $output) {
|
||
$lineText = [string]$line
|
||
if ($lineText -match '^\s*Index\s*:\s*(\d+)\s*$') {
|
||
if ($null -ne $current) { $normalized += [pscustomobject]$current }
|
||
$current = @{ Index = [int]$Matches[1]; Name = ''; Description = ''; Architecture = $null; Version = $null; Edition = $null }
|
||
} elseif ($null -ne $current -and $lineText -match '^\s*Name\s*:\s*(.*?)\s*$') {
|
||
$current.Name = $Matches[1]
|
||
} elseif ($null -ne $current -and $lineText -match '^\s*Description\s*:\s*(.*?)\s*$') {
|
||
$current.Description = $Matches[1]
|
||
}
|
||
}
|
||
if ($null -ne $current) { $normalized += [pscustomobject]$current }
|
||
}
|
||
|
||
if ($normalized.Count -eq 0) {
|
||
throw "DISM n’a retourné aucune édition exploitable pour : $ImagePath"
|
||
}
|
||
|
||
# Interroger DISM pour chaque index même si Get-WindowsImage a fourni les noms :
|
||
# selon la version du module, les propriétés Architecture/Version peuvent être absentes.
|
||
foreach ($edition in $normalized) {
|
||
if ($edition.Architecture -and $edition.Version) { continue }
|
||
|
||
$detailOutput = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" "/Index:$($edition.Index)" 2>&1
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Log "DISM n'a pas pu lire les metadonnees de l'index $($edition.Index) : $($detailOutput -join ' ')" 'WARNING'
|
||
continue
|
||
}
|
||
|
||
$insideImageDetails = $false
|
||
$imageVersion = $null
|
||
$servicePackBuild = $null
|
||
foreach ($detailLine in $detailOutput) {
|
||
$detailText = [string]$detailLine
|
||
if ($detailText -match '^\s*Details for image\s*:') {
|
||
$insideImageDetails = $true
|
||
continue
|
||
}
|
||
# Ignorer la version de DISM affichée au début de sa sortie.
|
||
if (-not $insideImageDetails) { continue }
|
||
|
||
if (-not $edition.Architecture -and $detailText -match '^\s*Architecture\s*:\s*(.*?)\s*$') {
|
||
$edition.Architecture = $Matches[1]
|
||
} elseif ($detailText -match '^\s*Version\s*:\s*(.*?)\s*$') {
|
||
$imageVersion = $Matches[1]
|
||
} elseif ($detailText -match '^\s*ServicePack Build\s*:\s*(.*?)\s*$') {
|
||
$servicePackBuild = $Matches[1]
|
||
} elseif (-not $edition.Name -and $detailText -match '^\s*Name\s*:\s*(.*?)\s*$') {
|
||
$edition.Name = $Matches[1]
|
||
} elseif (-not $edition.Edition -and $detailText -match '^\s*Edition\s*:\s*(.*?)\s*$') {
|
||
$edition.Edition = $Matches[1]
|
||
}
|
||
}
|
||
if ($imageVersion) {
|
||
$edition.Version = if ($servicePackBuild -and $servicePackBuild -ne '0') {
|
||
"$imageVersion.$servicePackBuild"
|
||
} else {
|
||
$imageVersion
|
||
}
|
||
}
|
||
}
|
||
|
||
# Version globale de l'image : ne l'afficher que si les métadonnées des index
|
||
# donnent une seule version cohérente. Ne pas répéter cette valeur sur chaque ligne.
|
||
$versionValues = @($normalized | ForEach-Object {
|
||
if (-not [string]::IsNullOrWhiteSpace([string]$_.Version)) { [string]$_.Version }
|
||
} | Select-Object -Unique)
|
||
$globalVersion = $null
|
||
if ($versionValues.Count -eq 1) {
|
||
$globalVersion = $versionValues[0]
|
||
}
|
||
|
||
foreach ($edition in $normalized) {
|
||
if ($edition.Architecture -is [int] -or [string]$edition.Architecture -match '^\d+$') {
|
||
switch ([string]$edition.Architecture) {
|
||
'0' { $edition.Architecture = 'x86' }
|
||
'5' { $edition.Architecture = 'ARM' }
|
||
'9' { $edition.Architecture = 'x64' }
|
||
'12' { $edition.Architecture = 'ARM64' }
|
||
}
|
||
}
|
||
}
|
||
|
||
return [pscustomobject]@{
|
||
Editions = @($normalized)
|
||
GlobalVersion = $globalVersion
|
||
}
|
||
}
|
||
|
||
function Resolve-InstallImagePath {
|
||
param([Parameter(Mandatory=$true)][string]$SelectedPath)
|
||
|
||
if ([IO.Path]::GetExtension($SelectedPath) -in @('.wim', '.esd')) {
|
||
return $SelectedPath
|
||
}
|
||
|
||
if (Test-Path -LiteralPath $SelectedPath -PathType Container) {
|
||
foreach ($relative in @('sources\install.wim', 'sources\install.esd', 'Sources\install.wim', 'Sources\install.esd')) {
|
||
$candidate = Join-Path $SelectedPath $relative
|
||
if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate }
|
||
}
|
||
throw "Aucun fichier Sources\install.wim ou Sources\install.esd dans : $SelectedPath"
|
||
}
|
||
|
||
if ([IO.Path]::GetExtension($SelectedPath) -ieq '.iso') {
|
||
if (-not (Get-Command Mount-DiskImage -ErrorAction SilentlyContinue)) {
|
||
throw "Le montage ISO n'est pas disponible dans cet environnement (commande Mount-DiskImage absente). Pour analyser cette source, utilise le dossier extrait ou un fichier install.wim/esd."
|
||
}
|
||
$diskImage = Mount-DiskImage -ImagePath $SelectedPath -Access ReadOnly -PassThru -ErrorAction Stop
|
||
try {
|
||
$volumes = @($diskImage | Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter })
|
||
foreach ($volume in $volumes) {
|
||
foreach ($relative in @('sources\install.wim', 'sources\install.esd')) {
|
||
$candidate = "$($volume.DriveLetter):\$relative"
|
||
if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate }
|
||
}
|
||
}
|
||
throw "L'ISO ne contient pas de fichier Sources\install.wim ou Sources\install.esd."
|
||
} finally {
|
||
Dismount-DiskImage -ImagePath $SelectedPath -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
|
||
throw "Type de source non pris en charge pour l'analyse : $SelectedPath"
|
||
}
|
||
|
||
function Show-WindowsSourcesDialog {
|
||
$dlg = New-Object System.Windows.Forms.Form
|
||
$dlg.Text = 'Sources Windows - lecteur DeployCenter et partage SMB'
|
||
$dlg.StartPosition = 'CenterParent'
|
||
$dlg.Size = New-Object System.Drawing.Size(1100, 700)
|
||
$dlg.MinimumSize = New-Object System.Drawing.Size(700, 450)
|
||
Set-DeployCenterFormSizing -Form $dlg -Resizable
|
||
# Ouvrir directement en plein écran pour exploiter toute la surface disponible.
|
||
$dlg.WindowState = [System.Windows.Forms.FormWindowState]::Maximized
|
||
$dlg.ShowInTaskbar = $false
|
||
$dlg.BackColor = $script:Theme.Background
|
||
$dlg.ForeColor = $script:Theme.Text
|
||
$dlg.Font = $script:FontRegular
|
||
|
||
$lblPath = New-Object System.Windows.Forms.Label
|
||
$lblPath.Text = 'Chemins à analyser (séparés par ;) :'
|
||
$lblPath.Location = New-Object System.Drawing.Point(16, 16)
|
||
$lblPath.Size = New-Object System.Drawing.Size(260, 24)
|
||
$lblPath.ForeColor = $script:Theme.Text
|
||
$dlg.Controls.Add($lblPath)
|
||
|
||
$txtPath = New-Object System.Windows.Forms.TextBox
|
||
$txtPath.Location = New-Object System.Drawing.Point(16, 42)
|
||
$txtPath.Size = New-Object System.Drawing.Size(560, 26)
|
||
$txtPath.Anchor = 'Top,Left,Right'
|
||
$defaultSourceRoot = $script:DeployVolumeRoot
|
||
if ([string]::IsNullOrWhiteSpace($defaultSourceRoot)) { $defaultSourceRoot = $script:DeployRoot }
|
||
$configuredPaths = [string]$script:Config.SourcePaths
|
||
if ([string]::IsNullOrWhiteSpace($configuredPaths)) {
|
||
$txtPath.Text = $defaultSourceRoot
|
||
} else {
|
||
$txtPath.Text = $configuredPaths.Replace("%DEPLOY_VOLUME%", $defaultSourceRoot)
|
||
}
|
||
$txtPath.BackColor = $script:Theme.PanelAlt
|
||
$txtPath.ForeColor = $script:Theme.Text
|
||
$dlg.Controls.Add($txtPath)
|
||
|
||
$btnScan = New-Object System.Windows.Forms.Button
|
||
$btnScan.Text = 'Rechercher'
|
||
$btnScan.Location = New-Object System.Drawing.Point(650, 40)
|
||
$btnScan.Size = New-Object System.Drawing.Size(100, 30)
|
||
$btnScan.Anchor = 'Top,Right'
|
||
$btnScan.UseVisualStyleBackColor = $false
|
||
$btnScan.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
|
||
$btnScan.FlatAppearance.BorderColor = $script:Theme.Border
|
||
$btnScan.BackColor = $script:Theme.Accent
|
||
$btnScan.ForeColor = [System.Drawing.Color]::White
|
||
$dlg.Controls.Add($btnScan)
|
||
|
||
$btnEditions = New-Object System.Windows.Forms.Button
|
||
$btnEditions.Text = 'Afficher les éditions'
|
||
$btnEditions.Location = New-Object System.Drawing.Point(760, 40)
|
||
$btnEditions.Size = New-Object System.Drawing.Size(150, 30)
|
||
$btnEditions.Anchor = 'Top,Right'
|
||
$btnEditions.UseVisualStyleBackColor = $false
|
||
$btnEditions.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
|
||
$btnEditions.FlatAppearance.BorderColor = $script:Theme.Border
|
||
$btnEditions.BackColor = $script:Theme.Panel
|
||
$btnEditions.ForeColor = $script:Theme.Text
|
||
$dlg.Controls.Add($btnEditions)
|
||
|
||
$list = New-Object System.Windows.Forms.ListView
|
||
$list.Location = New-Object System.Drawing.Point(16, 84)
|
||
$list.Size = New-Object System.Drawing.Size(770, 420)
|
||
$list.Anchor = 'Top,Bottom,Left,Right'
|
||
$list.View = 'Details'; $list.FullRowSelect = $true; $list.GridLines = $true
|
||
$list.MultiSelect = $false
|
||
$list.HideSelection = $true
|
||
$list.BackColor = $script:Theme.PanelAlt
|
||
$list.ForeColor = $script:Theme.Text
|
||
$list.OwnerDraw = $true
|
||
[void]$list.Columns.Add('Type', 135)
|
||
[void]$list.Columns.Add('Nom', 260)
|
||
[void]$list.Columns.Add('Taille', 80)
|
||
[void]$list.Columns.Add('Chemin', 250)
|
||
|
||
# La colonne Nom est prioritaire : elle absorbe l'espace libre.
|
||
$resizeSourceColumns = {
|
||
if ($list.Columns.Count -ge 4) {
|
||
$availableWidth = $list.ClientSize.Width - $list.Columns[0].Width - $list.Columns[2].Width - $list.Columns[3].Width - 4
|
||
if ($list.Items.Count -gt 0 -and $list.Items.Count -gt [math]::Floor($list.ClientSize.Height / 20)) {
|
||
$availableWidth -= [System.Windows.Forms.SystemInformation]::VerticalScrollBarWidth
|
||
}
|
||
$list.Columns[1].Width = [Math]::Max(180, $availableWidth)
|
||
}
|
||
}
|
||
$list.Add_SizeChanged($resizeSourceColumns)
|
||
$list.Add_HandleCreated($resizeSourceColumns)
|
||
|
||
# Dessin personnalise des en-tetes et des lignes pour que le ListView
|
||
# respecte le theme actif au lieu d'utiliser les en-tetes natifs blancs.
|
||
$list.Add_DrawColumnHeader({
|
||
param($sender, $e)
|
||
$headerColor = if ($script:Theme.Contains('TableHeader')) { $script:Theme.TableHeader } else { $script:Theme.PanelAlt }
|
||
$headerBrush = New-Object System.Drawing.SolidBrush($headerColor)
|
||
$textBrush = New-Object System.Drawing.SolidBrush($script:Theme.Text)
|
||
$borderPen = New-Object System.Drawing.Pen($script:Theme.Border)
|
||
try {
|
||
$e.Graphics.FillRectangle($headerBrush, $e.Bounds)
|
||
$e.Graphics.DrawRectangle($borderPen, $e.Bounds.X, $e.Bounds.Y, $e.Bounds.Width - 1, $e.Bounds.Height - 1)
|
||
$flags = [System.Windows.Forms.TextFormatFlags]::Left -bor [System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor [System.Windows.Forms.TextFormatFlags]::EndEllipsis
|
||
[System.Windows.Forms.TextRenderer]::DrawText($e.Graphics, $e.Header.Text, $list.Font, $e.Bounds, $script:Theme.Text, $flags)
|
||
} finally {
|
||
$headerBrush.Dispose(); $textBrush.Dispose(); $borderPen.Dispose()
|
||
}
|
||
})
|
||
$list.Add_DrawItem({
|
||
param($sender, $e)
|
||
if ($list.View -ne [System.Windows.Forms.View]::Details) {
|
||
$e.DrawDefault = $true
|
||
}
|
||
})
|
||
$list.Add_DrawSubItem({
|
||
param($sender, $e)
|
||
$rowBack = if (($e.Item.Index % 2) -eq 0) { $script:Theme.TableRowEven } else { $script:Theme.TableRowOdd }
|
||
$rowFore = $script:Theme.Text
|
||
# Ne surligner que l'element réellement sélectionné, jamais toutes les lignes.
|
||
if ($e.Item.Selected -and $list.Focused) {
|
||
$rowBack = $script:Theme.Accent
|
||
$rowFore = [System.Drawing.Color]::White
|
||
}
|
||
$rowBrush = New-Object System.Drawing.SolidBrush($rowBack)
|
||
$textBrush = New-Object System.Drawing.SolidBrush($rowFore)
|
||
$borderPen = New-Object System.Drawing.Pen($script:Theme.Border)
|
||
try {
|
||
$e.Graphics.FillRectangle($rowBrush, $e.Bounds)
|
||
$e.Graphics.DrawRectangle($borderPen, $e.Bounds.X, $e.Bounds.Y, $e.Bounds.Width - 1, $e.Bounds.Height - 1)
|
||
$flags = [System.Windows.Forms.TextFormatFlags]::Left -bor [System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor [System.Windows.Forms.TextFormatFlags]::EndEllipsis -bor [System.Windows.Forms.TextFormatFlags]::NoPrefix
|
||
[System.Windows.Forms.TextRenderer]::DrawText($e.Graphics, $e.SubItem.Text, $list.Font, $e.Bounds, $rowFore, $flags)
|
||
} finally {
|
||
$rowBrush.Dispose(); $textBrush.Dispose(); $borderPen.Dispose()
|
||
}
|
||
})
|
||
# Filtre local : il ne relance jamais l'analyse des lecteurs ou partages.
|
||
$lblFilter = New-Object System.Windows.Forms.Label
|
||
$lblFilter.Text = 'Filtrer la liste :'
|
||
$lblFilter.Location = New-Object System.Drawing.Point(16, 80)
|
||
$lblFilter.Size = New-Object System.Drawing.Size(100, 24)
|
||
$lblFilter.ForeColor = $script:Theme.Text
|
||
$dlg.Controls.Add($lblFilter)
|
||
|
||
$txtFilter = New-Object System.Windows.Forms.TextBox
|
||
$txtFilter.Location = New-Object System.Drawing.Point(120, 78)
|
||
$txtFilter.Size = New-Object System.Drawing.Size(500, 26)
|
||
$txtFilter.Anchor = 'Top,Left,Right'
|
||
$txtFilter.BackColor = $script:Theme.PanelAlt
|
||
$txtFilter.ForeColor = $script:Theme.Text
|
||
$dlg.Controls.Add($txtFilter)
|
||
|
||
$list.Location = New-Object System.Drawing.Point(16, 112)
|
||
$dlg.Controls.Add($list)
|
||
|
||
# Etat partage explicite entre les gestionnaires d'evenements WinForms.
|
||
# En PowerShell, les callbacks peuvent s'executer dans une portee differente :
|
||
# une variable locale $allFound peut alors rester vide dans le filtre.
|
||
$sourceState = [hashtable]::Synchronized(@{ AllFound = @() })
|
||
$renderSourceList = {
|
||
param([string]$Query)
|
||
$previousSelection = $null
|
||
if ($list.SelectedItems.Count -gt 0) {
|
||
$previousSelection = [string]$list.SelectedItems[0].SubItems[3].Text
|
||
}
|
||
|
||
$needle = ([string]$Query).Trim()
|
||
$visibleEntries = New-Object System.Collections.Generic.List[object]
|
||
$entries = @($sourceState.AllFound)
|
||
|
||
foreach ($entry in $entries) {
|
||
if ([string]::IsNullOrWhiteSpace($needle)) {
|
||
[void]$visibleEntries.Add($entry)
|
||
continue
|
||
}
|
||
|
||
$searchable = '{0} {1} {2} {3} {4}' -f `
|
||
[string]$entry.Type, [string]$entry.Nom, [string]$entry.Chemin, `
|
||
[string]$entry.Taille, [string]$entry.Detail
|
||
|
||
if ($searchable.IndexOf($needle, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
|
||
[void]$visibleEntries.Add($entry)
|
||
}
|
||
}
|
||
|
||
$list.BeginUpdate()
|
||
try {
|
||
$list.Items.Clear()
|
||
foreach ($entry in $visibleEntries) {
|
||
$item = New-Object System.Windows.Forms.ListViewItem([string]$entry.Type)
|
||
[void]$item.SubItems.Add([string]$entry.Nom)
|
||
[void]$item.SubItems.Add([string]$entry.Taille)
|
||
[void]$item.SubItems.Add([string]$entry.Chemin)
|
||
if ($previousSelection -and ([string]$entry.Chemin -eq $previousSelection)) {
|
||
$item.Selected = $true
|
||
}
|
||
[void]$list.Items.Add($item)
|
||
}
|
||
} finally {
|
||
$list.EndUpdate()
|
||
}
|
||
|
||
if ($entries.Count -gt 0) {
|
||
$lblStatus.Text = ('{0} résultat(s) affiché(s) sur {1}. Filtre instantané ; aucun fichier modifié.' -f $list.Items.Count, $entries.Count)
|
||
} elseif ([string]::IsNullOrWhiteSpace($needle)) {
|
||
$lblStatus.Text = 'Aucune source ISO/WIM/ESD ou installation extraite detectee dans les chemins accessibles.'
|
||
} else {
|
||
$lblStatus.Text = ('Aucun resultat pour « {0} ». Efface le filtre pour revoir la liste.' -f $needle)
|
||
}
|
||
}
|
||
$txtFilter.Add_TextChanged({ & $renderSourceList $txtFilter.Text })
|
||
|
||
$lblStatus = New-Object System.Windows.Forms.Label
|
||
$lblStatus.Text = 'Analyse en lecture seule du lecteur de DeployCenter et/ou du partage SMB. Plusieurs chemins peuvent etre separes par un point-virgule.'
|
||
$lblStatus.Location = New-Object System.Drawing.Point(16, 515)
|
||
$lblStatus.Size = New-Object System.Drawing.Size(770, 36)
|
||
$lblStatus.Anchor = 'Bottom,Left,Right'
|
||
$lblStatus.AutoSize = $false
|
||
$lblStatus.ForeColor = $script:Theme.Secondary
|
||
$dlg.Controls.Add($lblStatus)
|
||
|
||
# Recalculer les positions depuis la taille réelle de la fenêtre :
|
||
# le tableau occupe l'espace restant et le statut reste toujours visible en bas.
|
||
$resizeLayout = {
|
||
$clientWidth = $dlg.ClientSize.Width
|
||
$clientHeight = $dlg.ClientSize.Height
|
||
$rightMargin = 16
|
||
$gap = 12
|
||
$btnWidth = 88
|
||
$editionsWidth = 148
|
||
$left = 12
|
||
$right = [Math]::Max($left + $btnWidth, $clientWidth - $rightMargin)
|
||
$btnEditionsLeft = [Math]::Max($left, $right - $editionsWidth)
|
||
$btnScanLeft = [Math]::Max($left, $btnEditionsLeft - $gap - $btnWidth)
|
||
$txtPath.Width = [Math]::Max(100, $btnScanLeft - $left - $gap)
|
||
$btnEditions.Location = New-Object System.Drawing.Point($btnEditionsLeft, 40)
|
||
$btnEditions.Width = [Math]::Max(120, $right - $btnEditionsLeft)
|
||
$btnScan.Location = New-Object System.Drawing.Point($btnScanLeft, 40)
|
||
$btnScan.Width = [Math]::Max(80, $btnEditionsLeft - $gap - $btnScanLeft)
|
||
$btnScan.Anchor = 'Top,Right'
|
||
$btnEditions.Anchor = 'Top,Right'
|
||
$txtPath.Anchor = 'Top,Left,Right'
|
||
$lblFilter.Location = New-Object System.Drawing.Point(16, 80)
|
||
$txtFilter.Location = New-Object System.Drawing.Point(120, 78)
|
||
$txtFilter.Size = New-Object System.Drawing.Size([Math]::Max(180, $clientWidth - 152), 26)
|
||
$list.Location = New-Object System.Drawing.Point(16, 112)
|
||
$list.Size = New-Object System.Drawing.Size([Math]::Max(300, $clientWidth - 32), [Math]::Max(120, $clientHeight - 205))
|
||
$list.Anchor = 'Top,Bottom,Left,Right'
|
||
$lblStatus.Location = New-Object System.Drawing.Point(16, ($clientHeight - 40))
|
||
$lblStatus.Size = New-Object System.Drawing.Size([Math]::Max(300, $clientWidth - 32), 32)
|
||
$lblStatus.Anchor = 'Bottom,Left,Right'
|
||
}
|
||
$dlg.Add_Shown({
|
||
& $resizeLayout
|
||
# Lancer automatiquement la première analyse à l'ouverture.
|
||
# Le bouton Rechercher reste disponible si l'utilisateur change les chemins.
|
||
$btnScan.PerformClick()
|
||
})
|
||
$dlg.Add_SizeChanged({ & $resizeLayout })
|
||
|
||
$btnEditions.Add_Click({
|
||
if ($list.SelectedItems.Count -eq 0) {
|
||
[System.Windows.Forms.MessageBox]::Show('Sélectionne d''abord une source dans la liste.', 'Éditions Windows', 'OK', 'Information') | Out-Null
|
||
return
|
||
}
|
||
|
||
$selectedPath = [string]$list.SelectedItems[0].SubItems[3].Text
|
||
$selectedName = [string]$list.SelectedItems[0].SubItems[1].Text
|
||
$btnEditions.Enabled = $false
|
||
$btnScan.Enabled = $false
|
||
$dlg.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
|
||
$lblStatus.Text = "Analyse des editions de $selectedName en cours..."
|
||
try {
|
||
$imagePath = Resolve-InstallImagePath -SelectedPath $selectedPath
|
||
$editionResult = Get-WindowsImageEditions -ImagePath $imagePath
|
||
$editions = @($editionResult.Editions)
|
||
$globalImageVersion = [string]$editionResult.GlobalVersion
|
||
$lines = foreach ($edition in $editions) {
|
||
$index = if ($edition.ImageIndex) { $edition.ImageIndex } elseif ($edition.Index) { $edition.Index } else { '?' }
|
||
$name = if ($edition.ImageName) { $edition.ImageName } elseif ($edition.Name) { $edition.Name } else { 'Nom non disponible' }
|
||
$arch = if ($edition.Architecture) { [string]$edition.Architecture } else { 'Architecture non disponible' }
|
||
$version = if ($edition.Version) { [string]$edition.Version } else { 'Version non disponible' }
|
||
"Index : $index`r`nNom : $name`r`nArchitecture : $arch`r`nVersion : $version"
|
||
}
|
||
# Le formulaire enfant est modal : actualiser le statut avant son affichage,
|
||
# sinon le formulaire parent reste visuellement sur « analyse en cours ».
|
||
$lblStatus.Text = "$($editions.Count) édition(s) détectée(s) dans $selectedName. Sélectionne une édition."
|
||
$dlg.Refresh()
|
||
|
||
# Tableau des editions ; l'index DISM selectionne est conserve pour la suite.
|
||
$resultForm = New-Object System.Windows.Forms.Form
|
||
$resultForm.Text = 'Éditions Windows disponibles'
|
||
$resultForm.StartPosition = 'CenterParent'
|
||
$resultForm.Size = New-Object System.Drawing.Size(700, 480)
|
||
$resultForm.MinimumSize = New-Object System.Drawing.Size(520, 320)
|
||
Set-DeployCenterFormSizing -Form $resultForm
|
||
$resultForm.ShowInTaskbar = $false
|
||
$resultForm.BackColor = $script:Theme.Background
|
||
$resultForm.ForeColor = $script:Theme.Text
|
||
|
||
$sourceLabel = New-Object System.Windows.Forms.Label
|
||
$sourceLabel.Text = "Source : $selectedPath"
|
||
$sourceLabel.Dock = [System.Windows.Forms.DockStyle]::Top
|
||
$sourceLabel.Height = 38
|
||
$sourceLabel.Padding = New-Object System.Windows.Forms.Padding(10, 8, 10, 4)
|
||
$sourceLabel.AutoEllipsis = $true
|
||
$sourceLabel.BackColor = $script:Theme.Background
|
||
$sourceLabel.ForeColor = $script:Theme.Text
|
||
|
||
$editionList = New-Object System.Windows.Forms.ListView
|
||
$editionList.View = [System.Windows.Forms.View]::Details
|
||
$editionList.FullRowSelect = $true
|
||
$editionList.GridLines = $true
|
||
$editionList.MultiSelect = $false
|
||
$editionList.HideSelection = $false
|
||
# Une seule édition peut être sélectionnée à la fois.
|
||
$editionList.Sorting = [System.Windows.Forms.SortOrder]::None
|
||
$editionList.Dock = [System.Windows.Forms.DockStyle]::Fill
|
||
$editionList.BackColor = $script:Theme.Panel
|
||
$editionList.ForeColor = $script:Theme.Text
|
||
[void]$editionList.Columns.Add('Index', 65)
|
||
[void]$editionList.Columns.Add('Édition', 260)
|
||
[void]$editionList.Columns.Add('Architecture', 100)
|
||
[void]$editionList.Columns.Add('Version', 105)
|
||
|
||
# Édition est la colonne prioritaire : Version conserve une largeur fixe.
|
||
$resizeEditionColumns = {
|
||
if ($editionList.Columns.Count -ge 4) {
|
||
$availableWidth = $editionList.ClientSize.Width - $editionList.Columns[0].Width - $editionList.Columns[2].Width - $editionList.Columns[3].Width - 4
|
||
if ($editionList.Items.Count -gt 0 -and $editionList.Items.Count -gt [Math]::Floor($editionList.ClientSize.Height / 20)) {
|
||
$availableWidth -= [System.Windows.Forms.SystemInformation]::VerticalScrollBarWidth
|
||
}
|
||
$editionList.Columns[1].Width = [Math]::Max(220, $availableWidth)
|
||
}
|
||
}
|
||
$editionList.Add_SizeChanged($resizeEditionColumns)
|
||
$resultForm.Add_Shown($resizeEditionColumns)
|
||
|
||
# Dessin personnalise des en-tetes et cellules pour que ListView
|
||
# respecte le theme Light/Dark au lieu de garder un en-tete blanc.
|
||
$editionList.OwnerDraw = $true
|
||
$editionList.Add_DrawColumnHeader({
|
||
param($sender, $e)
|
||
$headerColor = if ($script:Theme.Contains('TableHeader')) { $script:Theme.TableHeader } else { $script:Theme.PanelAlt }
|
||
$headerBrush = New-Object System.Drawing.SolidBrush($headerColor)
|
||
$headerPen = New-Object System.Drawing.Pen($script:Theme.Border)
|
||
try {
|
||
$e.Graphics.FillRectangle($headerBrush, $e.Bounds)
|
||
$e.Graphics.DrawRectangle($headerPen, $e.Bounds.X, $e.Bounds.Y, $e.Bounds.Width - 1, $e.Bounds.Height - 1)
|
||
[System.Windows.Forms.TextRenderer]::DrawText(
|
||
$e.Graphics, $e.Header.Text, $sender.Font, $e.Bounds,
|
||
$script:Theme.Text,
|
||
([System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor
|
||
[System.Windows.Forms.TextFormatFlags]::Left -bor
|
||
[System.Windows.Forms.TextFormatFlags]::EndEllipsis)
|
||
)
|
||
} finally {
|
||
$headerBrush.Dispose()
|
||
$headerPen.Dispose()
|
||
}
|
||
})
|
||
$editionList.Add_DrawItem({ param($sender, $e) })
|
||
$editionList.Add_DrawSubItem({
|
||
param($sender, $e)
|
||
$cellBack = if (($e.Item.Index % 2) -eq 0) { $script:Theme.TableRowEven } else { $script:Theme.TableRowOdd }
|
||
$cellFore = $script:Theme.Text
|
||
if ($e.Item.Selected) {
|
||
$cellBack = $script:Theme.Accent
|
||
$cellFore = [System.Drawing.Color]::White
|
||
}
|
||
$cellBrush = New-Object System.Drawing.SolidBrush($cellBack)
|
||
$cellPen = New-Object System.Drawing.Pen($script:Theme.Border)
|
||
try {
|
||
$e.Graphics.FillRectangle($cellBrush, $e.Bounds)
|
||
if ($sender.GridLines) {
|
||
$e.Graphics.DrawRectangle($cellPen, $e.Bounds.X, $e.Bounds.Y, $e.Bounds.Width - 1, $e.Bounds.Height - 1)
|
||
}
|
||
[System.Windows.Forms.TextRenderer]::DrawText(
|
||
$e.Graphics, $e.SubItem.Text, $sender.Font, $e.Bounds,
|
||
$cellFore,
|
||
([System.Windows.Forms.TextFormatFlags]::VerticalCenter -bor
|
||
[System.Windows.Forms.TextFormatFlags]::Left -bor
|
||
[System.Windows.Forms.TextFormatFlags]::EndEllipsis)
|
||
)
|
||
} finally {
|
||
$cellBrush.Dispose()
|
||
$cellPen.Dispose()
|
||
}
|
||
})
|
||
|
||
foreach ($edition in $editions) {
|
||
$index = if ($edition.ImageIndex) { [string]$edition.ImageIndex } elseif ($edition.Index) { [string]$edition.Index } else { '?' }
|
||
$name = if ($edition.ImageName) { [string]$edition.ImageName } elseif ($edition.Name) { [string]$edition.Name } elseif ($edition.ImageDescription) { [string]$edition.ImageDescription } else { 'Nom non disponible' }
|
||
$archValue = $null
|
||
foreach ($propertyName in @('Architecture', 'ImageArchitecture')) {
|
||
if ($edition.PSObject.Properties[$propertyName] -and $edition.$propertyName) {
|
||
$archValue = $edition.$propertyName
|
||
break
|
||
}
|
||
}
|
||
$arch = if ($null -ne $archValue) {
|
||
switch ([string]$archValue) {
|
||
'0' { 'x86' }
|
||
'5' { 'ARM' }
|
||
'9' { 'x64' }
|
||
'12' { 'ARM64' }
|
||
default { [string]$archValue }
|
||
}
|
||
} else { 'Non disponible' }
|
||
$version = if ($edition.Version) { [string]$edition.Version } else { 'Non disponible' }
|
||
|
||
$row = New-Object System.Windows.Forms.ListViewItem($index)
|
||
[void]$row.SubItems.Add($name)
|
||
[void]$row.SubItems.Add($arch)
|
||
[void]$row.SubItems.Add($version)
|
||
$row.Tag = $edition
|
||
[void]$editionList.Items.Add($row)
|
||
}
|
||
|
||
$buttonPanel = New-Object System.Windows.Forms.Panel
|
||
$buttonPanel.Dock = [System.Windows.Forms.DockStyle]::Bottom
|
||
$buttonPanel.Height = 52
|
||
$buttonPanel.BackColor = $script:Theme.Background
|
||
|
||
$cancelButton = New-Object System.Windows.Forms.Button
|
||
$cancelButton.Text = 'Annuler'
|
||
$cancelButton.Size = New-Object System.Drawing.Size(100, 30)
|
||
$cancelButton.Anchor = 'Right,Top'
|
||
$cancelButton.Location = New-Object System.Drawing.Point(0, 10)
|
||
$cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
|
||
|
||
$validateButton = New-Object System.Windows.Forms.Button
|
||
$validateButton.Text = 'Valider l''édition'
|
||
$validateButton.Size = New-Object System.Drawing.Size(140, 30)
|
||
$validateButton.Anchor = 'Right,Top'
|
||
$validateButton.Location = New-Object System.Drawing.Point(0, 10)
|
||
$validateButton.Enabled = $false
|
||
|
||
$editionList.Add_SelectedIndexChanged({
|
||
$validateButton.Enabled = ($editionList.SelectedItems.Count -gt 0 -and $editionList.SelectedItems[0].SubItems[0].Text -ne '?')
|
||
})
|
||
$validateButton.Add_Click({
|
||
if ($editionList.SelectedItems.Count -gt 0) {
|
||
$resultForm.Tag = [pscustomobject]@{
|
||
SourcePath = $selectedPath
|
||
ImagePath = $imagePath
|
||
Edition = $editionList.SelectedItems[0].Tag
|
||
Index = [int]$editionList.SelectedItems[0].SubItems[0].Text
|
||
Name = $editionList.SelectedItems[0].SubItems[1].Text
|
||
}
|
||
$resultForm.DialogResult = [System.Windows.Forms.DialogResult]::OK
|
||
$resultForm.Close()
|
||
}
|
||
})
|
||
$editionList.Add_DoubleClick({
|
||
if ($editionList.SelectedItems.Count -gt 0 -and $validateButton.Enabled) {
|
||
$validateButton.PerformClick()
|
||
}
|
||
})
|
||
$buttonPanel.Controls.Add($cancelButton)
|
||
$buttonPanel.Controls.Add($validateButton)
|
||
$buttonPanel.Add_Resize({
|
||
$validateButton.Location = New-Object System.Drawing.Point(($buttonPanel.ClientSize.Width - $validateButton.Width - 12), 10)
|
||
$cancelButton.Location = New-Object System.Drawing.Point(($validateButton.Left - $cancelButton.Width - 8), 10)
|
||
})
|
||
|
||
$resultForm.Controls.Add($editionList)
|
||
$resultForm.Controls.Add($buttonPanel)
|
||
$resultForm.Controls.Add($sourceLabel)
|
||
$resultForm.AcceptButton = $validateButton
|
||
$resultForm.CancelButton = $cancelButton
|
||
Add-SessionFooter -TargetForm $resultForm -ShowFooter $false
|
||
$dialogResult = $resultForm.ShowDialog($dlg)
|
||
$chosenEdition = $resultForm.Tag
|
||
$resultForm.Dispose()
|
||
|
||
if ($dialogResult -eq [System.Windows.Forms.DialogResult]::OK -and $chosenEdition) {
|
||
$script:SelectedWindowsEdition = $chosenEdition
|
||
$lblStatus.Text = "Édition sélectionnée : $($chosenEdition.Name) (index $($chosenEdition.Index))."
|
||
Write-Log "Edition selectionnee : $($chosenEdition.Name), index $($chosenEdition.Index), image $imagePath" 'SUCCESS'
|
||
} else {
|
||
$lblStatus.Text = "$($editions.Count) édition(s) trouvée(s) dans $selectedName. Aucune nouvelle édition sélectionnée."
|
||
}
|
||
Write-Log "Analyse des editions terminee : $imagePath ($($editions.Count) edition(s))." 'SUCCESS'
|
||
} catch {
|
||
$lblStatus.Text = "Echec de l'analyse des editions de $selectedName."
|
||
Write-Log "Erreur analyse editions $selectedPath : $($_.Exception.Message)" 'ERROR'
|
||
[System.Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Analyse des editions', 'OK', 'Warning') | Out-Null
|
||
} finally {
|
||
$dlg.Cursor = [System.Windows.Forms.Cursors]::Default
|
||
$btnEditions.Enabled = $true
|
||
$btnScan.Enabled = $true
|
||
}
|
||
})
|
||
|
||
$btnScan.Add_Click({
|
||
$btnScan.Enabled = $false
|
||
$dlg.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
|
||
$lblStatus.Text = 'Recherche en cours sur les chemins indiques...'
|
||
$sourceState.AllFound = @()
|
||
$list.Items.Clear()
|
||
try {
|
||
# La recherche ne modifie jamais DeployCenter.ini.
|
||
$sourceState.AllFound = @(Find-WindowsSources -RootPath $txtPath.Text)
|
||
& $renderSourceList $txtFilter.Text
|
||
if (@($sourceState.AllFound).Count -eq 0) {
|
||
$lblStatus.Text = 'Aucune source ISO/WIM/ESD ou installation extraite detectee dans les chemins accessibles.'
|
||
Write-Log 'Aucune source Windows detectee dans les chemins accessibles.' 'WARNING'
|
||
}
|
||
} catch {
|
||
$lblStatus.Text = 'Echec de la recherche. Consulte le message.'
|
||
Write-Log "Erreur detection sources : $($_.Exception.Message)" 'ERROR'
|
||
[System.Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Detection des sources', 'OK', 'Warning') | Out-Null
|
||
} finally {
|
||
$dlg.Cursor = [System.Windows.Forms.Cursors]::Default
|
||
$btnScan.Enabled = $true
|
||
}
|
||
})
|
||
|
||
Add-SessionFooter -TargetForm $dlg
|
||
[void]$dlg.ShowDialog($script:form)
|
||
$dlg.Dispose()
|
||
}
|
||
|
||
# ============================================================
|
||
# INVENTAIRE MATERIEL
|
||
# ============================================================
|
||
|
||
function Update-HardwareView {
|
||
$script:btnRefresh.Enabled = $false
|
||
$script:form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
|
||
|
||
try {
|
||
if (-not (Test-Path $script:HardwareScript)) {
|
||
throw "Fichier HardwareInfo.ps1 introuvable : $script:HardwareScript"
|
||
}
|
||
|
||
. $script:HardwareScript
|
||
$info = Get-HardwareInfo
|
||
|
||
$script:lblManufacturer.Text = "Fabricant : $($info.Manufacturer)"
|
||
$script:lblModel.Text = "Modèle : $($info.Model)"
|
||
$script:lblSerial.Text = "Numéro de série : $($info.SerialNumber)"
|
||
$script:lblFirmware.Text = "Firmware : $($info.Firmware)"
|
||
$script:lblArchitecture.Text = "Architecture : $($info.Architecture)"
|
||
|
||
if ($info.WinPE) {
|
||
$script:lblWinPE.Text = "Environnement : WinPE"
|
||
$script:lblWinPE.ForeColor = $script:Theme.Success
|
||
}
|
||
else {
|
||
$script:lblWinPE.Text = "Environnement : Windows classique"
|
||
$script:lblWinPE.ForeColor = $script:Theme.Secondary
|
||
}
|
||
|
||
$previousDiskIndex = $null
|
||
if ($null -ne $script:cmbDisk.SelectedItem -and $null -ne $script:cmbDisk.SelectedItem.Tag) {
|
||
$previousDiskIndex = [string]$script:cmbDisk.SelectedItem.Tag
|
||
}
|
||
$script:cmbDisk.BeginUpdate()
|
||
try {
|
||
$script:cmbDisk.Items.Clear()
|
||
foreach ($disk in $info.Disks) {
|
||
$diskLabel = "Disque {0} - {1} - {2:N1} Go - {3} - {4}" -f $disk.Index, $disk.Model, $disk.SizeGB, $disk.Interface, $disk.Status
|
||
$diskChoice = [pscustomobject]@{ Text = $diskLabel; Tag = [string]$disk.Index }
|
||
[void]$script:cmbDisk.Items.Add($diskChoice)
|
||
}
|
||
$script:cmbDisk.DisplayMember = 'Text'
|
||
if ($script:cmbDisk.Items.Count -gt 0) {
|
||
$selectedIndex = -1
|
||
if ($null -ne $previousDiskIndex) {
|
||
for ($i = 0; $i -lt $script:cmbDisk.Items.Count; $i++) {
|
||
if ([string]$script:cmbDisk.Items[$i].Tag -eq $previousDiskIndex) { $selectedIndex = $i; break }
|
||
}
|
||
}
|
||
if ($selectedIndex -lt 0 -and $script:cmbDisk.Items.Count -eq 1) { $selectedIndex = 0 }
|
||
$script:cmbDisk.SelectedIndex = $selectedIndex
|
||
}
|
||
}
|
||
finally {
|
||
$script:cmbDisk.EndUpdate()
|
||
}
|
||
|
||
$json = $info | ConvertTo-Json -Depth 6
|
||
Set-Content -LiteralPath $script:HardwareJson -Value $json -Encoding UTF8
|
||
Write-DeployInventory -HardwareInfo $info
|
||
|
||
Write-Log "Inventaire materiel actualise." "SUCCESS"
|
||
|
||
foreach ($errorMessage in $info.Errors) {
|
||
Write-Log $errorMessage "WARNING"
|
||
}
|
||
|
||
if ($info.Disks.Count -eq 0) {
|
||
Write-Log "Aucun disque physique detecte." "WARNING"
|
||
}
|
||
}
|
||
catch {
|
||
Write-Log "Erreur inventaire : $($_.Exception.Message)" "ERROR"
|
||
|
||
[System.Windows.Forms.MessageBox]::Show(
|
||
$_.Exception.Message,
|
||
"Erreur DeployCenter",
|
||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||
) | Out-Null
|
||
}
|
||
finally {
|
||
$script:btnRefresh.Enabled = $true
|
||
$script:form.Cursor = [System.Windows.Forms.Cursors]::Default
|
||
}
|
||
}
|
||
|
||
# ============================================================
|
||
# MISE EN PAGE ADAPTATIVE
|
||
# Les grandes zones suivent la fenetre ; les marges et hauteurs de controles restent fixes.
|
||
# ============================================================
|
||
|
||
function Update-DeployCenterLayout {
|
||
if ($null -eq $script:form -or $script:form.IsDisposed) { return }
|
||
$w = $script:form.ClientSize.Width
|
||
$h = $script:form.ClientSize.Height
|
||
$m = 24
|
||
$contentWidth = [Math]::Max(300, $w - (2 * $m))
|
||
|
||
# En-tete : le selecteur de theme reste accroche au bord droit.
|
||
if ($null -ne $script:cmbTheme) {
|
||
$script:cmbTheme.Location = New-Object System.Drawing.Point([Math]::Max($m, $w - $m - $script:cmbTheme.Width), 25)
|
||
}
|
||
|
||
# Les deux panneaux superieurs conservent leur hauteur, mais s'etirent en largeur.
|
||
$script:groupPC.Location = New-Object System.Drawing.Point($m, 100)
|
||
$script:groupPC.Size = New-Object System.Drawing.Size($contentWidth, 105)
|
||
$script:groupDisks.Location = New-Object System.Drawing.Point($m, 220)
|
||
$script:groupDisks.Size = New-Object System.Drawing.Size($contentWidth, 78)
|
||
Set-DiskSelectorWidth
|
||
|
||
# Le journal absorbe l'espace vertical restant au-dessus des actions et du pied de page.
|
||
$footerHeight = 26
|
||
$buttonHeightLocal = 38
|
||
$buttonYLocal = $h - $footerHeight - 14 - $buttonHeightLocal
|
||
$logYLocal = $script:groupDisks.Bottom + 14
|
||
$logHeightLocal = [Math]::Max(90, $buttonYLocal - $logYLocal - 12)
|
||
$script:groupLog.Location = New-Object System.Drawing.Point($m, $logYLocal)
|
||
$script:groupLog.Size = New-Object System.Drawing.Size($contentWidth, $logHeightLocal)
|
||
$script:txtLog.Location = New-Object System.Drawing.Point(12, 28)
|
||
$script:txtLog.Size = New-Object System.Drawing.Size([Math]::Max(250, $script:groupLog.ClientSize.Width - 24), [Math]::Max(50, $script:groupLog.ClientSize.Height - 40))
|
||
|
||
$buttonWidthLocal = 140
|
||
$buttonGapLocal = 12
|
||
$script:btnClose.Size = New-Object System.Drawing.Size($buttonWidthLocal, $buttonHeightLocal)
|
||
$script:btnRefresh.Size = New-Object System.Drawing.Size($buttonWidthLocal, $buttonHeightLocal)
|
||
$script:btnSources.Size = New-Object System.Drawing.Size(170, $buttonHeightLocal)
|
||
$script:btnClose.Location = New-Object System.Drawing.Point([Math]::Max($m, $w - $m - $buttonWidthLocal), $buttonYLocal)
|
||
$script:btnRefresh.Location = New-Object System.Drawing.Point([Math]::Max($m, $script:btnClose.Left - $buttonGapLocal - $buttonWidthLocal), $buttonYLocal)
|
||
$script:btnSources.Location = New-Object System.Drawing.Point([Math]::Max($m, $script:btnRefresh.Left - $buttonGapLocal - 170), $buttonYLocal)
|
||
Set-DiskSelectorWidth
|
||
}
|
||
|
||
# ============================================================
|
||
# EVENEMENTS
|
||
# ============================================================
|
||
|
||
$script:btnSources.Add_Click({
|
||
Show-WindowsSourcesDialog
|
||
})
|
||
|
||
$script:btnRefresh.Add_Click({
|
||
Update-HardwareView
|
||
})
|
||
|
||
$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: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()
|
||
Write-Log "DeployCenter demarre."
|
||
Write-Log "Dossier : $script:DeployRoot"
|
||
Write-Log "Journal : $script:LogFile"
|
||
Update-HardwareView
|
||
})
|
||
|
||
$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()
|
||
})
|
||
|
||
# ============================================================
|
||
# LANCEMENT
|
||
# ============================================================
|
||
|
||
try {
|
||
[void]$script:form.ShowDialog()
|
||
}
|
||
finally {
|
||
if ($script:EnableVnc -and (Get-Command Stop-DeployCenterVnc -ErrorAction SilentlyContinue)) {
|
||
Stop-DeployCenterVnc
|
||
}
|
||
$script:form.Dispose()
|
||
}
|