<# C:\Temp\UpgradeToWin11.ps1 Nicolas BELLANGER - SI Charal Cholet Script upgrade Windows 10 -> Windows 11 (ISO) - version sans monitoring (UI de progression via /passive) Exigences couvertes - Contrôles de base + compatibilité simplifiée - Forçage "unsupported" UNIQUEMENT si : * PC non compatible * et l'utilisateur répond OUI à la question * ou si -ForceUnsupported est fourni - Peu d'interactions : questions avec défaut après 30s - RebootPending : * détection * proposition reboot (défaut NON) * si OUI => tâche Resume (AtLogOn) puis reboot - Reprise post-reboot : * une seule tâche active à la fois (Reload/Resume exclusives) - Post-upgrade : * tâche Reload (AtLogOn) : son succès/erreur + proposition nettoyage C:\Temp - Nettoyage "fin anticipée" : * si Win11 déjà installé OU arrêt avec erreur/annulation * on supprime la tâche Reload et on propose de supprimer C:\Temp (en démontant l'ISO avant) Arguments setup /auto upgrade /eula accept /dynamicupdate enable /compat ignorewarning /showoobe none Remarque importante - /auto upgrade /passive : l'UI minimale de progression (pas de monitoring custom). #> param( [string]$ISOPath = "C:\Temp\Win11_25H2_x64.iso", [string]$LogDir = "C:\Temp\Log", [int]$MinFreeSpaceGB = 25, [switch]$AutoYesUpgrade, [switch]$ForceUnsupported, [switch]$Reload, # post-reboot : fin + son + nettoyage [switch]$RebootPending, # post-reboot : (re)démarrer setup.exe après reboot pending [int]$PromptTimeoutSeconds = 30 ) # -------------------- Constantes -------------------- $TaskNameReload = "Win11Upgrade-PostReboot" $TaskNameResume = "Win11Upgrade-ResumeSetup" $StateFile = Join-Path $LogDir "UpgradeState.json" # -------------------- Logging -------------------- function Ensure-Dir([string]$p) { if (-not (Test-Path $p)) { New-Item -Path $p -ItemType Directory -Force | Out-Null } } Ensure-Dir $LogDir $Script:LogFile = Join-Path $LogDir ("UpgradeToWin11_{0}.log" -f (Get-Date -Format "yyyyMMdd_HHmmss")) function Write-Log { param( [Parameter(Mandatory=$true)][string]$Message, [ValidateSet("INFO","WARN","ERROR","SUCCESS")][string]$Level="INFO" ) $line = "{0} [{1}] {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Level, $Message Add-Content -Path $Script:LogFile -Value $line -Encoding UTF8 switch ($Level) { "ERROR" { Write-Host $line -ForegroundColor Red } "WARN" { Write-Host $line -ForegroundColor Yellow } "SUCCESS" { Write-Host $line -ForegroundColor Green } default { Write-Host $line -ForegroundColor Gray } } } function Read-HostWithTimeout { param([string]$Prompt,[int]$TimeoutSeconds=30,[string]$Default="") Write-Host "$Prompt (défaut dans $TimeoutSeconds s : $Default)" -ForegroundColor Cyan $sw=[Diagnostics.Stopwatch]::StartNew(); $input="" while ($sw.Elapsed.TotalSeconds -lt $TimeoutSeconds) { if ($Host.UI.RawUI.KeyAvailable) { $k=$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") if ($k.VirtualKeyCode -eq 13) { break } # Enter if ($k.Character -ne 0) { $input += $k.Character } } else { Start-Sleep -Milliseconds 200 } } if ([string]::IsNullOrWhiteSpace($input)) { return $Default } return $input } # -------------------- Sons -------------------- function Play-SuccessBeep { for ($i = 1; $i -lt 5; $i++) { [console]::Beep(300*$i, 400); Start-Sleep -Milliseconds 100 } } function Play-ErrorBeep { for ($i = 1; $i -le 3; $i++) { [console]::Beep(1200, 600); Start-Sleep -Milliseconds 150 } } # -------------------- Admin -------------------- function Assert-Admin { $id=[Security.Principal.WindowsIdentity]::GetCurrent() $p =New-Object Security.Principal.WindowsPrincipal($id) if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Log "Relance du script en admin (UAC)..." "WARN" Start-Process powershell.exe -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" $($MyInvocation.UnboundArguments)" exit } } # -------------------- State helpers -------------------- function Save-State([hashtable]$state) { Ensure-Dir $LogDir ($state | ConvertTo-Json -Depth 6) | Set-Content -Path $StateFile -Encoding UTF8 } function Load-State() { if (Test-Path $StateFile) { try { return (Get-Content $StateFile -Raw -Encoding UTF8 | ConvertFrom-Json) } catch { return $null } } return $null } # -------------------- ISO helpers -------------------- function Get-OrMount-IsoDriveLetter { param([Parameter(Mandatory=$true)][string]$IsoPath) if (-not (Test-Path $IsoPath)) { throw "ISO introuvable: $IsoPath" } # ISO déjà monté ? $img = Get-DiskImage -ImagePath $IsoPath -ErrorAction SilentlyContinue if ($img -and $img.Attached) { try { $dl = ($img | Get-Volume | Select-Object -First 1).DriveLetter if ($dl) { Write-Log "ISO déjà monté: $IsoPath -> $dl`:" "INFO" return $dl } } catch { Write-Log "ISO déjà monté mais lettre introuvable: $($_.Exception.Message)" "WARN" } } # Monter Write-Log "Montage ISO: $IsoPath" "INFO" $mounted = Mount-DiskImage -ImagePath $IsoPath -PassThru -ErrorAction Stop $driveLetter = ($mounted | Get-Volume | Select-Object -First 1).DriveLetter if (-not $driveLetter) { throw "ISO monté mais lettre lecteur introuvable." } Write-Log "ISO monté: $IsoPath -> $driveLetter`:" "SUCCESS" return $driveLetter } function Dismount-IsoIfMounted { param([string]$IsoPath) try { $img = Get-DiskImage -ImagePath $IsoPath -ErrorAction SilentlyContinue if ($img -and $img.Attached) { Write-Log "Démontage ISO (avant nettoyage) : $IsoPath" "INFO" Dismount-DiskImage -ImagePath $IsoPath -ErrorAction Stop Write-Log "ISO démonté : $IsoPath" "SUCCESS" } else { Write-Log "ISO non monté (rien à démonter) : $IsoPath" "INFO" } } catch { Write-Log "Impossible de démonter l'ISO $IsoPath : $($_.Exception.Message)" "WARN" } } # -------------------- Reboot pending -------------------- function Test-RebootPending { $reasons = New-Object System.Collections.Generic.List[string] if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending") { $reasons.Add("CBS:RebootPending") } if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") { $reasons.Add("WU:RebootRequired") } try { $p = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name "PendingFileRenameOperations" -ErrorAction Stop if ($p.PendingFileRenameOperations -and $p.PendingFileRenameOperations.Count -gt 0) { $reasons.Add("SessionManager:PendingFileRenameOperations") } } catch {} if (Test-Path "HKLM:\SOFTWARE\Microsoft\CCM\RebootPending") { $reasons.Add("ConfigMgr:RebootPending") } [pscustomobject]@{ IsPending = ($reasons.Count -gt 0); Reasons = $reasons } } # -------------------- Compat / bypass -------------------- function Enable-LabConfigBypass { $labConfigPath = "HKLM:\SYSTEM\Setup\LabConfig" if (-not (Test-Path $labConfigPath)) { New-Item -Path $labConfigPath -Force | Out-Null } New-ItemProperty -Path $labConfigPath -Name "BypassTPMCheck" -Value 1 -PropertyType DWord -Force | Out-Null New-ItemProperty -Path $labConfigPath -Name "BypassSecureBootCheck" -Value 1 -PropertyType DWord -Force | Out-Null New-ItemProperty -Path $labConfigPath -Name "BypassRAMCheck" -Value 1 -PropertyType DWord -Force | Out-Null New-ItemProperty -Path $labConfigPath -Name "BypassCPUCheck" -Value 1 -PropertyType DWord -Force | Out-Null Write-Log "LabConfig bypass appliqué (TPM/SecureBoot/CPU/RAM)." "WARN" } function Get-CompatibilityReport { $r=[ordered]@{} $cs=Get-CimInstance Win32_ComputerSystem $ramGB=[math]::Round($cs.TotalPhysicalMemory/1GB,2) $r.RAM_GB=$ramGB; $r.RAM_OK=($ramGB -ge 4) $cpu=Get-CimInstance Win32_Processor | Select-Object -First 1 $r.Cores=$cpu.NumberOfCores; $r.Cores_OK=($cpu.NumberOfCores -ge 2) $sysDrive = Get-PSDrive -Name $env:SystemDrive.TrimEnd(':') $freeGB=[math]::Round($sysDrive.Free/1GB,2) $r.FreeGB=$freeGB; $r.FreeGB_OK=($freeGB -ge $MinFreeSpaceGB) try { $tpm=Get-CimInstance -Namespace root\CIMV2\Security\MicrosoftTpm -ClassName Win32_Tpm -ErrorAction Stop $r.TPM2_0_OK=($tpm.SpecVersion -match '^2\.0') } catch { $r.TPM2_0_OK=$false } $r.SecureBootOn=$false try { $r.SecureBootOn=[bool](Confirm-SecureBootUEFI -ErrorAction Stop) } catch {} $r.Compatible = ($r.RAM_OK -and $r.Cores_OK -and $r.FreeGB_OK -and $r.TPM2_0_OK -and $r.SecureBootOn) return $r } function Is-Windows11 { $os = Get-CimInstance Win32_OperatingSystem return ([int]$os.BuildNumber -ge 22000) } # -------------------- Scheduled tasks (AtLogOn, exclusives) -------------------- function Register-TaskAtLogon { param([string]$TaskName,[string]$ScriptPath,[string]$ExtraArgs) $user = "$env:USERDOMAIN\$env:USERNAME" $args = "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`" $ExtraArgs" Write-Log "Création tâche planifiée '$TaskName' (AtLogOn user=$user) args=$ExtraArgs" "INFO" $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $args $trigger = New-ScheduledTaskTrigger -AtLogOn -User $user $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 4) try { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue | Out-Null } catch {} Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null Write-Log "Tâche '$TaskName' créée." "SUCCESS" } function Remove-TaskSafe([string]$TaskName) { try { if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false | Out-Null Write-Log "Tâche '$TaskName' supprimée." "SUCCESS" } } catch { Write-Log "Suppression tâche '$TaskName' impossible: $($_.Exception.Message)" "WARN" } } function Remove-OtherTask { param([string]$KeepTask) if ($KeepTask -eq $TaskNameReload) { Remove-TaskSafe $TaskNameResume } elseif ($KeepTask -eq $TaskNameResume) { Remove-TaskSafe $TaskNameReload } } function Register-ExclusiveTaskAtLogon { param([string]$TaskName,[string]$ScriptPath,[string]$ExtraArgs) Remove-OtherTask -KeepTask $TaskName Register-TaskAtLogon -TaskName $TaskName -ScriptPath $ScriptPath -ExtraArgs $ExtraArgs } function Cleanup-Temp { $temp = "C:\Temp" if (Test-Path $temp) { try { Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction Stop Write-Log "Nettoyage: $temp supprimé." "SUCCESS" } catch { Write-Log "Nettoyage: suppression $temp partielle/échouée: $($_.Exception.Message)" "WARN" } } } # -------------------- Fin "gracieuse" (cas Win11 déjà là / annulation / erreur) -------------------- function Prompt-CleanupTempWithIsoDismount { param([string]$IsoPath) # Exigence : supprimer la tâche Reload quand on termine ici Remove-TaskSafe $TaskNameReload $ans = Read-HostWithTimeout -Prompt "Supprimer le dossier C:\Temp ? (si oui, l'ISO sera démonté avant) [O/n]" -TimeoutSeconds 20 -Default "O" if ($ans.ToLower().StartsWith("o")) { Dismount-IsoIfMounted -IsoPath $IsoPath Cleanup-Temp } else { Write-Log "Suppression de C:\Temp refusée." "WARN" } } function Exit-Gracefully { param( [int]$Code = 0, [string]$Message = "" ) if ($Message) { Write-Log $Message ($(if($Code -eq 0){"SUCCESS"}else{"ERROR"})) } Prompt-CleanupTempWithIsoDismount -IsoPath $ISOPath exit $Code } # -------------------- Setup runner -------------------- function Start-UpgradeSetup { param([string]$IsoPath) $upgradeStartTime = Get-Date Write-Log ("Début mise à niveau : {0}" -f $upgradeStartTime.ToString("yyyy-MM-dd HH:mm:ss")) "INFO" $driveLetter = Get-OrMount-IsoDriveLetter -IsoPath $IsoPath $setupPath = "$driveLetter`:\setup.exe" if (-not (Test-Path $setupPath)) { throw "setup.exe introuvable: $setupPath" } # Ligne demandée + UI (progression) sans monitoring custom $arguments = "/auto upgrade /eula accept /dynamicupdate enable /compat ignorewarning /showoobe none" Write-Log "Commande: `"$setupPath`" $arguments" "INFO" $p = Start-Process -FilePath $setupPath -ArgumentList $arguments -PassThru Write-Log "setup.exe lancé (PID=$($p.Id))." "INFO" Write-Host "" Write-Host "Installation en cours, veuillez patienter..." -ForegroundColor Cyan Write-Host "La progression est affichée par Windows Setup (/passive)." -ForegroundColor DarkGray Write-Host "En cas de redémarrage, la tâche 'Reload' jouera le son au prochain logon." -ForegroundColor DarkGray Write-Host "" # IMPORTANT : on ne démonte pas l'ISO ici (setup peut en avoir besoin) # La suppression C:\Temp (si choisie) démontera l'ISO avant suppression. } # ============================================================ # MODE RELOAD # ============================================================ if ($Reload) { Write-Log "Mode -Reload (fin/son/cleanup)..." "INFO" # Anti-boucle : supprimer Reload dès l'entrée Remove-TaskSafe $TaskNameReload # Sécurité : il ne doit pas rester Resume Remove-TaskSafe $TaskNameResume $state = Load-State if ($state) { Write-Log "State chargé: StartTime=$($state.StartTime) ISO=$($state.ISOPath)" "INFO" } else { Write-Log "State introuvable ($StateFile)." "WARN" } if (Is-Windows11) { Write-Log "Détection Windows 11: OUI (build>=22000) -> succès." "SUCCESS" Play-SuccessBeep } else { Write-Log "Détection Windows 11: NON -> échec ou upgrade non terminé." "ERROR" Play-ErrorBeep } $ans = Read-HostWithTimeout -Prompt "Supprimer C:\Temp (fichiers + logs) ? (démonte l'ISO si monté) [O/n]" -TimeoutSeconds 20 -Default "O" if ($ans.ToLower().StartsWith("o")) { Dismount-IsoIfMounted -IsoPath $ISOPath Cleanup-Temp } else { Write-Log "Nettoyage C:\Temp refusé." "WARN" } Write-Log "Fin -Reload." "INFO" exit 0 } # ============================================================ # MODE REBOOTPENDING # ============================================================ if ($RebootPending) { Assert-Admin Write-Log "Mode -RebootPending : reprise après reboot pending (démarrage setup)..." "INFO" # Anti-boucle : suppression immédiate de Resume Remove-TaskSafe $TaskNameResume # Préparer la fin : créer Reload (exclusive) Register-ExclusiveTaskAtLogon -TaskName $TaskNameReload -ScriptPath $MyInvocation.MyCommand.Path -ExtraArgs "-Reload" # Re-test reboot pending et reproposition (défaut NON) $rb = Test-RebootPending if ($rb.IsPending) { Write-Log "Reboot pending encore détecté: $($rb.Reasons -join ', ')" "WARN" $a = Read-HostWithTimeout -Prompt "Un redémarrage est encore en attente. Voulez-vous redémarrer à nouveau ? [o/N]" -TimeoutSeconds 30 -Default "N" if ($a.ToLower().StartsWith("o")) { $forceArg = if ($ForceUnsupported) { " -ForceUnsupported" } else { "" } Register-ExclusiveTaskAtLogon -TaskName $TaskNameResume -ScriptPath $MyInvocation.MyCommand.Path ` -ExtraArgs ("-RebootPending -AutoYesUpgrade" + $forceArg) Write-Log "Redémarrage demandé (encore). Reprise via tâche '$TaskNameResume'." "WARN" Restart-Computer -Force exit } else { Write-Log "Choix utilisateur: poursuivre sans redémarrer." "WARN" } } try { Start-UpgradeSetup -IsoPath $ISOPath } catch { Exit-Gracefully -Code 1 -Message ("Erreur -RebootPending: " + $_.Exception.Message) } Write-Log "Fin -RebootPending." "INFO" exit 0 } # ============================================================ # MODE NORMAL # ============================================================ Assert-Admin Write-Log "Démarrage mode normal..." "INFO" Write-Log "ISO=$ISOPath | AutoYes=$AutoYesUpgrade | ForceUnsupported=$ForceUnsupported" "INFO" Save-State @{ StartTime = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") ISOPath = $ISOPath User = "$env:USERDOMAIN\$env:USERNAME" Computer = $env:COMPUTERNAME } Write-Log "State enregistré: $StateFile" "SUCCESS" # Crée Reload (exclusive) Register-ExclusiveTaskAtLogon -TaskName $TaskNameReload -ScriptPath $MyInvocation.MyCommand.Path -ExtraArgs "-Reload" # Reboot pending ? (défaut NON => continuer) $rb = Test-RebootPending if ($rb.IsPending) { Write-Log "Redémarrage en attente détecté: $($rb.Reasons -join ', ')" "WARN" $a = Read-HostWithTimeout -Prompt "Un redémarrage est en attente. Voulez-vous redémarrer maintenant ? [o/N]" -TimeoutSeconds 30 -Default "N" if ($a.ToLower().StartsWith("o")) { $forceArg = if ($ForceUnsupported) { " -ForceUnsupported" } else { "" } Register-ExclusiveTaskAtLogon -TaskName $TaskNameResume -ScriptPath $MyInvocation.MyCommand.Path ` -ExtraArgs ("-RebootPending -AutoYesUpgrade" + $forceArg) Write-Log "Redémarrage demandé. Reprise via tâche '$TaskNameResume' au prochain logon." "WARN" Restart-Computer -Force exit } else { Write-Log "Choix utilisateur: ne pas redémarrer. Poursuite sans reboot." "WARN" } } # Déjà Win11 ? if (Is-Windows11) { Exit-Gracefully -Code 0 -Message "Poste déjà sous Windows 11 -> rien à faire." } # Compat + forçage interactif si nécessaire $compat = Get-CompatibilityReport Write-Log ("Compatibilité (simplifié) : {0}" -f $compat.Compatible) ($(if($compat.Compatible){"SUCCESS"}else{"WARN"})) if (-not $compat.Compatible) { Write-Log "Poste non compatible (simplifié)." "WARN" if ($ForceUnsupported) { Write-Log "Forçage activé (-ForceUnsupported) -> LabConfig bypass." "WARN" Enable-LabConfigBypass } else { $ans = Read-HostWithTimeout -Prompt "Le PC n'est pas compatible Windows 11. Voulez-vous forcer la mise à jour ? [O/n]" -TimeoutSeconds $PromptTimeoutSeconds -Default "N" if ($ans.ToLower().StartsWith("o")) { Write-Log "Forçage accepté -> LabConfig bypass." "WARN" Enable-LabConfigBypass $ForceUnsupported = $true } else { Exit-Gracefully -Code 2 -Message "Forçage refusé. Arrêt de la mise à niveau." } } } # Confirmation lancement $launch = $false if ($AutoYesUpgrade) { $launch = $true Write-Log "AutoYesUpgrade activé : lancement sans question." "WARN" } else { $q = Read-HostWithTimeout -Prompt "Lancer la mise à niveau vers Windows 11 ? [O/n]" -TimeoutSeconds $PromptTimeoutSeconds -Default "O" if ($q.ToLower().StartsWith("o")) { $launch = $true } } if (-not $launch) { # On supprime Resume par sécurité et on termine "gracieusement" (supprime Reload + prompt nettoyage) Remove-TaskSafe $TaskNameResume Exit-Gracefully -Code 0 -Message "Upgrade annulé par l'utilisateur." } # Lance setup (UI /passive). Si reboot pendant l'upgrade, -Reload fera le son/cleanup au prochain logon. try { Start-UpgradeSetup -IsoPath $ISOPath } catch { Exit-Gracefully -Code 1 -Message ("Erreur lancement upgrade: " + $_.Exception.Message) } Write-Log "Fin mode normal (si reboot, -Reload fera le son/cleanup)." "INFO" exit 0