Runtime/Invoke-OSDStageWindowsUpdate.ps1

[CmdletBinding()]
param([string]$ConfigPath)
$script:OSDStageWindowsUpdateLogPath=$null

function Assert-OSDStageWindowsUpdateConfiguration {
    param([Parameter(Mandatory)]$Configuration)
    $id=[guid]::Empty
    if ($Configuration.SchemaVersion -ne 2 -or
        -not [guid]::TryParse([string]$Configuration.DeploymentId,[ref]$id) -or
        [string]::IsNullOrWhiteSpace([string]$Configuration.ResultPath) -or
        $Configuration.IncludeDrivers -isnot [bool] -or $Configuration.ScanOnly -isnot [bool] -or
        ($Configuration.TimeoutMinutes -isnot [int] -and $Configuration.TimeoutMinutes -isnot [long]) -or
        $Configuration.TimeoutMinutes -lt 5 -or $Configuration.TimeoutMinutes -gt 180) {
        throw 'Invalid Windows Update configuration.'
    }
    if ($Configuration.PSObject.Properties.Name -contains 'DriverClass') {
        $classes=@($Configuration.DriverClass)
        if (($classes.Count -gt 0 -and -not $Configuration.IncludeDrivers) -or
            @($classes | Where-Object { $_ -isnot [string] -or [string]::IsNullOrWhiteSpace($_) -or $_ -match '[*?\[\]]' }).Count -gt 0) {
            throw 'DriverClass must contain exact, nonempty WUA class names and requires IncludeDrivers.'
        }
    }
}

function Write-OSDStageWindowsUpdateMessage {
    param([string]$Message,[ValidateSet('INFO','WARN','ERROR')][string]$Level='INFO')
    $color=switch ($Level) { 'WARN' {'Yellow'} 'ERROR' {'Red'} default {'Cyan'} }
    $line='[{0}] [{1}] Windows Update: {2}' -f (Get-Date -Format 'yyyy-MM-ddTHH:mm:ss'),$Level,$Message
    Write-Host $line -ForegroundColor $color
    if ($script:OSDStageWindowsUpdateLogPath) { Add-Content -LiteralPath $script:OSDStageWindowsUpdateLogPath -Value $line -Encoding UTF8 -ErrorAction Stop }
}

function Get-OSDStageWindowsUpdateRelease {
    $cv=Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop
    [pscustomobject]@{DisplayVersion=[string]$cv.DisplayVersion;Build=[string]$cv.CurrentBuildNumber;Revision=$cv.UBR;InstallationType=[string]$cv.InstallationType}
}

function Assert-OSDStageWindowsUpdateContext {
    param([switch]$OobeTest)
    if ((Test-Path -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\MiniNT') -or
        -not [Environment]::Is64BitProcess) {
        throw 'Windows Update runtime requires native 64-bit PowerShell as SYSTEM during SetupComplete.'
    }
    $identity=[Security.Principal.WindowsIdentity]::GetCurrent()
    $principal=[Security.Principal.WindowsPrincipal]::new($identity)
    if ($identity.User.Value -ne 'S-1-5-18' -and
        -not ($OobeTest -and $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {
        throw 'Use native 64-bit PowerShell as SYSTEM for SetupComplete, or an elevated console for the explicit OOBE test.'
    }
    $release=Get-OSDStageWindowsUpdateRelease
    if ($release.InstallationType -ne 'Client' -or [int]$release.Build -lt 26200) {
        throw 'Windows Update requires Windows 11 25H2 or later (client build 26200+).'
    }
}

function Get-OSDStageWindowsUpdateSelection {
    param([Parameter(Mandatory)]$Update,[bool]$IncludeDrivers=$false,[int]$Depth=0,
        [string[]]$ParentSoftwareClassifications=@(),[string[]]$DriverClass=@())
    # Category IDs are language independent. Root updates require their own
    # classification. Unclassified software members can use a checked software
    # parent's classification; explicit disallowed metadata is never overridden.
    # Regular quality servicing includes Updates and Update Rollups (for example
    # .NET servicing and MSRT). Upgrades/Feature Packs remain excluded; root
    # software also requires automatic, non-browse-only selection and title checks.
    $safe=@('0fa1201d-4330-4fa8-8ae9-b877473b6441', # Security Updates
        'e6cf1350-c01b-414d-a61f-263d14d133b4', # Critical Updates
        'e0789628-ce08-4437-be74-2495b842f43b', # Definition Updates
        '28bc880e-0592-4cbf-8f95-c79b17911d5f', # Update Rollups
        'cd5ffd1e-e932-4e3a-bf74-18bf0b1bbd83') # Updates
    $reason='MissingOrUnsupportedMetadata';$selected=$false
    $categories=@();$effectiveCategories=@();$classificationSource='None'
    $bundleDecisions=[Collections.Generic.List[object]]::new()
    try {
        if ($Depth -gt 16) { throw 'BundleDepthExceeded' }
        if ($Update.IsInstalled -ne $false -or $Update.IsHidden -ne $false) { throw 'NotApplicable' }
        if ([int]$Update.Type -eq 2 -and -not $IncludeDrivers) { throw 'DriversDisabled' }
        if ($Depth -eq 0 -and [int]$Update.Type -ne 2) {
            if ($Update.BrowseOnly -ne $false -or $Update.AutoSelectOnWebSites -ne $true) { throw 'NotAutomatic' }
        }
        # RebootBehavior=1 means a restart is required AFTER installation, not
        # that WUA should initiate it. Cumulative updates must remain eligible.
        $behavior=$Update.InstallationBehavior
        if ($null -eq $behavior) { throw 'InstallationBehaviorMissing' }
        if ($behavior.CanRequestUserInput -isnot [bool]) { throw 'InteractionMetadataUnknown' }
        if ($null -eq $behavior.RebootBehavior -or
            [int]$behavior.RebootBehavior -notin @(0,1,2)) { throw 'RebootBehaviorUnknown' }
        # Explicitly opted-in drivers may advertise UI capability. Installation
        # still MUST use ForceQuiet, with no interactive fallback. WUA rejects
        # updates that cannot honor it; the runtime records failure and stops.
        if ($behavior.CanRequestUserInput -and
            -not ([int]$Update.Type -eq 2 -and $IncludeDrivers)) { throw 'InteractiveSoftwareUpdate' }
        if ([string]$Update.Title -match '(?i)preview|voorvertoning|vorschau|aper\u00e7u|enablement|feature\s+update|functie.update') { throw 'ExcludedTitle' }
        if ($null -eq $Update.Categories) { throw 'ClassificationMetadataMissing' }
        $categories=@($Update.Categories | Where-Object Type -eq 'UpdateClassification' | ForEach-Object { [string]$_.CategoryID })
        $effectiveCategories=$categories
        if ($categories.Count -gt 0) { $classificationSource='Own' }
        if ([int]$Update.Type -eq 1) {
            if ([string]$Update.Title -match '(?i)firmware|\bBIOS\b|\bUEFI\b') { throw 'ExcludedTitle' }
            if ($categories.Count -eq 0 -and $Depth -gt 0 -and
                $ParentSoftwareClassifications.Count -gt 0 -and
                @($ParentSoftwareClassifications | Where-Object { $_ -notin $safe }).Count -eq 0) {
                $effectiveCategories=@($ParentSoftwareClassifications)
                $classificationSource='ParentBundle'
            }
            if ($effectiveCategories.Count -eq 0 -or @($effectiveCategories | Where-Object { $_ -notin $safe }).Count -gt 0) { throw 'ClassificationNotAllowed' }
        }
        elseif ([int]$Update.Type -eq 2) {
            # Explicit opt-in includes ALL WUA driver classes, including firmware.
            # An optional exact-name allowlist narrows root and member offers.
            # Hardware IDs and titles are evidence, not a home-grown PnP ranker.
            if ([string]::IsNullOrWhiteSpace([string]$Update.DriverClass)) { throw 'DriverClassMissing' }
            if ($DriverClass.Count -gt 0 -and [string]$Update.DriverClass -notin $DriverClass) { throw 'DriverClassNotIncluded' }
            if ($categories.Count -eq 0 -or @($categories | Where-Object { $_ -ne 'ebfc1fc5-71a4-4f7b-9aca-3b9a503104a0' }).Count -gt 0) { throw 'DriverClassificationNotAllowed' }
        }
        else { throw 'UpdateTypeNotAllowed' }
        if ($null -eq $Update.BundledUpdates) { throw 'BundleMetadataMissing' }
        $childClassifications=@()
        if ([int]$Update.Type -eq 1) { $childClassifications=@($effectiveCategories) }
        $bundleRejection=$null
        foreach ($child in $Update.BundledUpdates) {
            $decision=Get-OSDStageWindowsUpdateSelection -Update $child -IncludeDrivers $IncludeDrivers -Depth ($Depth+1) -ParentSoftwareClassifications $childClassifications -DriverClass $DriverClass
            $bundleDecisions.Add([pscustomobject]@{
                UpdateId=[string]$child.Identity.UpdateID;Title=[string]$child.Title;Type=[int]$child.Type
                DriverClass=$(if ([int]$child.Type -eq 2) { [string]$child.DriverClass } else { $null })
                DriverHardwareId=$(if ([int]$child.Type -eq 2) { [string]$child.DriverHardwareID } else { $null })
                Selected=$decision.Selected;Reason=$decision.Reason
                ClassificationIds=$decision.ClassificationIds;EffectiveClassificationIds=$decision.EffectiveClassificationIds
                ClassificationSource=$decision.ClassificationSource;Children=$decision.BundleDecisions
            })
            if (-not $decision.Selected -and -not $bundleRejection) { $bundleRejection='BundleRejected: ' + $decision.Reason }
        }
        if ($bundleRejection) { throw $bundleRejection }
        $selected=$true;$reason=if ([int]$Update.Type -eq 2) {'OptedInHardwareDriver'} else {'AllowedQualityClassification'}
    }
    catch { $reason=$_.Exception.Message }
    [pscustomobject]@{
        Selected=$selected;Reason=$reason;ClassificationIds=@($categories)
        EffectiveClassificationIds=@($effectiveCategories);ClassificationSource=$classificationSource
        BundleDecisions=@($bundleDecisions.ToArray())
    }
}

function Get-OSDStageWindowsUpdateSelections {
    param([AllowEmptyCollection()][object[]]$Updates,[bool]$IncludeDrivers=$false,[string[]]$DriverClass=@())
    # Do not discard distinct updates just because their hardware IDs match.
    # Keep WUA identities and delegate installation to WUA; no version/title sorting.
    foreach ($update in $Updates) {
        [pscustomobject]@{Update=$update;Decision=(Get-OSDStageWindowsUpdateSelection -Update $update -IncludeDrivers $IncludeDrivers -DriverClass $DriverClass)}
    }
}

function New-OSDStageWindowsUpdateComObject {
    param([Parameter(Mandatory)][string]$ProgId)
    New-Object -ComObject $ProgId -ErrorAction Stop
}

function Test-OSDStageWindowsUpdateRebootPrerequisite {
    param([Parameter(Mandatory)]$Installer)
    $required=$Installer.RebootRequiredBeforeInstallation
    if ($required -isnot [bool]) {
        throw 'WUA did not return a Boolean reboot prerequisite; installation was not attempted.'
    }
    $required
}

function New-OSDStageWindowsUpdateCallback {
    # WUA supports an Automation callback with DISPID 0. The managed callback
    # never enters a PowerShell runspace on a COM worker thread. The caller polls.
    if (-not ('OSDStage.WuaCallback' -as [type])) {
        Add-Type -TypeDefinition @'
using System.Runtime.InteropServices;
namespace OSDStage {
    [ComVisible(true), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IWuaCallback {
        [DispId(0)] void Invoke(object job, object args);
    }
    [ComVisible(true), ClassInterface(ClassInterfaceType.None)]
    public sealed class WuaCallback : IWuaCallback {
        public void Invoke(object job, object args) { }
    }
}
'@
 -ErrorAction Stop
    }
    New-Object OSDStage.WuaCallback
}

function Invoke-OSDStageWindowsUpdateAsync {
    param([Parameter(Mandatory)]$Agent,[ValidateSet('Search','Download','Install')][string]$Phase,
        [int]$TimeoutMinutes,[string]$Criteria="IsInstalled=0 and IsHidden=0")
    $callback=New-OSDStageWindowsUpdateCallback
    $job=$null;$timedOut=$false;$watch=[Diagnostics.Stopwatch]::StartNew();$nextHeartbeat=15
    try {
        $job=switch ($Phase) {
            'Search' { $Agent.BeginSearch($Criteria,$callback,$null) }
            'Download' { $Agent.BeginDownload($callback,$callback,$null) }
            'Install' { $Agent.BeginInstall($callback,$callback,$null) }
        }
        while (-not $job.IsCompleted) {
            if (-not $timedOut -and $watch.Elapsed.TotalMinutes -ge $TimeoutMinutes) {
                $timedOut=$true
                Write-OSDStageWindowsUpdateMessage "$Phase exceeded $TimeoutMinutes minutes; requesting cancellation and waiting for WUA to finish safely." WARN
                $job.RequestAbort()
            }
            if ($watch.Elapsed.TotalSeconds -ge $nextHeartbeat) {
                Write-OSDStageWindowsUpdateMessage ("{0} running ({1}s); cancellation requested: {2}." -f $Phase,[int]$watch.Elapsed.TotalSeconds,$timedOut)
                $nextHeartbeat+=15
            }
            Start-Sleep -Milliseconds 500
        }
        $result=switch ($Phase) {
            'Search' { $Agent.EndSearch($job) }
            'Download' { $Agent.EndDownload($job) }
            'Install' { $Agent.EndInstall($job) }
        }
        [pscustomobject]@{Result=$result;TimedOut=$timedOut;DurationSeconds=[math]::Round($watch.Elapsed.TotalSeconds,1)}
    }
    finally {
        # CleanUp waits for quiescence, including when a COM call throws. Never
        # kill servicing or leave an installer running behind the wrapper.
        if ($null -ne $job) { $job.CleanUp() }
        $watch.Stop()
    }
}

function Save-OSDStageWindowsUpdateReport {
    param([Parameter(Mandatory)]$Report,[Parameter(Mandatory)][string]$Path)
    $resolved=[Environment]::ExpandEnvironmentVariables($Path)
    New-Item -ItemType Directory -Path (Split-Path $resolved -Parent) -Force | Out-Null
    $temp=$resolved+'.'+[guid]::NewGuid().ToString('N')+'.tmp'
    try {
        $Report | ConvertTo-Json -Depth 15 | Set-Content -LiteralPath $temp -Encoding UTF8 -ErrorAction Stop
        Move-Item -LiteralPath $temp -Destination $resolved -Force -ErrorAction Stop
    }
    finally { if (Test-Path -LiteralPath $temp) { Remove-Item -LiteralPath $temp -Force } }
}

function Invoke-OSDStageWindowsUpdateConfiguration {
    param([Parameter(Mandatory)]$Configuration,[switch]$OobeTest)
    Assert-OSDStageWindowsUpdateConfiguration $Configuration
    $driverClasses=@()
    if ($Configuration.PSObject.Properties.Name -contains 'DriverClass') { $driverClasses=@($Configuration.DriverClass) }
    $previousLogPath=$script:OSDStageWindowsUpdateLogPath
    $resolvedResult=[Environment]::ExpandEnvironmentVariables([string]$Configuration.ResultPath)
    $script:OSDStageWindowsUpdateLogPath=[IO.Path]::ChangeExtension($resolvedResult,'.log')
    New-Item -ItemType Directory -Path (Split-Path $resolvedResult -Parent) -Force | Out-Null
    Set-Content -LiteralPath $script:OSDStageWindowsUpdateLogPath -Value '' -NoNewline -Encoding UTF8 -ErrorAction Stop
    $watch=[Diagnostics.Stopwatch]::StartNew()
    $report=[ordered]@{
        SchemaVersion=2;DeploymentId=[string]$Configuration.DeploymentId
        Phase=$(if ($OobeTest) {'ManualOobeTest'} else {'LastWithinSetupComplete'});Status='Running';Outcome='Running';ScanOnly=$Configuration.ScanOnly
        IncludeDrivers=$Configuration.IncludeDrivers;DriverClass=$driverClasses;Source='PolicyDefault';SelectionPolicy='QualityAndDriversV5'
        StartedAtUtc=[DateTime]::UtcNow.ToString('o');CompletedAtUtc=$null;DurationSeconds=0
        LogPath=$script:OSDStageWindowsUpdateLogPath
        ReleaseBefore=$null;ReleaseAfter=$null;ReleaseUnchanged=$null
        SearchResultCode=$null;FoundCount=0;SelectedCount=0;InstalledCount=0
        RebootRequired=$false;RebootStateKnown=$false;RebootRequested=$false;TimedOut=$false;FurtherScanRequired=$true
        ExecutionPolicy='ContinueUntilRebootPrerequisiteV1';InstallationBlockedByReboot=$false
        Items=@();Error=$null
    }
    try {
        Assert-OSDStageWindowsUpdateContext -OobeTest:$OobeTest
        $report.ReleaseBefore=Get-OSDStageWindowsUpdateRelease
        Save-OSDStageWindowsUpdateReport $report $Configuration.ResultPath
        $session=New-OSDStageWindowsUpdateComObject 'Microsoft.Update.Session'
        $session.ClientApplicationID='OSDStage'
        $searcher=$session.CreateUpdateSearcher()
        # Leave ServerSelection, ServiceID and all update policies unchanged.
        Write-OSDStageWindowsUpdateMessage 'Searching the configured update source.'
        $search=Invoke-OSDStageWindowsUpdateAsync -Agent $searcher -Phase Search -TimeoutMinutes $Configuration.TimeoutMinutes
        $report.SearchResultCode=[int]$search.Result.ResultCode;$report.TimedOut=$search.TimedOut
        if ($search.TimedOut -or $report.SearchResultCode -ne 2) { throw 'Update search did not complete successfully; no updates were installed.' }
        $candidates=@($search.Result.Updates)
        $report.FoundCount=$candidates.Count
        $selected=[Collections.Generic.List[object]]::new()
        $items=[Collections.Generic.List[object]]::new()
        foreach ($candidate in @(Get-OSDStageWindowsUpdateSelections -Updates $candidates -IncludeDrivers $Configuration.IncludeDrivers -DriverClass $driverClasses)) {
            $update=$candidate.Update;$decision=$candidate.Decision
            $item=[pscustomobject]@{
                UpdateId=[string]$update.Identity.UpdateID;Revision=$update.Identity.RevisionNumber
                Title=[string]$update.Title;Type=[int]$update.Type
                BrowseOnly=$update.BrowseOnly;AutoSelectOnWebSites=$update.AutoSelectOnWebSites
                CanRequestUserInput=$update.InstallationBehavior.CanRequestUserInput
                RebootBehavior=$update.InstallationBehavior.RebootBehavior
                DriverClass=$(if ([int]$update.Type -eq 2) { [string]$update.DriverClass } else { $null })
                DriverHardwareId=$(if ([int]$update.Type -eq 2) { [string]$update.DriverHardwareID } else { $null })
                KBArticleIDs=@($update.KBArticleIDs);BundleDecisions=$decision.BundleDecisions
                Categories=@($update.Categories | ForEach-Object { [pscustomobject]@{Id=[string]$_.CategoryID;Type=[string]$_.Type;Name=[string]$_.Name} })
                Selected=$decision.Selected;SelectionReason=$decision.Reason
                Status=if ($decision.Selected) {'Selected'} else {'Skipped'}
                DownloadResultCode=$null;DownloadHResult=$null;InstallResultCode=$null;InstallHResult=$null;RebootRequired=$false
            }
            $items.Add($item)
            if ($decision.Selected) { $selected.Add([pscustomobject]@{Update=$update;Item=$item}) }
            Write-OSDStageWindowsUpdateMessage ("{0}: {1} ({2})." -f $item.Status,$item.Title,$decision.Reason)
        }
        $report.Items=@($items.ToArray());$report.SelectedCount=$selected.Count
        Save-OSDStageWindowsUpdateReport $report $Configuration.ResultPath
        if ($Configuration.ScanOnly) { $report.Status='Succeeded';$report.Outcome='ScanOnly' }
        elseif ($selected.Count -eq 0) { $report.Status='Succeeded';$report.Outcome='NoEligibleUpdates';$report.FurtherScanRequired=$false }
        else {
            $installer=$session.CreateUpdateInstaller()
            $installer.AllowSourcePrompts=$false;$installer.ForceQuiet=$true
            if ($installer.IsBusy) { throw 'Windows Update installer is busy; no concurrent installation was started.' }
            if (Test-OSDStageWindowsUpdateRebootPrerequisite -Installer $installer) {
                $report.InstallationBlockedByReboot=$true
                $report.RebootRequired=$true;$report.Status='Deferred';$report.Outcome='RebootRequiredBeforeInstallation'
                foreach ($entry in $selected) { $entry.Item.Status='Deferred' }
            }
            else {
                $downloads=New-OSDStageWindowsUpdateComObject 'Microsoft.Update.UpdateColl'
                foreach ($entry in $selected) {
                    if (-not $entry.Update.EulaAccepted) { $entry.Update.AcceptEula() }
                    [void]$downloads.Add($entry.Update)
                }
                $downloader=$session.CreateUpdateDownloader();$downloader.Updates=$downloads
                foreach ($entry in $selected) { $entry.Item.Status='Downloading' }
                Save-OSDStageWindowsUpdateReport $report $Configuration.ResultPath
                Write-OSDStageWindowsUpdateMessage ("Downloading {0} selected update(s) before any driver changes." -f $selected.Count)
                $download=Invoke-OSDStageWindowsUpdateAsync -Agent $downloader -Phase Download -TimeoutMinutes $Configuration.TimeoutMinutes
                $report.TimedOut=$download.TimedOut
                for ($i=0;$i -lt $selected.Count;$i++) {
                    $dr=$download.Result.GetUpdateResult($i)
                    $selected[$i].Item.DownloadResultCode=[int]$dr.ResultCode;$selected[$i].Item.DownloadHResult=[int]$dr.HResult
                    $selected[$i].Item.Status=if ([int]$dr.ResultCode -eq 2 -and $selected[$i].Update.IsDownloaded) {'Downloaded'} else {'DownloadFailed'}
                }
                Save-OSDStageWindowsUpdateReport $report $Configuration.ResultPath
                if ($download.TimedOut -or [int]$download.Result.ResultCode -ne 2 -or @($selected | Where-Object {$_.Item.Status -ne 'Downloaded'}).Count -gt 0) {
                    throw 'Download incomplete; no installations were started.'
                }
                # Software before drivers. Install individually for accurate results,
                # exclusive installers and a reboot prerequisite check between updates.
                # Sort-Object is not stable on Windows PowerShell 5.1. Preserve
                # discovery order within each type when partitioning the queue.
                $installQueue=@($selected | Where-Object { [int]$_.Update.Type -eq 1 }) +
                    @($selected | Where-Object { [int]$_.Update.Type -eq 2 })
                foreach ($entry in $installQueue) {
                    if ($report.InstallationBlockedByReboot) { $entry.Item.Status='Deferred';continue }
                    if ($installer.IsBusy) { throw 'Windows Update installer became busy; no concurrent installation was started.' }
                    $single=New-OSDStageWindowsUpdateComObject 'Microsoft.Update.UpdateColl'
                    [void]$single.Add($entry.Update);$installer.Updates=$single
                    # A successful update may need a restart to finish without
                    # blocking further installation. Keep the aggregate flag,
                    # but defer only when WUA says a restart is a prerequisite.
                    if (Test-OSDStageWindowsUpdateRebootPrerequisite -Installer $installer) {
                        $report.InstallationBlockedByReboot=$true
                        $report.RebootRequired=$true;$entry.Item.Status='Deferred'
                        Write-OSDStageWindowsUpdateMessage 'WUA requires a restart before further installation; remaining updates are deferred. No restart requested.' WARN
                        continue
                    }
                    $entry.Item.Status='Installing';Save-OSDStageWindowsUpdateReport $report $Configuration.ResultPath
                    Write-OSDStageWindowsUpdateMessage ('Installing '+$entry.Item.Title)
                    $install=Invoke-OSDStageWindowsUpdateAsync -Agent $installer -Phase Install -TimeoutMinutes $Configuration.TimeoutMinutes
                    $ir=$install.Result.GetUpdateResult(0)
                    $entry.Item.InstallResultCode=[int]$ir.ResultCode;$entry.Item.InstallHResult=[int]$ir.HResult
                    $entry.Item.RebootRequired=([bool]$ir.RebootRequired -or [bool]$install.Result.RebootRequired)
                    $report.RebootRequired=$report.RebootRequired -or $entry.Item.RebootRequired
                    $entry.Item.Status=if ([int]$ir.ResultCode -eq 2) {'Installed'} else {'InstallFailed'}
                    if ($entry.Item.Status -eq 'Installed') { $report.InstalledCount++ }
                    $report.TimedOut=$install.TimedOut
                    Save-OSDStageWindowsUpdateReport $report $Configuration.ResultPath
                    if ($install.TimedOut -or [int]$ir.ResultCode -ne 2 -or [int]$install.Result.ResultCode -ne 2) { throw 'Installation did not complete successfully; remaining updates were not attempted.' }
                    if ($entry.Item.RebootRequired) {
                        Write-OSDStageWindowsUpdateMessage 'Installed update requires a later restart. Checking WUA prerequisites before the next update; no restart requested.'
                    }
                }
                $report.Status=if (@($selected | Where-Object {$_.Item.Status -eq 'Deferred'}).Count -gt 0) {'Deferred'} else {'Succeeded'}
                $report.Outcome=if ($report.RebootRequired) {'RebootRequired'} else {'SelectedUpdatesInstalled'}
            }
        }
    }
    catch { $report.Status='Failed';$report.Outcome='Failed';$report.Error=$_.Exception.Message;Write-OSDStageWindowsUpdateMessage $report.Error ERROR }
    finally {
        if ($null -ne $report.ReleaseBefore) {
            try {
                # Preserve a reboot requirement even if EndInstall throws after
                # servicing changed the machine, or it already existed at scan time.
                $systemInfo=New-OSDStageWindowsUpdateComObject 'Microsoft.Update.SystemInfo'
                $pendingReboot=$systemInfo.RebootRequired
                if ($pendingReboot -isnot [bool]) { throw 'WUA did not return a Boolean reboot state.' }
                $report.RebootRequired=$report.RebootRequired -or $pendingReboot
                $report.RebootStateKnown=$true
            }
            catch { Write-OSDStageWindowsUpdateMessage ('Unable to query final reboot state: '+$_.Exception.Message) WARN }
            try {
                $report.ReleaseAfter=Get-OSDStageWindowsUpdateRelease
                $report.ReleaseUnchanged=($report.ReleaseBefore.DisplayVersion -eq $report.ReleaseAfter.DisplayVersion -and $report.ReleaseBefore.Build -eq $report.ReleaseAfter.Build)
                if (-not $report.ReleaseUnchanged) { $report.Status='Failed';$report.Outcome='ReleaseChanged';$report.Error='Windows release/build changed during the update pass.' }
            }
            catch { $report.Status='Failed';$report.Error=$_.Exception.Message }
        }
        foreach ($item in $report.Items) {
            if ($item.Status -eq 'Installing') { $item.Status='InstallOutcomeUnknown' }
            elseif ($item.Status -eq 'Downloading') { $item.Status='DownloadOutcomeUnknown' }
            elseif ($item.Status -eq 'Downloaded' -or ($item.Status -eq 'Selected' -and -not $Configuration.ScanOnly)) { $item.Status='NotAttempted' }
        }
        $watch.Stop();$report.DurationSeconds=[math]::Round($watch.Elapsed.TotalSeconds,1);$report.CompletedAtUtc=[DateTime]::UtcNow.ToString('o')
        Save-OSDStageWindowsUpdateReport $report $Configuration.ResultPath
    }
    Write-OSDStageWindowsUpdateMessage ("{0}: {1}/{2} installed; reboot required: {3}; {4}s. No reboot requested." -f $report.Outcome,$report.InstalledCount,$report.SelectedCount,$report.RebootRequired,$report.DurationSeconds)
    $script:OSDStageWindowsUpdateLogPath=$previousLogPath
    [pscustomobject]$report
}

if ($MyInvocation.InvocationName -ne '.' -and $ConfigPath) {
    $ErrorActionPreference='Stop'
    try {
        $configuration=Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
        $result=Invoke-OSDStageWindowsUpdateConfiguration $configuration
        if ($result.Status -eq 'Failed') { exit 1 }
        if ($result.RebootRequired) { exit 3010 }
        exit 0
    }
    catch { Write-Error -ErrorRecord $_;exit 1 }
}