Public/Dell.Client.Update/Public/Install-DellUpdate.ps1

function Install-DellUpdate {
    <#
    .SYNOPSIS
        Installs applicable Dell updates.
 
    .DESCRIPTION
        Accepts update objects from Get-DellUpdate through the pipeline or
        searches by package ID. The command lists selected updates and asks
        for confirmation unless -Force is specified. Payloads are downloaded
        over HTTPS, validated with Dell's SHA-256 digest, silently installed,
        logged, and retained in the reusable download cache.
 
    .PARAMETER PackageIds
        Install only these Dell package or release IDs. Multiple IDs may be
        supplied as an array or as comma-separated values. Cannot be combined
        with -Packages.
 
    .PARAMETER Packages
        Update objects returned by Get-DellUpdate. Accepts pipeline input.
        Cannot be combined with -PackageIds.
 
    .PARAMETER ExcludePackageIds
        Exclude these package or release IDs from installation.
 
    .PARAMETER NoLog
        Do not create an installation log.
 
    .PARAMETER Force
        Start installation without the confirmation prompt.
 
    .PARAMETER SaveBIOSUpdateInfoToRegistry
        Save the latest BIOS installation result under
        HKLM:\SOFTWARE\Dell\ClientUpdate\BIOSUpdate.
 
    .PARAMETER ExportToWMI
        Append installation results to the Dell_UpdateHistory class in the
        root\DellClientUpdate WMI namespace for inventory and compliance use.
 
    .PARAMETER Proxy
        Proxy server URI used for package downloads.
 
    .PARAMETER ProxyCredential
        Credential used to authenticate to the proxy server.
 
    .PARAMETER ProxyUseDefaultCredentials
        Use the current user's credentials for proxy authentication.
 
    .PARAMETER Category
        Install only updates matching Dell catalog categories. Multiple Dell
        categories may be specified. Omit this parameter to include all.
 
    .PARAMETER Type
        Install only updates matching Dell component types. Valid values are
        Application, BIOS, Driver, and Firmware. Omit this parameter to include all.
 
    .PARAMETER Severities
        Install only updates matching Dell's Urgent or Recommended criticality.
        Omit this parameter to include all.
 
    .PARAMETER Path
        Optional payload download directory. When omitted, the reusable cache
        at C:\ProgramData\OEMWrapPS\Downloads is used.
 
    .EXAMPLE
        Get-DellUpdate | Install-DellUpdate
 
    .EXAMPLE
        Install-DellUpdate -PackageIds G896W, 4YHV8 -Force
 
    .EXAMPLE
        Get-DellUpdate | Install-DellUpdate -ExcludePackageIds 4YHV8 -WhatIf
 
    .NOTES
        Installation requires elevation. Package SHA-256 and Dell
        Authenticode checks are mandatory. Exit code 0 means success; exit
        code 2 means success with a required reboot. The command does not
        reboot the computer or modify BitLocker protection. Every attempted
        package is recorded in JSON history unless the operation is WhatIf.
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Search', ConfirmImpact = 'Medium')]
    param(
        [Parameter(Position = 0, ParameterSetName = 'Search')]
        [Alias('PackageId')]
        [string[]]$PackageIds,

        [Parameter(Position = 0, ValueFromPipeline, ParameterSetName = 'Packages')]
        [Alias('Package', 'Update')]
        [psobject[]]$Packages,

        [string[]]$ExcludePackageIds,
        [switch]$NoLog,
        [switch]$Force,
        [switch]$SaveBIOSUpdateInfoToRegistry,
        [switch]$ExportToWMI,

        [uri]$Proxy,
        [pscredential]$ProxyCredential,
        [switch]$ProxyUseDefaultCredentials,

        [ValidateSet('Application', 'Audio', 'BIOS', 'Chipset', 'Communications', 'Docks/Stands', 'Input', 'Network', 'Security', 'Serial ATA', 'Storage', 'Systems Management', 'Video')]
        [string[]]$Category,

        [ValidateSet('Application', 'BIOS', 'Driver', 'Firmware')]
        [string[]]$Type,

        [ValidateSet('Urgent', 'Recommended')]
        [string[]]$Severities,

        [string]$Path
    )

    begin {
        $pipelinePackages = [System.Collections.Generic.List[object]]::new()
        $historyRecords = [System.Collections.Generic.List[object]]::new()
    }

    process {
        foreach ($package in @($Packages)) {
            if ($null -ne $package) { $pipelinePackages.Add($package) }
        }
    }

    end {
        function Expand-DellPackageIdList {
            param([string[]]$Values)
            @($Values | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim() } | Where-Object { $_ })
        }

        function Write-DellInstallationLog {
            param([string]$Message)
            if ($NoLog -or -not $logPath) { return }
            Add-Content -LiteralPath $logPath -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message" -Encoding UTF8
        }

        function Publish-DellInstallationResult {
            param(
                [Parameter(Mandatory)][psobject]$Package,
                [Parameter(Mandatory)][psobject]$Result
            )

            if ($SaveBIOSUpdateInfoToRegistry -and $Package.Type -eq 'BIOS') {
                try {
                    Save-DellBiosUpdateInfoToRegistry -Package $Package -Result $Result
                }
                catch {
                    Write-Warning "Could not save BIOS update information to the registry: $($_.Exception.Message)"
                    Write-DellInstallationLog "BIOS registry reporting failed for $($Package.ReleaseID): $($_.Exception.Message)"
                }
            }
            if ($ExportToWMI) {
                try {
                    Export-DellUpdateResultToWmi -Package $Package -Result $Result
                }
                catch {
                    Write-Warning "Could not export update result to WMI: $($_.Exception.Message)"
                    Write-DellInstallationLog "WMI reporting failed for $($Package.ReleaseID): $($_.Exception.Message)"
                }
            }
        }

        $requestedIds = @(Expand-DellPackageIdList -Values $PackageIds)
        $excludedIds = @(Expand-DellPackageIdList -Values $ExcludePackageIds)
        $selectedPackages = if ($PSCmdlet.ParameterSetName -eq 'Packages') {
            @($pipelinePackages)
        }
        else {
            $callerWhatIfPreference = $WhatIfPreference
            try {
                $WhatIfPreference = $false
                @(Get-DellUpdate)
            }
            finally {
                $WhatIfPreference = $callerWhatIfPreference
            }
        }

        if ($requestedIds.Count) {
            $selectedPackages = @($selectedPackages | Where-Object { $_.PackageID -in $requestedIds -or $_.ReleaseID -in $requestedIds -or $_.ID -in $requestedIds })
        }
        if ($excludedIds.Count) {
            $selectedPackages = @($selectedPackages | Where-Object { $_.PackageID -notin $excludedIds -and $_.ReleaseID -notin $excludedIds -and $_.ID -notin $excludedIds })
        }

        if ($PSBoundParameters.ContainsKey('Category')) {
            $selectedPackages = @($selectedPackages | Where-Object { $_.Category -in $Category })
        }

        if ($PSBoundParameters.ContainsKey('Type')) {
            $selectedPackages = @($selectedPackages | Where-Object { $_.Type -in $Type })
        }

        if ($PSBoundParameters.ContainsKey('Severities')) {
            $selectedPackages = @($selectedPackages | Where-Object { $_.Severity -in $Severities })
        }

        $selectedPackages = @($selectedPackages | Sort-Object ReleaseID -Unique)
        if (-not $selectedPackages.Count) {
            Write-Verbose 'No applicable Dell updates matched the specified criteria.'
            return
        }

        foreach ($package in $selectedPackages) {
            if ($package.PSObject.TypeNames -notcontains 'Dell.Client.Update.DellUpdate') {
                throw 'Install-DellUpdate requires update objects returned by Get-DellUpdate.'
            }
            if ($package.IsApplicable -ne $true -or $package.IsInstalled -ne $false) {
                throw "Refusing to install '$($package.Title)': it is not positively marked applicable and not installed."
            }
            if (-not $package.DownloadUri -or -not $package.Sha256) {
                throw "Dell model catalog payload metadata is incomplete for '$($package.Title)'."
            }
        }

        if (-not $Force -and -not $WhatIfPreference) {
            Write-Host 'The following Dell updates will be installed:'
            $selectedPackages | Select-Object ReleaseID, Name, Version, Severity | Format-Table -AutoSize | Out-Host
            if (-not $PSCmdlet.ShouldContinue("Install $($selectedPackages.Count) Dell update package(s)?", 'Install Dell updates')) {
                return
            }
        }

        $isAdministrator = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
        if (-not $WhatIfPreference -and -not $isAdministrator) {
            throw 'Install-DellUpdate must be run from an elevated PowerShell session.'
        }

        $payloadDirectory = if ([string]::IsNullOrWhiteSpace($Path)) {
            Get-DellPSUpdatePath -Name Downloads
        }
        else {
            $Path
        }
        $logPath = $null
        if (-not $NoLog -and -not $WhatIfPreference) {
            $logDirectory = Get-DellPSUpdatePath -Name Logs
            $null = New-Item -Path $logDirectory -ItemType Directory -Force
            $logPath = Join-Path $logDirectory "$(Get-Date -Format 'yyyyMMdd_HHmmss_fff')-$([guid]::NewGuid().ToString('N')).log"
            Write-DellInstallationLog "Selected $($selectedPackages.Count) Dell update package(s)."
        }

        try {
            Write-Host "Found $($selectedPackages.Count) updates, starting process.."
            $downloadedPackages = [System.Collections.Generic.List[object]]::new()
            $downloadNumber = 0
            foreach ($package in $selectedPackages) {
                $downloadNumber++
                if (-not $PSCmdlet.ShouldProcess($package.Title, 'Download, verify, and install Dell update')) { continue }

                $startedAt = Get-Date
                try {
                    Write-Host "Downloading update $downloadNumber of $($selectedPackages.Count): $($package.Title)"
                    $sourceUri = [uri]$package.DownloadUri
                    if ($sourceUri.Scheme -ne 'https') { throw "Refusing non-HTTPS catalog payload URI: '$sourceUri'." }
                    $null = New-Item -Path $payloadDirectory -ItemType Directory -Force
                    $installerPath = Join-Path $payloadDirectory ([IO.Path]::GetFileName($sourceUri.AbsolutePath))

                    $actualDigest = if (Test-Path -LiteralPath $installerPath -PathType Leaf) {
                        (Get-FileHash -LiteralPath $installerPath -Algorithm SHA256).Hash
                    }
                    if ($actualDigest -ine [string]$package.Sha256) {
                        Write-DellInstallationLog "Downloading $($package.ReleaseID) from $sourceUri"
                        Invoke-DellDownload -Source $sourceUri -Destination $installerPath -Proxy $Proxy -ProxyCredential $ProxyCredential -ProxyUseDefaultCredentials:$ProxyUseDefaultCredentials
                    }
                    else {
                        Write-Host "Using previously downloaded payload for $($package.ReleaseID)."
                    }
                    $null = Test-DellUpdatePackage -Path $installerPath -ExpectedSha256 $package.Sha256
                    $downloadedPackages.Add([pscustomobject]@{
                        Package = $package
                        InstallerPath = $installerPath
                        StartedAt = $startedAt
                    })
                }
                catch {
                    Write-DellInstallationLog "Failed to download or validate $($package.ReleaseID): $($_.Exception.Message)"
                    $result = [pscustomobject]@{
                        ID = $package.ID
                        PackageID = $package.PackageID
                        ReleaseID = $package.ReleaseID
                        Title = $package.Title
                        Success = $false
                        RebootRequired = $false
                        PendingAction = 'NONE'
                        ExitCode = $null
                        FailureReason = $_.Exception.Message
                        LogPath = $logPath
                        Runtime = (Get-Date) - $startedAt
                    }
                    $result.PSObject.TypeNames.Insert(0, 'Dell.Client.Update.DellInstallResult')
                    Publish-DellInstallationResult -Package $package -Result $result
                    $historyRecords.Add((New-DellUpdateHistoryRecord -Package $package -Result $result))
                    $result
                }
            }

            if ($downloadedPackages.Count) {
                Write-Host 'Download phase complete. Starting installation phase..'
            }
            $installNumber = 0
            foreach ($downloadedPackage in $downloadedPackages) {
                $installNumber++
                $package = $downloadedPackage.Package
                $installerPath = $downloadedPackage.InstallerPath
                $startedAt = $downloadedPackage.StartedAt
                try {
                    Write-Host "Installing update $installNumber of $($downloadedPackages.Count): $($package.Title)"

                    Write-DellInstallationLog "Starting $($package.ReleaseID): $installerPath /s"
                    $processResult = Start-Process -FilePath $installerPath -ArgumentList '/s' -WorkingDirectory $payloadDirectory -Wait -PassThru
                    $success = $processResult.ExitCode -in @(0, 2)
                    $rebootRequired = $processResult.ExitCode -eq 2
                    Write-DellInstallationLog "Completed $($package.ReleaseID) with exit code $($processResult.ExitCode)."

                    $result = [pscustomobject]@{
                        ID = $package.ID
                        PackageID = $package.PackageID
                        ReleaseID = $package.ReleaseID
                        Title = $package.Title
                        Success = [bool]$success
                        RebootRequired = [bool]$rebootRequired
                        PendingAction = if ($rebootRequired) { 'REBOOT_MANDATORY' } else { 'NONE' }
                        ExitCode = $processResult.ExitCode
                        FailureReason = if ($success) { '' } else { "Dell update package exited with code $($processResult.ExitCode)." }
                        LogPath = $logPath
                        Runtime = $processResult.ExitTime - $processResult.StartTime
                    }
                    $result.PSObject.TypeNames.Insert(0, 'Dell.Client.Update.DellInstallResult')
                    Publish-DellInstallationResult -Package $package -Result $result
                    $historyRecords.Add((New-DellUpdateHistoryRecord -Package $package -Result $result))
                    $result
                }
                catch {
                    Write-DellInstallationLog "Failed $($package.ReleaseID): $($_.Exception.Message)"
                    $result = [pscustomobject]@{
                        ID = $package.ID
                        PackageID = $package.PackageID
                        ReleaseID = $package.ReleaseID
                        Title = $package.Title
                        Success = $false
                        RebootRequired = $false
                        PendingAction = 'NONE'
                        ExitCode = $null
                        FailureReason = $_.Exception.Message
                        LogPath = $logPath
                        Runtime = (Get-Date) - $startedAt
                    }
                    $result.PSObject.TypeNames.Insert(0, 'Dell.Client.Update.DellInstallResult')
                    Publish-DellInstallationResult -Package $package -Result $result
                    $historyRecords.Add((New-DellUpdateHistoryRecord -Package $package -Result $result))
                    $result
                }
            }
        }
        finally {
            if (-not $WhatIfPreference -and $historyRecords.Count) {
                try {
                    $historyFile = Write-DellUpdateHistorySession -Records @($historyRecords)
                    Write-DellInstallationLog "Installation history saved to $historyFile"
                }
                catch {
                    Write-Warning "Could not save Dell update installation history: $($_.Exception.Message)"
                }
            }
        }
    }
}