Runtime/Invoke-OSDStageWindowsCapabilities.ps1

[CmdletBinding()]
param([string]$ConfigPath)

function Assert-OSDStageWindowsCapabilityName {
    param([AllowNull()][AllowEmptyString()][string]$Name)
    # Literal DISM identity, including the version/locale separators. No wildcards
    # or native command-line delimiters; do not maintain a capability allowlist.
    if ($Name -notmatch '\A[A-Za-z0-9][A-Za-z0-9._~-]{0,255}\z' -or $Name -notlike '*~*') {
        throw 'Specify an exact capability identity from Get-WindowsCapability, without whitespace, quotes or wildcards.'
    }
}

function Assert-OSDStageWindowsCapabilityConfiguration {
    param([Parameter(Mandatory)]$Configuration)
    $id=[guid]::Empty
    if ([int]$Configuration.SchemaVersion -ne 2 -or
        -not [guid]::TryParse([string]$Configuration.DeploymentId,[ref]$id) -or
        [string]::IsNullOrWhiteSpace([string]$Configuration.ResultPath) -or
        $Configuration.Operations -isnot [System.Collections.IList] -or
        @($Configuration.Operations).Count -lt 1 -or @($Configuration.Operations).Count -gt 2048) {
        throw 'Invalid Windows Capabilities configuration.'
    }
    $seen=@{}
    foreach ($operation in $Configuration.Operations) {
        Assert-OSDStageWindowsCapabilityName -Name ([string]$operation.Name)
        if ([string]$operation.Action -notin @('Add','Remove')) { throw 'Capability action must be Add or Remove.' }
        if ($seen.ContainsKey([string]$operation.Name)) { throw 'Duplicate Windows capability operation.' }
        $seen[[string]$operation.Name]=$true
    }
}

function Assert-OSDStageWindowsCapabilityVersion {
    param([Parameter(Mandatory)]$CurrentVersion)
    if ([string]$CurrentVersion.InstallationType -ne 'Client' -or [int]$CurrentVersion.CurrentBuildNumber -lt 26200) {
        throw 'Windows capabilities require Windows 11 25H2 or later (client build 26200+).'
    }
}

function Assert-OSDStageWindowsCapabilityContext {
    if (Test-Path -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\MiniNT') { throw 'Windows capabilities runtime requires full Windows, not WinPE.' }
    if (-not [Environment]::Is64BitProcess -or [Security.Principal.WindowsIdentity]::GetCurrent().User.Value -ne 'S-1-5-18') {
        throw 'Windows capabilities runtime requires native 64-bit PowerShell as SYSTEM during SetupComplete.'
    }
    $cv=Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop
    Assert-OSDStageWindowsCapabilityVersion -CurrentVersion $cv
}

function Get-OSDStageWindowsCapabilityState {
    param([Parameter(Mandatory)][string]$Name)
    Assert-OSDStageWindowsCapabilityName -Name $Name
    $capabilityMatches=@(Get-WindowsCapability -Online -Name $Name -ErrorAction Stop | Where-Object { [string]$_.Name -ieq $Name })
    if ($capabilityMatches.Count -ne 1) { throw "Capability '$Name' is not available on this Windows image." }
    [string]$capabilityMatches[0].State
}

function Invoke-OSDStageCapabilityDism {
    param(
        [Parameter(Mandatory)][ValidateSet('Add','Remove')][string]$Action,
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][string]$LogPath,
        [ValidateRange(1,3600)][int]$TimeoutSeconds=1800
    )
    Assert-OSDStageWindowsCapabilityName -Name $Name
    if (-not [IO.Path]::IsPathRooted($LogPath) -or $LogPath -match '["\x00-\x1F]' -or $LogPath.EndsWith('\')) {
        throw 'DISM log path must be an absolute file path without quotes or control characters.'
    }
    # Add-WindowsCapability has no NoRestart switch. Use the native DISM switch
    # explicitly, while leaving source/update policy selection to Windows.
    $process=[Diagnostics.Process]::new()
    try {
        $process.StartInfo.FileName=Join-Path $env:SystemRoot 'System32\dism.exe'
        $process.StartInfo.Arguments='/Online /{0}-Capability /CapabilityName:{1} /NoRestart /English /LogPath:"{2}"' -f $Action,$Name,$LogPath
        $process.StartInfo.UseShellExecute=$false;$process.StartInfo.CreateNoWindow=$true
        $process.StartInfo.RedirectStandardOutput=$true;$process.StartInfo.RedirectStandardError=$true
        if (-not $process.Start()) { throw 'Unable to start capability servicing.' }
        $stdout=$process.StandardOutput.ReadToEndAsync();$stderr=$process.StandardError.ReadToEndAsync()
        if (-not $process.WaitForExit($TimeoutSeconds*1000)) {
            $process.Kill();$null=$process.WaitForExit(5000)
            throw [TimeoutException]::new('Capability servicing timed out. Windows servicing may still be active; no further operations, retry or reboot are requested.')
        }
        if (-not $stdout.Wait(5000) -or -not $stderr.Wait(5000)) { throw [TimeoutException]::new('DISM output capture timed out; no further operations are requested.') }
        [pscustomobject]@{ExitCode=$process.ExitCode;StandardOutput=$stdout.Result;StandardError=$stderr.Result;LogPath=$LogPath}
    }
    finally { $process.Dispose() }
}

function Invoke-OSDStageWindowsCapabilityOperation {
    param([Parameter(Mandatory)]$Operation,[Parameter(Mandatory)][string]$LogPath)
    $name=[string]$Operation.Name;$action=[string]$Operation.Action
    $record=[ordered]@{
        Name=$name;Action=$action;Status='Failed';Outcome='Failed';StateBefore=$null;StateAfter=$null
        ExitCode=$null;RestartNeeded=$false;TimedOut=$false;DismLogPath=$null;Error=$null
    }
    try {
        if ($action -notin @('Add','Remove')) { throw 'Capability action must be Add or Remove.' }
        $record.StateBefore=Get-OSDStageWindowsCapabilityState -Name $name
        $record.StateAfter=$record.StateBefore
        $desired=if ($action -eq 'Add') { 'Installed' } else { 'NotPresent' }
        if ($record.StateBefore -eq $desired) {
            $record.Status='Succeeded';$record.Outcome=if ($action -eq 'Add') { 'AlreadyInstalled' } else { 'AlreadyAbsent' }
            return [pscustomobject]$record
        }
        if ($record.StateBefore -notin @('Installed','NotPresent','Staged','Resolved')) {
            if ($record.StateBefore -match 'Pending') { $record.RestartNeeded=$true }
            throw ('Capability is in state {0}; no servicing attempt was made.' -f $record.StateBefore)
        }
        $record.DismLogPath=$LogPath
        $record.StateAfter=$null
        $native=Invoke-OSDStageCapabilityDism -Action $action -Name $name -LogPath $LogPath
        $record.ExitCode=$native.ExitCode
        $record.RestartNeeded=($native.ExitCode -eq 3010)
        if ($native.ExitCode -notin @(0,3010)) { throw ('DISM {0}-Capability failed with exit code {1}. See {2}' -f $action,$native.ExitCode,$LogPath) }
        $record.StateAfter=Get-OSDStageWindowsCapabilityState -Name $name
        if ($record.StateAfter -ne $desired) {
            if ($record.RestartNeeded -or $record.StateAfter -match 'Pending') {
                $record.RestartNeeded=$true;$record.Status='Succeeded';$record.Outcome='PendingRestart'
            } else { throw ('DISM returned success but capability state is {0}, expected {1}.' -f $record.StateAfter,$desired) }
        } else {
            $record.Status='Succeeded';$record.Outcome=if ($action -eq 'Add') { 'Installed' } else { 'Removed' }
        }
    }
    catch {
        $record.Error=$_.Exception.Message
        $record.TimedOut=($_.Exception -is [TimeoutException])
    }
    [pscustomobject]$record
}

function Invoke-OSDStageWindowsCapabilityConfiguration {
    param([Parameter(Mandatory)]$Configuration)
    Assert-OSDStageWindowsCapabilityConfiguration -Configuration $Configuration
    $record=[ordered]@{
        SchemaVersion=2;DeploymentId=[string]$Configuration.DeploymentId;Status='Failed'
        OperationCount=@($Configuration.Operations).Count;SuccessfulOperationCount=0;FailedOperationCount=0
        RestartNeeded=$false;RebootRequested=$false;Items=@();Error=$null
    }
    try {
        Assert-OSDStageWindowsCapabilityContext
        $resultPath=[Environment]::ExpandEnvironmentVariables([string]$Configuration.ResultPath)
        $logDirectory=Split-Path $resultPath -Parent
        New-Item -ItemType Directory -Path $logDirectory -Force -ErrorAction Stop | Out-Null
        $items=[System.Collections.Generic.List[object]]::new()
        $timedOut=$false
        foreach ($operation in $Configuration.Operations) {
            if ($timedOut) {
                $items.Add([pscustomobject]@{Name=$operation.Name;Action=$operation.Action;Status='Failed';Outcome='NotAttempted';StateBefore=$null;StateAfter=$null;ExitCode=$null;RestartNeeded=$false;TimedOut=$false;DismLogPath=$null;Error='Not attempted after a servicing timeout.'})
                continue
            }
            $log=Join-Path $logDirectory ('WindowsCapabilities-{0}-{1}.log' -f $Configuration.DeploymentId,$items.Count)
            $item=Invoke-OSDStageWindowsCapabilityOperation -Operation $operation -LogPath $log
            $items.Add($item)
            $timedOut=$item.TimedOut
        }
        $record.Items=@($items.ToArray())
        $record.SuccessfulOperationCount=@($items | Where-Object Status -eq 'Succeeded').Count
        $record.FailedOperationCount=@($items | Where-Object Status -eq 'Failed').Count
        $record.RestartNeeded=@($items | Where-Object RestartNeeded).Count -gt 0
        if ($record.FailedOperationCount -gt 0) { throw ('{0} Windows capability operation(s) failed.' -f $record.FailedOperationCount) }
        $record.Status='Succeeded'
    }
    catch { $record.Error=$_.Exception.Message }
    [pscustomobject]$record
}

if ($MyInvocation.InvocationName -eq '.') { return }
$ErrorActionPreference='Stop'
try {
    if (-not $ConfigPath) { $ConfigPath=Join-Path $PSScriptRoot 'config.json' }
    $configuration=Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
    $record=Invoke-OSDStageWindowsCapabilityConfiguration -Configuration $configuration
    $resultPath=[Environment]::ExpandEnvironmentVariables([string]$configuration.ResultPath)
    New-Item -ItemType Directory -Path (Split-Path $resultPath -Parent) -Force -ErrorAction Stop | Out-Null
    $record | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $resultPath -Encoding UTF8 -ErrorAction Stop
    if ($record.Status -eq 'Failed') { Write-Error $record.Error -ErrorAction Continue;exit 1 }
    if ($record.RestartNeeded) { exit 3010 }
    exit 0
}
catch { Write-Error $_.Exception.Message -ErrorAction Continue;exit 1 }