DeployCenter

This commit is contained in:
benoit committed 2026-10-15 21:15:33 +02:00
commit 7c724d475e
14 files changed
+2460

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
# DeployCenter - Gestion des journaux
# Compatible Windows PowerShell 5.1
function Initialize-DeployCenterLogging {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$LogRoot,
[AllowNull()]
[string]$IPv4 = $null
)
$script:LogRoot = $LogRoot
if (-not (Test-Path -LiteralPath $script:LogRoot -PathType Container)) {
New-Item -Path $script:LogRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null
}
# L'adresse IPv4 est fournie par Modules\\Network.psm1.
$script:LogIPv4 = $IPv4
$logIpTag = if ([string]::IsNullOrWhiteSpace($script:LogIPv4)) {
'sans-ip'
} else {
$script:LogIPv4 -replace '[^0-9A-Za-z.-]', '_'
}
$script:LogFile = Join-Path $script:LogRoot (
'DeployCenter_{0}_{1}.log' -f $logIpTag, (Get-Date -Format 'yyyyMMdd_HHmmss')
)
}
function Write-Log {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Message,
[ValidateSet('INFO', 'SUCCESS', 'WARNING', 'ERROR')]
[string]$Level = 'INFO'
)
$line = '{0} [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message
try {
[System.IO.File]::AppendAllText(
$script:LogFile,
$line + [Environment]::NewLine,
[System.Text.Encoding]::UTF8
)
} catch {
# Le journal graphique reste disponible si le fichier n'est pas accessible.
}
if ($null -ne $script:txtLog -and -not $script:txtLog.IsDisposed) {
$script:txtLog.AppendText($line + [Environment]::NewLine)
$script:txtLog.SelectionStart = $script:txtLog.TextLength
$script:txtLog.ScrollToCaret()
}
}
+29
View File
@@ -0,0 +1,29 @@
# DeployCenter - Détection réseau
# Compatible Windows PowerShell 5.1
function Get-DeployNetworkIPv4 {
[CmdletBinding()]
param()
# Priorité à une interface avec passerelle par défaut ; exclut loopback,
# APIPA et adresses IPv4 non routables couramment inutiles en WinPE.
$candidates = @()
try {
foreach ($cfg in (Get-CimInstance Win32_NetworkAdapterConfiguration -Filter 'IPEnabled=True' -ErrorAction Stop)) {
$hasGateway = @($cfg.DefaultIPGateway | Where-Object { $_ -match '^\d{1,3}(\.\d{1,3}){3}$' }).Count -gt 0
foreach ($address in @($cfg.IPAddress | Where-Object { $_ -match '^\d{1,3}(\.\d{1,3}){3}$' })) {
if ($address -eq '127.0.0.1' -or $address -like '169.254.*' -or $address -eq '0.0.0.0') { continue }
$candidates += [pscustomobject]@{ Address = $address; HasGateway = $hasGateway; Index = [int]$cfg.Index }
}
}
} catch {
# La détection réseau ne doit pas empêcher le démarrage de DeployCenter.
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
Write-Log "Détection réseau WMI : $($_.Exception.Message)" 'WARNING'
}
}
$chosen = $candidates | Sort-Object @{Expression='HasGateway';Descending=$true}, Index | Select-Object -First 1
if ($chosen) { return [string]$chosen.Address }
return $null
}
+62
View File
@@ -0,0 +1,62 @@
Set-StrictMode -Version 2.0
function ConvertFrom-ThemeColor {
param([Parameter(Mandatory=$true)][string]$Value)
$hex = $Value.Trim().TrimStart('#')
if ($hex -notmatch '^[0-9A-Fa-f]{6}$') {
throw "Couleur invalide dans un thème : '$Value'. Utiliser #RRGGBB."
}
return [System.Drawing.Color]::FromArgb(
[Convert]::ToInt32($hex.Substring(0,2),16),
[Convert]::ToInt32($hex.Substring(2,2),16),
[Convert]::ToInt32($hex.Substring(4,2),16)
)
}
function Get-DeployCenterThemeNames {
param([Parameter(Mandatory=$true)][string]$ThemesRoot)
$names = @()
foreach ($file in (Get-ChildItem -LiteralPath $ThemesRoot -Filter '*.psd1' -File -ErrorAction SilentlyContinue)) {
if ($file.BaseName -match '^[A-Za-z0-9_-]+$') { $names += $file.BaseName }
}
return @($names | Sort-Object -Unique)
}
function Import-DeployCenterTheme {
param(
[Parameter(Mandatory=$true)][string]$ThemesRoot,
[Parameter(Mandatory=$true)][string]$ThemeName
)
if ($ThemeName -notmatch '^[A-Za-z0-9_-]+$') { throw "Nom de thème invalide : $ThemeName" }
$path = Join-Path $ThemesRoot ($ThemeName + '.psd1')
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Thème introuvable : $ThemeName" }
$data = Import-PowerShellDataFile -LiteralPath $path
$required = @('Background','Panel','PanelAlt','Text','Secondary','Accent','Success','Border','Danger')
foreach ($key in $required) {
if (-not $data.ContainsKey($key)) { throw "Le thème '$ThemeName' ne contient pas la clé '$key'." }
}
$theme = @{}
foreach ($key in $required) { $theme[$key] = ConvertFrom-ThemeColor -Value ([string]$data[$key]) }
foreach ($key in @('TableHeader','TableRowEven','TableRowOdd')) {
if ($data.ContainsKey($key)) { $theme[$key] = ConvertFrom-ThemeColor -Value ([string]$data[$key]) }
else { $theme[$key] = $theme.PanelAlt }
}
$buttonDefaults = @{
Button = @{ Background=$theme.PanelAlt; Foreground=$theme.Text; Border=$theme.Border; HoverBackground=$theme.Panel; PressedBackground=$theme.Border; DisabledBackground=$theme.Panel; DisabledForeground=$theme.Secondary }
PrimaryButton = @{ Background=$theme.Accent; Foreground=[System.Drawing.Color]::White; Border=$theme.Accent; HoverBackground=$theme.Accent; PressedBackground=$theme.Accent; DisabledBackground=$theme.PanelAlt; DisabledForeground=$theme.Secondary }
DangerButton = @{ Background=$theme.Danger; Foreground=[System.Drawing.Color]::White; Border=$theme.Danger; HoverBackground=$theme.Danger; PressedBackground=$theme.Danger; DisabledBackground=$theme.PanelAlt; DisabledForeground=$theme.Secondary }
}
foreach ($role in @('Button','PrimaryButton','DangerButton')) {
$theme[$role] = @{}
$provided = $data[$role]
foreach ($property in @('Background','Foreground','Border','HoverBackground','PressedBackground','DisabledBackground','DisabledForeground')) {
$raw = $null
if ($provided -is [System.Collections.IDictionary] -and $provided.Contains($property)) { $raw = [string]$provided[$property] }
if ([string]::IsNullOrWhiteSpace($raw)) { $theme[$role][$property] = $buttonDefaults[$role][$property] }
else { $theme[$role][$property] = ConvertFrom-ThemeColor -Value $raw }
}
}
return $theme
}
Export-ModuleMember -Function ConvertFrom-ThemeColor, Get-DeployCenterThemeNames, Import-DeployCenterTheme
+22
View File
@@ -0,0 +1,22 @@
function Set-DeployCenterButtonStyle {
param(
[Parameter(Mandatory=$true)][System.Windows.Forms.Button]$Button,
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Theme,
[ValidateSet('Button','PrimaryButton','DangerButton')][string]$Role = 'Button'
)
$style = $Theme[$Role]
$Button.BackColor = $style.Background
$Button.ForeColor = $style.Foreground
$Button.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
$Button.UseVisualStyleBackColor = $false
$Button.FlatAppearance.BorderSize = 1
$Button.FlatAppearance.BorderColor = $style.Border
$Button.FlatAppearance.MouseOverBackColor = $style.HoverBackground
$Button.FlatAppearance.MouseDownBackColor = $style.PressedBackground
if (-not $Button.Enabled) {
$Button.BackColor = $style.DisabledBackground
$Button.ForeColor = $style.DisabledForeground
}
}
Export-ModuleMember -Function Set-DeployCenterButtonStyle
+127
View File
@@ -0,0 +1,127 @@
# Module VNC de DeployCenter
# Exécuté uniquement avec -Vnc dans le scope principal pour partager le contexte WinPE.
function Start-DeployCenterVnc {
$parentRoot = Split-Path -Path $script:DeployRoot -Parent
$sourceVnc = Join-Path $parentRoot 'uvnc'
$sourceExe = Join-Path $sourceVnc 'winvnc.exe'
$passwordTool = Join-Path $sourceVnc 'createpassword64.exe'
$sourceIni = Join-Path $sourceVnc 'ultravnc.ini'
$script:DeployIPv4 = Get-DeployNetworkIPv4
if ([string]::IsNullOrWhiteSpace($script:DeployIPv4)) {
Write-Log 'Aucune adresse IPv4 de déploiement détectée.' 'WARNING'
}
if (-not (Test-Path -LiteralPath $sourceExe -PathType Leaf)) {
Write-Log "UltraVNC non démarré : exécutable absent ($sourceExe)." 'WARNING'
Write-DeployInventory
return
}
if (-not (Test-Path -LiteralPath $passwordTool -PathType Leaf)) {
Write-Log "UltraVNC non démarré : createpassword64.exe absent ($passwordTool)." 'WARNING'
Write-DeployInventory
return
}
if (-not (Test-Path -LiteralPath $sourceIni -PathType Leaf)) {
Write-Log "UltraVNC non démarré : fichier absent ($sourceIni)." 'WARNING'
Write-DeployInventory
return
}
try {
if (Test-Path -LiteralPath $script:VncDirectory) {
Remove-Item -LiteralPath $script:VncDirectory -Recurse -Force -ErrorAction Stop
}
New-Item -Path $script:VncDirectory -ItemType Directory -Force | Out-Null
Copy-Item -Path (Join-Path $sourceVnc '*') -Destination $script:VncDirectory -Recurse -Force
# WinPE ne permet pas toujours d'ajouter des règles de pare-feu.
# La désactivation globale n'est effectuée que lorsque VNC est demandé.
$wpeutil = Join-Path $env:WINDIR 'System32\wpeutil.exe'
if (-not (Test-Path -LiteralPath $wpeutil -PathType Leaf)) {
$wpeutil = 'wpeutil.exe'
}
& $wpeutil DisableFirewall
if ($LASTEXITCODE -ne 0) {
throw "wpeutil DisableFirewall a renvoyé le code $LASTEXITCODE."
}
Write-Log 'Pare-feu WinPE désactivé à la demande de -Vnc.' 'WARNING'
# Force le mode portable : l'INI actif est celui placé à côté de winvnc.exe.
$portableFlag = Join-Path $script:VncDirectory 'ultravnc.portable'
Set-Content -LiteralPath $portableFlag -Value '' -Encoding ASCII
if (-not [string]::IsNullOrWhiteSpace([string]$script:Config.VncPassword)) {
$script:VncPassword = [string]$script:Config.VncPassword
$passwordSource = 'configuré dans DeployCenter.ini'
}
else {
$script:VncPassword = -join (1..8 | ForEach-Object {
$alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'
$alphabet[(Get-Random -Minimum 0 -Maximum $alphabet.Length)]
})
$passwordSource = 'généré automatiquement'
}
# createpassword64 encode le mot de passe choisi dans le champ passwd d'ultravnc.ini.
$tool = Join-Path $script:VncDirectory 'createpassword64.exe'
$script:VncProcess = Start-Process -FilePath $tool -ArgumentList @($script:VncPassword) `
-WorkingDirectory $script:VncDirectory -Wait -PassThru -WindowStyle Hidden
if ($script:VncProcess.ExitCode -ne 0) {
throw "createpassword64.exe a renvoyé le code $($script:VncProcess.ExitCode)."
}
$activeIni = Join-Path $script:VncDirectory 'ultravnc.ini'
if (-not (Test-Path -LiteralPath $activeIni -PathType Leaf) -or
-not (Select-String -LiteralPath $activeIni -Pattern '^passwd\s*=' -Quiet)) {
throw "createpassword64.exe n'a pas créé le champ passwd dans ultravnc.ini."
}
$server = Join-Path $script:VncDirectory 'winvnc.exe'
$script:VncProcess = Start-Process -FilePath $server -ArgumentList @('-run') `
-WorkingDirectory $script:VncDirectory -PassThru -WindowStyle Hidden
Start-Sleep -Milliseconds 800
if ($script:VncProcess.HasExited) {
throw "winvnc.exe s'est arrêté immédiatement (code $($script:VncProcess.ExitCode))."
}
$script:VncStarted = $true
Write-Log "UltraVNC démarré. IP : $($script:DeployIPv4) ; mot de passe $passwordSource." 'SUCCESS'
} catch {
Write-Log "Échec du démarrage UltraVNC : $($_.Exception.Message)" 'ERROR'
$script:VncStarted = $false
$script:VncPassword = $null
}
Write-DeployInventory
}
function Stop-DeployCenterVnc {
try {
if ($null -ne $script:VncProcess) {
$proc = Get-Process -Id $script:VncProcess.Id -ErrorAction SilentlyContinue
if ($null -ne $proc) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue }
}
# Au cas où winvnc lance un processus serveur distinct, ne cibler que les
# processus dont le chemin correspond à notre copie temporaire.
foreach ($proc in (Get-CimInstance Win32_Process -Filter "Name='winvnc.exe'" -ErrorAction SilentlyContinue)) {
if ($proc.ExecutablePath -and $proc.ExecutablePath.StartsWith($script:VncDirectory, [StringComparison]::OrdinalIgnoreCase)) {
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
}
}
} catch {
Write-Log "Arrêt UltraVNC : $($_.Exception.Message)" 'WARNING'
}
# Le processus winvnc peut conserver brièvement un handle sur son dossier.
# Le dossier VNC est propre à ce processus PowerShell (uvnc_<PID>) : on ne
# supprime pas récursivement le dossier partagé InventoryDir, ce qui évite
# de bloquer le lancement suivant si un handle est encore ouvert.
try {
if (Test-Path -LiteralPath $script:InventoryFile -PathType Leaf) {
Remove-Item -LiteralPath $script:InventoryFile -Force -ErrorAction SilentlyContinue
}
if (Test-Path -LiteralPath $script:VncDirectory -PathType Container) {
Start-Sleep -Milliseconds 500
Remove-Item -LiteralPath $script:VncDirectory -Recurse -Force -ErrorAction SilentlyContinue
}
} catch { }
}