|
<# |
|
.SYNOPSIS |
|
Configures Windows 11 Enterprise to auto-install updates without ever auto-rebooting. |
|
|
|
.DESCRIPTION |
|
- Enables fully automatic download + scheduled install of Windows Updates. |
|
- Disables every known auto-reboot path: logged-on-user reboots, scheduled-time |
|
reboots, deadline-forced reboots, and the UpdateOrchestrator reboot tasks. |
|
- Verifies all settings after applying and prints a pass/fail report. |
|
|
|
.NOTES |
|
Run elevated. Designed for a long-lived VM where you want zero surprise restarts. |
|
#> |
|
|
|
#Requires -RunAsAdministrator |
|
[CmdletBinding()] |
|
param() |
|
|
|
$ErrorActionPreference = 'Stop' |
|
$script:Failures = @() |
|
|
|
function Write-Section($Text) { |
|
Write-Host "" |
|
Write-Host ("=" * 70) -ForegroundColor Cyan |
|
Write-Host $Text -ForegroundColor Cyan |
|
Write-Host ("=" * 70) -ForegroundColor Cyan |
|
} |
|
|
|
# Walks each segment of a registry path and creates anything missing. |
|
# More reliable than `New-Item -Force` which has quirks on some PS 5.1 builds. |
|
function Ensure-RegistryKey { |
|
param([string]$Path) |
|
$segments = $Path -split '\\' |
|
$current = $segments[0] # e.g. 'HKLM:' |
|
for ($i = 1; $i -lt $segments.Count; $i++) { |
|
$current = Join-Path $current $segments[$i] |
|
if (-not (Test-Path -LiteralPath $current)) { |
|
New-Item -Path $current -ErrorAction Stop | Out-Null |
|
Write-Host " Created: $current" -ForegroundColor DarkGray |
|
} |
|
} |
|
if (-not (Test-Path -LiteralPath $Path)) { |
|
throw "Failed to create registry key: $Path" |
|
} |
|
} |
|
|
|
function Set-RegDWord { |
|
param([string]$Path, [string]$Name, [int]$Value) |
|
Set-ItemProperty -Path $Path -Name $Name -Type DWord -Value $Value -ErrorAction Stop |
|
} |
|
|
|
function Test-RegValue { |
|
param([string]$Path, [string]$Name, [int]$Expected, [string]$Description) |
|
try { |
|
$actual = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction Stop).$Name |
|
if ($actual -eq $Expected) { |
|
Write-Host (" [PASS] {0,-55} = {1}" -f $Description, $actual) -ForegroundColor Green |
|
return $true |
|
} else { |
|
Write-Host (" [FAIL] {0,-55} = {1} (expected {2})" -f $Description, $actual, $Expected) -ForegroundColor Red |
|
$script:Failures += "$Description : got $actual, expected $Expected" |
|
return $false |
|
} |
|
} catch { |
|
Write-Host (" [FAIL] {0,-55} = <missing>" -f $Description) -ForegroundColor Red |
|
$script:Failures += "$Description : value missing" |
|
return $false |
|
} |
|
} |
|
|
|
# ---------------------------------------------------------------------------- |
|
# 1. Apply Windows Update policy |
|
# ---------------------------------------------------------------------------- |
|
Write-Section "Step 1: Applying Windows Update policy" |
|
|
|
$WU = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' |
|
$AU = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' |
|
|
|
Ensure-RegistryKey -Path $WU |
|
Ensure-RegistryKey -Path $AU |
|
|
|
# Automatic updates: download + scheduled install, daily at 03:00 |
|
Set-RegDWord $AU 'NoAutoUpdate' 0 |
|
Set-RegDWord $AU 'AUOptions' 4 |
|
Set-RegDWord $AU 'ScheduledInstallDay' 0 |
|
Set-RegDWord $AU 'ScheduledInstallTime' 3 |
|
|
|
# Block auto-reboot |
|
Set-RegDWord $AU 'NoAutoRebootWithLoggedOnUsers' 1 |
|
Set-RegDWord $AU 'AlwaysAutoRebootAtScheduledTime' 0 |
|
|
|
# Kill deadline-forced reboots |
|
Set-RegDWord $WU 'SetAutoRestartDeadline' 0 |
|
Set-RegDWord $WU 'SetAutoRestartRequiredNotificationDismissal' 0 |
|
Set-RegDWord $WU 'SetEDURestart' 0 |
|
|
|
# Active hours (widest allowed: 18h) |
|
Set-RegDWord $WU 'SetActiveHours' 1 |
|
Set-RegDWord $WU 'ActiveHoursStart' 6 |
|
Set-RegDWord $WU 'ActiveHoursEnd' 23 |
|
|
|
Write-Host " Registry policy written." -ForegroundColor Gray |
|
|
|
# ---------------------------------------------------------------------------- |
|
# 2. Disable UpdateOrchestrator reboot tasks (requires SYSTEM context) |
|
# ---------------------------------------------------------------------------- |
|
Write-Section "Step 2: Disabling UpdateOrchestrator reboot tasks (SYSTEM context)" |
|
|
|
$logPath = 'C:\Windows\Temp\disable-uo-reboot.log' |
|
if (Test-Path $logPath) { Remove-Item $logPath -Force } |
|
|
|
$inner = @' |
|
$tasks = @('Reboot','Reboot_AC','Reboot_Battery','Schedule Reboot') |
|
foreach ($t in $tasks) { |
|
try { |
|
Disable-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName $t -ErrorAction Stop | Out-Null |
|
Add-Content C:\Windows\Temp\disable-uo-reboot.log "$(Get-Date -f s) DISABLED: $t" |
|
} catch { |
|
Add-Content C:\Windows\Temp\disable-uo-reboot.log "$(Get-Date -f s) SKIPPED: $t ($($_.Exception.Message))" |
|
} |
|
} |
|
'@ |
|
|
|
$b64 = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($inner)) |
|
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -EncodedCommand $b64" |
|
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(3) |
|
$princ = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest |
|
|
|
Register-ScheduledTask -TaskName '_disable_uo_reboot' -Action $action -Trigger $trigger -Principal $princ -Force | Out-Null |
|
Write-Host " Spawned SYSTEM helper task, waiting for completion..." -ForegroundColor Gray |
|
|
|
# Wait for the helper task to fire and finish (poll for log) |
|
$deadline = (Get-Date).AddSeconds(20) |
|
while ((Get-Date) -lt $deadline) { |
|
Start-Sleep -Milliseconds 500 |
|
if (Test-Path $logPath) { |
|
$lines = (Get-Content $logPath -ErrorAction SilentlyContinue | Measure-Object).Count |
|
if ($lines -ge 1) { Start-Sleep -Seconds 2; break } |
|
} |
|
} |
|
|
|
Unregister-ScheduledTask -TaskName '_disable_uo_reboot' -Confirm:$false -ErrorAction SilentlyContinue |
|
|
|
if (Test-Path $logPath) { |
|
Get-Content $logPath | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } |
|
} else { |
|
Write-Host " (no log produced; tasks may not exist on this build)" -ForegroundColor Yellow |
|
} |
|
|
|
# ---------------------------------------------------------------------------- |
|
# 3. Refresh policy |
|
# ---------------------------------------------------------------------------- |
|
Write-Section "Step 3: Refreshing group policy" |
|
gpupdate /force | Out-Null |
|
Write-Host " gpupdate complete." -ForegroundColor Gray |
|
|
|
# ---------------------------------------------------------------------------- |
|
# 4. Verification - registry |
|
# ---------------------------------------------------------------------------- |
|
Write-Section "Step 4: Verifying registry policy" |
|
|
|
Test-RegValue $AU 'NoAutoUpdate' 0 'AU enabled' | Out-Null |
|
Test-RegValue $AU 'AUOptions' 4 'Auto download + scheduled install' | Out-Null |
|
Test-RegValue $AU 'ScheduledInstallDay' 0 'Install every day' | Out-Null |
|
Test-RegValue $AU 'ScheduledInstallTime' 3 'Install at 03:00' | Out-Null |
|
Test-RegValue $AU 'NoAutoRebootWithLoggedOnUsers' 1 'Block reboot if user logged on' | Out-Null |
|
Test-RegValue $AU 'AlwaysAutoRebootAtScheduledTime' 0 'No auto-reboot at scheduled time' | Out-Null |
|
Test-RegValue $WU 'SetAutoRestartDeadline' 0 'No deadline-forced reboot' | Out-Null |
|
Test-RegValue $WU 'SetAutoRestartRequiredNotificationDismissal' 0 'No restart notif dismissal lock' | Out-Null |
|
Test-RegValue $WU 'SetEDURestart' 0 'No EDU restart override' | Out-Null |
|
Test-RegValue $WU 'SetActiveHours' 1 'Active hours enforced' | Out-Null |
|
Test-RegValue $WU 'ActiveHoursStart' 6 'Active hours start = 06:00' | Out-Null |
|
Test-RegValue $WU 'ActiveHoursEnd' 23 'Active hours end = 23:00' | Out-Null |
|
|
|
# ---------------------------------------------------------------------------- |
|
# 5. Verification - scheduled tasks |
|
# ---------------------------------------------------------------------------- |
|
Write-Section "Step 5: Verifying UpdateOrchestrator reboot tasks" |
|
$uoTasks = @('Reboot','Reboot_AC','Reboot_Battery','Schedule Reboot') |
|
$foundAny = $false |
|
foreach ($t in $uoTasks) { |
|
$task = Get-ScheduledTask -TaskPath '\Microsoft\Windows\UpdateOrchestrator\' -TaskName $t -ErrorAction SilentlyContinue |
|
if ($null -eq $task) { |
|
Write-Host (" [skip] {0,-20} not present on this build" -f $t) -ForegroundColor DarkGray |
|
continue |
|
} |
|
$foundAny = $true |
|
if ($task.State -eq 'Disabled') { |
|
Write-Host (" [PASS] {0,-20} state = Disabled" -f $t) -ForegroundColor Green |
|
} else { |
|
Write-Host (" [FAIL] {0,-20} state = {1}" -f $t, $task.State) -ForegroundColor Red |
|
$script:Failures += "Scheduled task $t is $($task.State), expected Disabled" |
|
} |
|
} |
|
if (-not $foundAny) { |
|
Write-Host " (no UpdateOrchestrator reboot tasks exist on this build - nothing to disable)" -ForegroundColor Yellow |
|
} |
|
|
|
# ---------------------------------------------------------------------------- |
|
# 6. Pending-reboot sanity check |
|
# ---------------------------------------------------------------------------- |
|
Write-Section "Step 6: Pending reboot status" |
|
$pending = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' |
|
if ($pending) { |
|
Write-Host " [WARN] System currently has a pending reboot from prior updates." -ForegroundColor Yellow |
|
Write-Host " Running 'shutdown /a' to cancel any scheduled auto-restart..." -ForegroundColor Yellow |
|
& shutdown /a 2>&1 | Out-Null |
|
Write-Host " Reboot at your discretion to clear the pending state." -ForegroundColor Yellow |
|
} else { |
|
Write-Host " [OK] No pending reboot." -ForegroundColor Green |
|
} |
|
|
|
# ---------------------------------------------------------------------------- |
|
# Summary |
|
# ---------------------------------------------------------------------------- |
|
Write-Section "Summary" |
|
if ($script:Failures.Count -eq 0) { |
|
Write-Host " All checks passed. The VM will auto-install updates and never auto-reboot." -ForegroundColor Green |
|
Write-Host "" |
|
Write-Host " Reminders:" -ForegroundColor Cyan |
|
Write-Host " - Cumulative updates still require a manual reboot (~monthly)." -ForegroundColor Gray |
|
Write-Host " - Defender signature updates flow through their own channel (always on)." -ForegroundColor Gray |
|
Write-Host " - Re-run this script after major Windows updates; MS sometimes re-enables UO reboot tasks." -ForegroundColor Gray |
|
exit 0 |
|
} else { |
|
Write-Host " $($script:Failures.Count) check(s) failed:" -ForegroundColor Red |
|
$script:Failures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } |
|
exit 1 |
|
} |