Public/Dell.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Dell driver management functions .DESCRIPTION Comprehensive Dell driver and update management using Dell Command Update. Includes catalog-based version detection, offline catalog support, and comprehensive exit code handling inspired by Gary Blok's Dell-EMPS.ps1. .NOTES Reference: https://github.com/gwblok/garytown/blob/master/hardware/Dell/CommandUpdate/EMPS/Dell-EMPS.ps1 DCU Reference Guide: https://dl.dell.com/content/manual13608255-dell-command-update-version-5-x-reference-guide.pdf #> #region DCU Exit Codes # Comprehensive DCU exit codes per Dell documentation $script:DCUExitCodes = @{ 0 = @{ Description = "Command execution successful"; Resolution = "None required" } 1 = @{ Description = "Reboot required"; Resolution = "Reboot the system to complete updates" } 2 = @{ Description = "Unknown application error"; Resolution = "Check DCU logs for details" } 3 = @{ Description = "Incomplete command line"; Resolution = "Verify command syntax" } # NOTE: Dell documentation for DCU 5.x indicates "not launched with administrative privileges" is exit code 4. 4 = @{ Description = "CLI was not launched with administrative privileges"; Resolution = "Run PowerShell/CLI as Administrator. If already elevated, check Dell services and ProgramData\\Dell permissions." } # Exit code 5 meaning varies by DCU version/environment; we keep it actionable and add diagnostics elsewhere. 5 = @{ Description = "Privilege / qualification error"; Resolution = "If elevated, check Dell Client Management Service and ProgramData\\Dell folder permissions; review DCU logs." } 6 = @{ Description = "No update filters found"; Resolution = "Check update type/severity filters" } 7 = @{ Description = "Duplicate command line option"; Resolution = "Remove duplicate options" } 8 = @{ Description = "Cannot create the scheduled task"; Resolution = "Check Task Scheduler permissions" } 9 = @{ Description = "Cannot remove the scheduled task"; Resolution = "Check Task Scheduler permissions" } 10 = @{ Description = "Download failed, no update(s) to apply"; Resolution = "Check network connectivity" } 11 = @{ Description = "Suspend Bitlocker failed"; Resolution = "Manually suspend BitLocker" } 12 = @{ Description = "Another instance of DCU running"; Resolution = "Wait for other instance to complete" } 13 = @{ Description = "Invalid catalog file"; Resolution = "Re-download or regenerate catalog" } 14 = @{ Description = "Unable to schedule updates"; Resolution = "Check scheduled task configuration" } 15 = @{ Description = "Invalid export file format"; Resolution = "Check export file path/format" } 16 = @{ Description = "Invalid password"; Resolution = "Verify BIOS password" } 17 = @{ Description = "System is not supported"; Resolution = "Verify Dell system compatibility" } 18 = @{ Description = "No updates available"; Resolution = "System is up to date" } 19 = @{ Description = "Network error"; Resolution = "Check network connectivity to Dell servers" } 20 = @{ Description = "Catalog sync failed"; Resolution = "Check internet connectivity" } 21 = @{ Description = "Running in OS pre-boot"; Resolution = "Run after Windows boot completes" } 500 = @{ Description = "No updates available"; Resolution = "System is up to date" } 501 = @{ Description = "Soft dependency error"; Resolution = "Check for prerequisite updates" } 502 = @{ Description = "Hard dependency error"; Resolution = "Install prerequisite updates first" } 503 = @{ Description = "Already running"; Resolution = "Wait for other DCU instance" } 504 = @{ Description = "System reboot pending"; Resolution = "Reboot system first" } 505 = @{ Description = "Rollback"; Resolution = "Update failed and was rolled back" } 506 = @{ Description = "Update failed"; Resolution = "Check DCU logs for failure details" } 507 = @{ Description = "Download progress"; Resolution = "Update is still downloading" } 508 = @{ Description = "Install progress"; Resolution = "Update is still installing" } 3006 = @{ Description = "System in Windows OOBE state"; Resolution = "Complete Windows setup first, then try again" } } function Get-DCUExitInfo { <# .SYNOPSIS Gets information about Dell Command Update exit codes .DESCRIPTION Returns description and resolution for DCU exit codes. Can return all exit codes or a specific one. .PARAMETER ExitCode Specific exit code to get information for .EXAMPLE Get-DCUExitInfo -ExitCode 1 .EXAMPLE Get-DCUExitInfo # Returns all exit codes #> [CmdletBinding()] param( [Parameter()] [int]$ExitCode ) if ($PSBoundParameters.ContainsKey('ExitCode')) { if ($script:DCUExitCodes.ContainsKey($ExitCode)) { $info = $script:DCUExitCodes[$ExitCode] return [PSCustomObject]@{ ExitCode = $ExitCode Description = $info.Description Resolution = $info.Resolution } } else { return [PSCustomObject]@{ ExitCode = $ExitCode Description = "Unknown exit code" Resolution = "Check Dell Command Update logs" } } } # Return all exit codes $script:DCUExitCodes.GetEnumerator() | ForEach-Object { [PSCustomObject]@{ ExitCode = $_.Key Description = $_.Value.Description Resolution = $_.Value.Resolution } } | Sort-Object ExitCode } #endregion #region DCU Installation and Version Detection function Get-DellCommandUpdatePath { <# .SYNOPSIS Finds the Dell Command Update CLI executable .OUTPUTS Path to dcu-cli.exe or $null if not found #> [CmdletBinding()] param() $paths = @( "${env:ProgramFiles}\Dell\CommandUpdate\dcu-cli.exe", "${env:ProgramFiles(x86)}\Dell\CommandUpdate\dcu-cli.exe" ) return $paths | Where-Object { Test-Path $_ } | Select-Object -First 1 } function Get-DellClientManagementServiceInternal { [CmdletBinding()] param() # Known service names vary slightly by DCU generation; try the most common first. $candidates = @( 'DellClientManagementService', 'DellCommandUpdateService', 'DellCommandUpdate', 'Dell Update Service' ) foreach ($name in $candidates) { $svc = Get-Service -Name $name -ErrorAction SilentlyContinue if ($svc) { return $svc } } # Fallback: search by display name $svc = Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like '*Dell Client Management*' -or $_.DisplayName -like '*Dell Command*Update*' -or $_.DisplayName -like '*Dell Update Service*' } | Select-Object -First 1 return $svc } function Ensure-DellClientManagementServiceInternal { [CmdletBinding()] param() $svc = Get-DellClientManagementServiceInternal if (-not $svc) { return @{ Found = $false Name = $null DisplayName = $null Status = $null Started = $false Message = "Dell Client Management / DCU service not found" } } $started = $false $message = "Service present" try { if ($svc.Status -ne 'Running') { Start-Service -Name $svc.Name -ErrorAction Stop $svc = Get-Service -Name $svc.Name -ErrorAction SilentlyContinue $started = $svc -and $svc.Status -eq 'Running' $message = if ($started) { "Service started" } else { "Service start attempted but status is $($svc.Status)" } } } catch { $message = "Failed to start service: $($_.Exception.Message)" } return @{ Found = $true Name = $svc.Name DisplayName = $svc.DisplayName Status = $svc.Status.ToString() Started = $started Message = $message } } function Test-WriteAccessInternal { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path ) try { if (-not (Test-Path $Path)) { New-Item -Path $Path -ItemType Directory -Force | Out-Null } $probe = Join-Path $Path ("dm_write_probe_{0}.tmp" -f ([guid]::NewGuid().ToString('n'))) 'probe' | Set-Content -Path $probe -Encoding ASCII -Force Remove-Item -Path $probe -Force -ErrorAction SilentlyContinue return $true } catch { return $false } } function Get-DCUInstallDetails { <# .SYNOPSIS Gets Dell Command Update installation details from registry .DESCRIPTION Queries the registry to determine installed DCU version and type (Universal Windows Platform vs Classic). .EXAMPLE Get-DCUInstallDetails .OUTPUTS PSCustomObject with Version, AppType, Path properties #> [CmdletBinding()] param() $result = [PSCustomObject]@{ IsInstalled = $false Version = $null AppType = $null # 'Universal' or 'Classic' Path = $null InstallDate = $null } # Check Universal Windows Platform (UWP) version $uwpReg = "HKLM:\SOFTWARE\Dell\UpdateService\Clients\CommandUpdate" if (Test-Path $uwpReg) { try { $regData = Get-ItemProperty -Path $uwpReg -ErrorAction Stop $result.IsInstalled = $true $result.Version = if ($regData.Version) { $regData.Version } else { $regData.ProductVersion } $result.AppType = 'Universal' } catch { } } # Check Classic version $classicReg = "HKLM:\SOFTWARE\DELL\CommandUpdate" if (-not $result.IsInstalled -and (Test-Path $classicReg)) { try { $regData = Get-ItemProperty -Path $classicReg -ErrorAction Stop $result.IsInstalled = $true $result.Version = if ($regData.Version) { $regData.Version } else { $regData.ProductVersion } $result.AppType = 'Classic' } catch { } } # Also check Programs and Features via Uninstall registry $uninstallPaths = @( "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" ) foreach ($path in $uninstallPaths) { $dcu = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "*Dell Command*Update*" } | Select-Object -First 1 if ($dcu) { $result.IsInstalled = $true $result.Version = if ($result.Version) { $result.Version } else { $dcu.DisplayVersion } $result.InstallDate = $dcu.InstallDate break } } # Get executable path $result.Path = Get-DellCommandUpdatePath return $result } #region Dell SupportAssist Handling function Get-DellSupportAssistInstallDetails { <# .SYNOPSIS Detects Dell SupportAssist installations. .DESCRIPTION Searches Uninstall registry keys for SupportAssist-related products. Returns entries with uninstall strings when available. #> [CmdletBinding()] param() $results = @() $uninstallPaths = @( "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" ) foreach ($path in $uninstallPaths) { $items = Get-ItemProperty $path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -and ( $_.DisplayName -match 'SupportAssist' -or $_.DisplayName -match 'Dell SupportAssist' ) } foreach ($i in $items) { $results += [PSCustomObject]@{ DisplayName = $i.DisplayName DisplayVersion = $i.DisplayVersion Publisher = $i.Publisher UninstallString = $i.UninstallString QuietUninstallString = $i.QuietUninstallString ProductCodeOrKey = $i.PSChildName } } } # De-dupe by name/version return $results | Sort-Object DisplayName, DisplayVersion -Unique } function Uninstall-DellSupportAssist { <# .SYNOPSIS Uninstalls Dell SupportAssist if installed. .DESCRIPTION Best-effort silent uninstall using: - QuietUninstallString if available - MSI uninstall for GUID-based products - WinGet uninstall fallback when possible .PARAMETER Force Attempt removal even if the Publisher does not look like Dell. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter()] [switch]$Force ) Assert-Elevation -Operation "Uninstalling Dell SupportAssist" $installs = Get-DellSupportAssistInstallDetails if (-not $installs -or $installs.Count -eq 0) { Write-DriverLog -Message "Dell SupportAssist not detected" -Severity Info return $true } Write-DriverLog -Message "Dell SupportAssist detected ($($installs.Count) entries) - attempting uninstall" -Severity Warning ` -Context @{ Products = ($installs | Select-Object DisplayName, DisplayVersion, Publisher) } $allOk = $true foreach ($app in $installs) { # Avoid removing unrelated “SupportAssist” unless forced or looks Dell-authored if (-not $Force) { if ($app.Publisher -and ($app.Publisher -notmatch 'Dell')) { Write-DriverLog -Message "Skipping uninstall for $($app.DisplayName) (publisher: $($app.Publisher)) - use -Force to override" -Severity Warning continue } } if (-not $PSCmdlet.ShouldProcess($app.DisplayName, "Uninstall")) { continue } try { # 1) QuietUninstallString if ($app.QuietUninstallString) { Write-DriverLog -Message "Uninstalling via QuietUninstallString: $($app.DisplayName)" -Severity Info $p = Start-Process -FilePath "cmd.exe" -ArgumentList @('/c', $app.QuietUninstallString) -Wait -PassThru -NoNewWindow if ($p.ExitCode -ne 0) { throw "Quiet uninstall exit code $($p.ExitCode)" } continue } # 2) MSI GUID based uninstall $guid = $null if ($app.UninstallString -match '\{[0-9A-Fa-f-]{36}\}') { $guid = $Matches[0] } elseif ($app.ProductCodeOrKey -match '^\{[0-9A-Fa-f-]{36}\}$') { $guid = $app.ProductCodeOrKey } if ($guid) { Write-DriverLog -Message "Uninstalling via msiexec: $($app.DisplayName) ($guid)" -Severity Info $args = @('/x', $guid, '/qn', '/norestart') $p = Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -PassThru -NoNewWindow if ($p.ExitCode -ne 0) { throw "msiexec exit code $($p.ExitCode)" } continue } # 3) UninstallString fallback (best effort) if ($app.UninstallString) { Write-DriverLog -Message "Uninstalling via UninstallString (best effort): $($app.DisplayName)" -Severity Info $p = Start-Process -FilePath "cmd.exe" -ArgumentList @('/c', $app.UninstallString) -Wait -PassThru -NoNewWindow if ($p.ExitCode -ne 0) { throw "UninstallString exit code $($p.ExitCode)" } continue } throw "No uninstall command available" } catch { Write-DriverLog -Message "Registry uninstall failed for $($app.DisplayName): $($_.Exception.Message). Trying WinGet uninstall..." -Severity Warning try { if (-not (Ensure-WinGetInternal -AutoInstall)) { throw "WinGet not available" } $winget = Get-Command winget.exe -ErrorAction Stop # Try common IDs first; if not, attempt by name match $candidateIds = @( 'Dell.SupportAssist', 'Dell.SupportAssistforBusinessPCs' ) $uninstalled = $false foreach ($id in $candidateIds) { $p = Start-Process -FilePath $winget.Source -ArgumentList @('uninstall','-e','--id',$id,'--silent') -Wait -PassThru -NoNewWindow if ($p.ExitCode -eq 0) { $uninstalled = $true; break } } if (-not $uninstalled) { # Name-based fallback (no exact match, but usually works) $p = Start-Process -FilePath $winget.Source -ArgumentList @('uninstall','--name','SupportAssist','--silent') -Wait -PassThru -NoNewWindow if ($p.ExitCode -eq 0) { $uninstalled = $true } } if (-not $uninstalled) { throw "WinGet uninstall did not succeed" } Write-DriverLog -Message "Dell SupportAssist removed via WinGet fallback: $($app.DisplayName)" -Severity Info } catch { $allOk = $false Write-DriverLog -Message "Failed to uninstall $($app.DisplayName): $($_.Exception.Message)" -Severity Error } } } return $allOk } #endregion function Get-DellCatalog { <# .SYNOPSIS Downloads and parses the Dell CatalogIndexPC.cab .DESCRIPTION Retrieves the Dell update catalog which contains information about supported models, available updates, and download URLs. Based on Gary Blok's Get-DellSupportedModels function. .PARAMETER Force Force re-download even if cached .EXAMPLE Get-DellCatalog .OUTPUTS Array of supported Dell models with SystemID, Model, URL, Date #> [CmdletBinding()] param( [Parameter()] [switch]$Force ) $cabPath = "$env:ProgramData\PSDriverManagement\DellCatalog\CatalogIndexPC.cab" $extractPath = "$env:ProgramData\PSDriverManagement\DellCatalog\Extract" $xmlPath = "$extractPath\CatalogIndexPC.xml" # Check cache (valid for 24 hours) $cacheValid = $false if (-not $Force -and (Test-Path $xmlPath)) { $cacheAge = (Get-Date) - (Get-Item $xmlPath).LastWriteTime if ($cacheAge.TotalHours -lt 24) { $cacheValid = $true Write-DriverLog -Message "Using cached Dell catalog (age: $([math]::Round($cacheAge.TotalHours, 1)) hours)" -Severity Info } } if (-not $cacheValid) { # Ensure directories exist $cabDir = Split-Path $cabPath -Parent if (-not (Test-Path $cabDir)) { New-Item -Path $cabDir -ItemType Directory -Force | Out-Null } if (-not (Test-Path $extractPath)) { New-Item -Path $extractPath -ItemType Directory -Force | Out-Null } Write-DriverLog -Message "Downloading Dell catalog from downloads.dell.com" -Severity Info try { Invoke-WebRequest -Uri "https://downloads.dell.com/catalog/CatalogIndexPC.cab" -OutFile $cabPath -UseBasicParsing -ErrorAction Stop # Extract CAB if (Test-Path "$extractPath\CatalogIndexPC.xml") { Remove-Item "$extractPath\CatalogIndexPC.xml" -Force } $expandResult = & expand.exe $cabPath -F:CatalogIndexPC.xml $extractPath 2>&1 if (-not (Test-Path $xmlPath)) { throw "Failed to extract catalog XML" } Write-DriverLog -Message "Dell catalog downloaded and extracted" -Severity Info } catch { Write-DriverLog -Message "Failed to download Dell catalog: $($_.Exception.Message)" -Severity Error throw } } # Parse XML Write-DriverLog -Message "Parsing Dell catalog XML" -Severity Info [xml]$catalogXml = Get-Content $xmlPath $models = $catalogXml.ManifestIndex.GroupManifest | ForEach-Object { [PSCustomObject]@{ SystemID = $_.SupportedSystems.Brand.Model.systemID Model = $_.SupportedSystems.Brand.Model.Display.'#cdata-section' URL = $_.ManifestInformation.path Date = $_.ManifestInformation.version } } return $models } function Get-LatestDCUVersion { <# .SYNOPSIS Gets the latest available Dell Command Update version .DESCRIPTION Queries Dell's catalog to find the latest DCU version available for download. Optionally checks against the currently installed version. .PARAMETER CheckUpdate Compare with installed version and return if update is available .EXAMPLE Get-LatestDCUVersion .EXAMPLE Get-LatestDCUVersion -CheckUpdate #> [CmdletBinding()] param( [Parameter()] [switch]$CheckUpdate ) # Dell Command Update package info URL $dcuInfoUrl = "https://downloads.dell.com/catalog/CatalogIndexPC.cab" # Try to get version from Dell's driver catalog try { $cabPath = "$env:ProgramData\PSDriverManagement\DellCatalog\DCUVersion.cab" $extractPath = "$env:ProgramData\PSDriverManagement\DellCatalog\DCUExtract" $cabDir = Split-Path $cabPath -Parent if (-not (Test-Path $cabDir)) { New-Item -Path $cabDir -ItemType Directory -Force | Out-Null } # Download CatalogPC.cab which contains DCU info $catalogUrl = "https://downloads.dell.com/catalog/CatalogPC.cab" Invoke-WebRequest -Uri $catalogUrl -OutFile $cabPath -UseBasicParsing -ErrorAction Stop if (-not (Test-Path $extractPath)) { New-Item -Path $extractPath -ItemType Directory -Force | Out-Null } # Extract & expand.exe $cabPath -F:* $extractPath 2>&1 | Out-Null # Find DCU in catalog $catalogXmlPath = Get-ChildItem $extractPath -Filter "*.xml" | Select-Object -First 1 if ($catalogXmlPath) { [xml]$catalog = Get-Content $catalogXmlPath.FullName $dcuPackage = $catalog.Manifest.SoftwareComponent | Where-Object { $_.Name.Display.'#cdata-section' -like "*Dell Command*Update*" } | Sort-Object { [version]$_.vendorVersion } -Descending | Select-Object -First 1 if ($dcuPackage) { $latestInfo = [PSCustomObject]@{ Version = $dcuPackage.vendorVersion ReleaseDate = $dcuPackage.releaseDate DownloadUrl = "https://downloads.dell.com/$($dcuPackage.path)" FileName = Split-Path $dcuPackage.path -Leaf Size = $dcuPackage.size } if ($CheckUpdate) { $installed = Get-DCUInstallDetails $latestInfo | Add-Member -NotePropertyName 'InstalledVersion' -NotePropertyValue $installed.Version $latestInfo | Add-Member -NotePropertyName 'UpdateAvailable' -NotePropertyValue $false if ($installed.IsInstalled -and $installed.Version) { try { $latestInfo.UpdateAvailable = ([version]$latestInfo.Version) -gt ([version]$installed.Version) } catch { # Version comparison failed $latestInfo.UpdateAvailable = $latestInfo.Version -ne $installed.Version } } else { $latestInfo.UpdateAvailable = $true # Not installed } } return $latestInfo } } } catch { Write-DriverLog -Message "Failed to get latest DCU version from catalog: $($_.Exception.Message)" -Severity Warning } # Fallback: Return known latest version return [PSCustomObject]@{ Version = "5.4.0" ReleaseDate = "2024-01-01" DownloadUrl = "https://dl.dell.com/FOLDER11914155M/1/Dell-Command-Update-Windows-Universal-Application_601KT_WIN_5.4.0_A00.EXE" FileName = "Dell-Command-Update-Windows-Universal-Application_601KT_WIN_5.4.0_A00.EXE" Size = $null } } function Install-DellCommandUpdate { <# .SYNOPSIS Downloads and installs Dell Command Update .DESCRIPTION Automatically downloads the latest Dell Command Update from Dell's website and performs a silent installation. Checks if update is needed first. .PARAMETER Force Install even if current version is up to date .PARAMETER Version Specific version to install (default: latest) .EXAMPLE Install-DellCommandUpdate .EXAMPLE Install-DellCommandUpdate -Force .NOTES Dell Command Update Universal Windows Platform application https://www.dell.com/support/kbdoc/en-us/000177325/dell-command-update #> [CmdletBinding()] param( [Parameter()] [switch]$Force ) Assert-Elevation -Operation "Installing Dell Command Update" # If SupportAssist is installed, remove it proactively (commonly causes conflicts / stale Dell tooling) try { Uninstall-DellSupportAssist | Out-Null } catch { Write-DriverLog -Message "SupportAssist removal encountered an error but will not block DCU install: $($_.Exception.Message)" -Severity Warning } $config = $script:ModuleConfig # Check if update is needed if (-not $Force) { $installed = Get-DCUInstallDetails if ($installed.IsInstalled) { $latestInfo = Get-LatestDCUVersion -CheckUpdate if (-not $latestInfo.UpdateAvailable) { Write-DriverLog -Message "Dell Command Update is already up to date (v$($installed.Version))" -Severity Info return } Write-DriverLog -Message "Update available: $($installed.Version) -> $($latestInfo.Version)" -Severity Info } } # Determine download URL # Priority: 1) Environment variable, 2) Module config, 3) Catalog lookup, 4) Default $dcuUrl = if ($env:PSDM_DCU_URL) { Write-DriverLog -Message "Using DCU URL from environment variable" -Severity Info $env:PSDM_DCU_URL } elseif ($config.DellCommandUpdateUrl) { $config.DellCommandUpdateUrl } else { # Try to get from catalog $latestInfo = Get-LatestDCUVersion if ($latestInfo.DownloadUrl) { $latestInfo.DownloadUrl } else { "https://dl.dell.com/FOLDER11914155M/1/Dell-Command-Update-Windows-Universal-Application_601KT_WIN_5.4.0_A00.EXE" } } $installerPath = Join-Path $env:TEMP "DellCommandUpdate_$(Get-Date -Format 'yyyyMMddHHmmss').exe" Write-DriverLog -Message "Downloading Dell Command Update from $dcuUrl" -Severity Info try { # Download with retry logic Invoke-WithRetry -ScriptBlock { # Use BITS for reliable download, fallback to WebRequest try { Start-BitsTransfer -Source $dcuUrl -Destination $installerPath -ErrorAction Stop } catch { Invoke-WebRequest -Uri $dcuUrl -OutFile $installerPath -UseBasicParsing -ErrorAction Stop } } -MaxAttempts 3 -ExponentialBackoff if (-not (Test-Path $installerPath)) { throw "Download failed - installer not found" } $fileSize = (Get-Item $installerPath).Length / 1MB Write-DriverLog -Message "Downloaded DCU installer ($([math]::Round($fileSize, 1)) MB)" -Severity Info # Silent install Write-DriverLog -Message "Installing Dell Command Update silently..." -Severity Info $installProcess = Start-Process -FilePath $installerPath -ArgumentList "/s" -Wait -PassThru -NoNewWindow $exitCode = $installProcess.ExitCode # Interpret exit code $exitInfo = Get-DCUExitInfo -ExitCode $exitCode if ($exitCode -eq 0) { Write-DriverLog -Message "Dell Command Update installed successfully" -Severity Info } elseif ($exitCode -eq 1) { Write-DriverLog -Message "Dell Command Update installed - reboot required" -Severity Warning } else { throw "Installation failed: $($exitInfo.Description) (Exit: $exitCode)" } return [PSCustomObject]@{ Success = $exitCode -in @(0, 1) ExitCode = $exitCode Message = $exitInfo.Description RebootRequired = ($exitCode -eq 1) } } catch { # Fallback: install via WinGet if Dell download is blocked (common 403/Akamai) Write-DriverLog -Message "Failed to install Dell Command Update via direct download: $($_.Exception.Message). Trying WinGet (Dell.CommandUpdate)..." -Severity Warning try { if (-not (Ensure-WinGetInternal -AutoInstall)) { throw "WinGet is not available and could not be installed automatically" } $winget = Get-Command winget.exe -ErrorAction SilentlyContinue if (-not $winget) { throw "winget.exe not found" } # Prefer exact ID match $args = @('install','-e','--id','Dell.CommandUpdate','--silent','--accept-package-agreements','--accept-source-agreements') $p = Start-Process -FilePath $winget.Source -ArgumentList $args -Wait -PassThru -NoNewWindow if ($p.ExitCode -eq 0) { Write-DriverLog -Message "Dell Command Update installed successfully via WinGet" -Severity Info return [PSCustomObject]@{ Success = $true ExitCode = 0 Message = "Installed via WinGet" RebootRequired = $false } } throw "WinGet install failed with exit code $($p.ExitCode)" } catch { Write-DriverLog -Message "Failed to install Dell Command Update via WinGet fallback: $($_.Exception.Message)" -Severity Error throw } } finally { # Cleanup installer if (Test-Path $installerPath) { Remove-Item $installerPath -Force -ErrorAction SilentlyContinue } } } #endregion #region DCU Settings function Get-DCUSettings { <# .SYNOPSIS Gets current Dell Command Update settings .DESCRIPTION Reads DCU configuration from registry. .EXAMPLE Get-DCUSettings #> [CmdletBinding()] param() $settings = [PSCustomObject]@{ UserConsent = $null AutoSuspendBitLocker = $null ScheduleAction = $null ScheduleAuto = $null InstallationDeferral = $null SystemRestartDeferral = $null AdvancedDriverRestore = $null LockSettings = $null CatalogLocation = $null } $regPath = "HKLM:\SOFTWARE\Dell\UpdateService\Clients\CommandUpdate\Preferences\Settings" if (Test-Path $regPath) { try { $regData = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue $settings.UserConsent = $regData.UserConsent $settings.AutoSuspendBitLocker = $regData.AutoSuspendBitLocker $settings.ScheduleAction = $regData.ScheduleAction $settings.ScheduleAuto = $regData.ScheduleAuto $settings.InstallationDeferral = $regData.InstallationDeferral $settings.SystemRestartDeferral = $regData.SystemRestartDeferral $settings.AdvancedDriverRestore = $regData.AdvancedDriverRestore $settings.LockSettings = $regData.LockSettings $settings.CatalogLocation = $regData.CatalogLocation } catch { Write-DriverLog -Message "Failed to read DCU settings: $($_.Exception.Message)" -Severity Warning } } return $settings } function Set-DCUSettings { <# .SYNOPSIS Configures Dell Command Update settings .DESCRIPTION Uses dcu-cli.exe to configure DCU settings. .PARAMETER UserConsent Enable or disable user consent prompts .PARAMETER AutoSuspendBitLocker Enable automatic BitLocker suspension for BIOS updates .PARAMETER AdvancedDriverRestore Enable driver restore points .PARAMETER ScheduleAction Schedule action: DownloadOnly, DownloadAndNotify, DownloadInstallAndNotify .PARAMETER InstallationDeferral Number of days to defer installation .PARAMETER SystemRestartDeferral Number of days to defer restart .EXAMPLE Set-DCUSettings -AutoSuspendBitLocker enable -AdvancedDriverRestore enable #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter()] [ValidateSet('enable', 'disable')] [string]$UserConsent, [Parameter()] [ValidateSet('enable', 'disable')] [string]$AutoSuspendBitLocker, [Parameter()] [ValidateSet('enable', 'disable')] [string]$AdvancedDriverRestore, [Parameter()] [ValidateSet('DownloadOnly', 'DownloadAndNotify', 'DownloadInstallAndNotify')] [string]$ScheduleAction, [Parameter()] [ValidateRange(0, 365)] [int]$InstallationDeferral, [Parameter()] [ValidateRange(0, 365)] [int]$SystemRestartDeferral ) $dcuPath = Get-DellCommandUpdatePath if (-not $dcuPath) { throw "Dell Command Update is not installed" } $configArgs = @('/configure', '-silent') $changes = @() if ($UserConsent) { $configArgs += "-userConsent=$UserConsent" $changes += "UserConsent=$UserConsent" } if ($AutoSuspendBitLocker) { $configArgs += "-autoSuspendBitLocker=$AutoSuspendBitLocker" $changes += "AutoSuspendBitLocker=$AutoSuspendBitLocker" } if ($AdvancedDriverRestore) { $configArgs += "-advancedDriverRestore=$AdvancedDriverRestore" $changes += "AdvancedDriverRestore=$AdvancedDriverRestore" } if ($ScheduleAction) { $configArgs += "-scheduleAction=$ScheduleAction" $changes += "ScheduleAction=$ScheduleAction" } if ($PSBoundParameters.ContainsKey('InstallationDeferral')) { if ($ScheduleAction -eq 'DownloadInstallAndNotify' -or -not $ScheduleAction) { $configArgs += "-installationDeferral=$InstallationDeferral" $changes += "InstallationDeferral=$InstallationDeferral" } else { Write-DriverLog -Message "InstallationDeferral only applies to DownloadInstallAndNotify schedule action" -Severity Warning } } if ($PSBoundParameters.ContainsKey('SystemRestartDeferral')) { if ($ScheduleAction -eq 'DownloadInstallAndNotify' -or -not $ScheduleAction) { $configArgs += "-systemRestartDeferral=$SystemRestartDeferral" $changes += "SystemRestartDeferral=$SystemRestartDeferral" } else { Write-DriverLog -Message "SystemRestartDeferral only applies to DownloadInstallAndNotify schedule action" -Severity Warning } } if ($changes.Count -eq 0) { Write-DriverLog -Message "No settings specified to change" -Severity Warning return } if ($PSCmdlet.ShouldProcess("DCU Settings: $($changes -join ', ')", "Configure")) { Write-DriverLog -Message "Configuring DCU: $($changes -join ', ')" -Severity Info & $dcuPath @configArgs 2>&1 | Out-Null $exitCode = $LASTEXITCODE $exitInfo = Get-DCUExitInfo -ExitCode $exitCode if ($exitCode -eq 0) { Write-DriverLog -Message "DCU settings configured successfully" -Severity Info } else { Write-DriverLog -Message "DCU configuration returned: $($exitInfo.Description)" -Severity Warning } return Get-DCUSettings } } #endregion #region Offline Catalog Support function Get-DCUCatalogPath { <# .SYNOPSIS Gets the configured DCU catalog path .DESCRIPTION Returns the custom catalog path if configured, or the default Dell catalog location. .EXAMPLE Get-DCUCatalogPath #> [CmdletBinding()] param() # Check environment variable first if ($env:PSDM_DCU_CATALOG) { return $env:PSDM_DCU_CATALOG } # Check DCU settings $settings = Get-DCUSettings if ($settings.CatalogLocation) { return $settings.CatalogLocation } # Return default return $null # DCU uses Dell's online catalog by default } function Set-DCUCatalogPath { <# .SYNOPSIS Sets a custom catalog path for Dell Command Update .DESCRIPTION Configures DCU to use a local or network catalog file instead of Dell's online catalog. Useful for offline environments or controlled update deployments. .PARAMETER CatalogPath Path to the local catalog XML file or network share .PARAMETER Reset Reset to use Dell's online catalog .EXAMPLE Set-DCUCatalogPath -CatalogPath 'C:\DCUCatalog\Catalog.xml' .EXAMPLE Set-DCUCatalogPath -CatalogPath '\\server\share\DCUCatalog\Catalog.xml' .EXAMPLE Set-DCUCatalogPath -Reset #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, ParameterSetName = 'SetPath')] [string]$CatalogPath, [Parameter(Mandatory, ParameterSetName = 'Reset')] [switch]$Reset ) $dcuPath = Get-DellCommandUpdatePath if (-not $dcuPath) { throw "Dell Command Update is not installed" } if ($Reset) { if ($PSCmdlet.ShouldProcess("DCU catalog", "Reset to online")) { Write-DriverLog -Message "Resetting DCU to use online catalog" -Severity Info & $dcuPath /configure -catalogLocation= -silent 2>&1 | Out-Null Write-DriverLog -Message "DCU catalog reset to online" -Severity Info } } else { if (-not (Test-Path $CatalogPath)) { throw "Catalog path not found: $CatalogPath" } if ($PSCmdlet.ShouldProcess($CatalogPath, "Set as DCU catalog")) { Write-DriverLog -Message "Setting DCU catalog path: $CatalogPath" -Severity Info & $dcuPath /configure "-catalogLocation=$CatalogPath" -silent 2>&1 | Out-Null $exitCode = $LASTEXITCODE if ($exitCode -eq 0) { Write-DriverLog -Message "DCU catalog path configured" -Severity Info } else { $exitInfo = Get-DCUExitInfo -ExitCode $exitCode Write-DriverLog -Message "DCU catalog configuration: $($exitInfo.Description)" -Severity Warning } } } } function New-DCUOfflineCatalog { <# .SYNOPSIS Creates an offline Dell Command Update catalog .DESCRIPTION Downloads the Dell catalog and optionally driver packages for offline use. Rewrites the catalog base location for local paths. .PARAMETER OutputPath Directory to store the offline catalog and drivers .PARAMETER SystemID Specific system ID to create catalog for (default: current system) .PARAMETER IncludeDrivers Download driver packages along with catalog .PARAMETER DriverTypes Types of drivers to include: Driver, BIOS, Firmware, Application .EXAMPLE New-DCUOfflineCatalog -OutputPath 'C:\DCUOffline' -IncludeDrivers .EXAMPLE New-DCUOfflineCatalog -OutputPath '\\server\share\DCU' -SystemID '0A5C' #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$OutputPath, [Parameter()] [string]$SystemID, [Parameter()] [switch]$IncludeDrivers, [Parameter()] [ValidateSet('Driver', 'BIOS', 'Firmware', 'Application', 'All')] [string[]]$DriverTypes = @('Driver', 'BIOS', 'Firmware') ) # Get system ID if not provided if (-not $SystemID) { $systemInfo = Get-CimInstance -ClassName Win32_ComputerSystem $SystemID = $systemInfo.SystemSKUNumber if (-not $SystemID) { throw "Could not determine system ID. Please provide -SystemID parameter." } } Write-DriverLog -Message "Creating offline catalog for SystemID: $SystemID" -Severity Info # Create output directory if (-not (Test-Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null } $catalogDir = Join-Path $OutputPath "Catalog" $driversDir = Join-Path $OutputPath "Drivers" if (-not (Test-Path $catalogDir)) { New-Item -Path $catalogDir -ItemType Directory -Force | Out-Null } # Download main catalog $cabPath = Join-Path $catalogDir "CatalogPC.cab" $extractPath = Join-Path $catalogDir "Extract" Write-DriverLog -Message "Downloading Dell catalog" -Severity Info Invoke-WebRequest -Uri "https://downloads.dell.com/catalog/CatalogPC.cab" -OutFile $cabPath -UseBasicParsing if (-not (Test-Path $extractPath)) { New-Item -Path $extractPath -ItemType Directory -Force | Out-Null } & expand.exe $cabPath -F:* $extractPath 2>&1 | Out-Null # Parse catalog $catalogXmlPath = Get-ChildItem $extractPath -Filter "*.xml" | Select-Object -First 1 [xml]$catalog = Get-Content $catalogXmlPath.FullName # Filter for system $systemComponents = $catalog.Manifest.SoftwareComponent | Where-Object { $_.SupportedSystems.Brand.Model.systemID -eq $SystemID -or $_.SupportedSystems.Brand.Model.systemID -contains $SystemID } Write-DriverLog -Message "Found $($systemComponents.Count) components for system" -Severity Info # Download drivers if requested $downloadedFiles = @() if ($IncludeDrivers -and $systemComponents) { if (-not (Test-Path $driversDir)) { New-Item -Path $driversDir -ItemType Directory -Force | Out-Null } $filteredComponents = $systemComponents | Where-Object { $type = $_.ComponentType.Display.'#cdata-section' 'All' -in $DriverTypes -or ($type -like '*Driver*' -and 'Driver' -in $DriverTypes) -or ($type -like '*BIOS*' -and 'BIOS' -in $DriverTypes) -or ($type -like '*Firmware*' -and 'Firmware' -in $DriverTypes) -or ($type -like '*Application*' -and 'Application' -in $DriverTypes) } Write-DriverLog -Message "Downloading $($filteredComponents.Count) driver packages" -Severity Info $count = 0 foreach ($component in $filteredComponents) { $count++ $downloadUrl = "https://downloads.dell.com/$($component.path)" $fileName = Split-Path $component.path -Leaf $localPath = Join-Path $driversDir $fileName Write-Progress -Activity "Downloading drivers" -Status "$count of $($filteredComponents.Count): $fileName" -PercentComplete (($count / $filteredComponents.Count) * 100) try { if (-not (Test-Path $localPath)) { Invoke-WebRequest -Uri $downloadUrl -OutFile $localPath -UseBasicParsing -ErrorAction Stop } $downloadedFiles += $fileName } catch { Write-DriverLog -Message "Failed to download $fileName`: $($_.Exception.Message)" -Severity Warning } } Write-Progress -Activity "Downloading drivers" -Completed } # Create modified catalog with local paths $offlineCatalogPath = Join-Path $catalogDir "OfflineCatalog_$SystemID.xml" # Modify base location in catalog $catalogContent = Get-Content $catalogXmlPath.FullName -Raw $catalogContent = $catalogContent -replace 'downloads\.dell\.com', $OutputPath.Replace('\', '/') $catalogContent | Set-Content -Path $offlineCatalogPath -Encoding UTF8 $result = [PSCustomObject]@{ SystemID = $SystemID CatalogPath = $offlineCatalogPath DriversPath = $driversDir ComponentCount = $systemComponents.Count DownloadedFiles = $downloadedFiles.Count OutputPath = $OutputPath } Write-DriverLog -Message "Offline catalog created: $offlineCatalogPath" -Severity Info ` -Context @{ SystemID = $SystemID; Components = $systemComponents.Count } return $result } #endregion #region OOBE Detection and Direct Driver Installation function Test-DellOOBEBlocked { <# .SYNOPSIS Checks if Dell Command Update is blocked due to OOBE state .OUTPUTS $true if OOBE blocking is active, $false otherwise #> [CmdletBinding()] param() $regPath = "HKLM:\SOFTWARE\Dell\UpdateService\Service\UpdateScheduler" $regName = "IsFirstScanAfterOOBEPending" try { $value = (Get-ItemProperty -Path $regPath -Name $regName -ErrorAction SilentlyContinue).$regName return ($value -eq 1) } catch { return $false } } function Get-DellDriverPackUrl { <# .SYNOPSIS Gets the Dell driver pack download URL for the current system .DESCRIPTION Queries Dell's Driver Pack Catalog to find the driver pack CAB/EXE for the current system model. This is used for direct driver installation during OOBE. Reference: https://www.dell.com/support/kbdoc/en-us/000122176/driver-pack-catalog .OUTPUTS PSCustomObject with Url, FileName, SystemID, Model, and OS info #> [CmdletBinding()] param() # Get system info $systemInfo = Get-CimInstance -ClassName Win32_ComputerSystem $osInfo = Get-CimInstance -ClassName Win32_OperatingSystem # Get SystemSKU (Dell SystemID) and Model Name $systemId = $systemInfo.SystemSKUNumber $modelName = $systemInfo.Model # Get OS version info (Windows 10 = 10.0, Windows 11 = 10.0 with build >= 22000) $osMajor = [System.Environment]::OSVersion.Version.Major $osMinor = [System.Environment]::OSVersion.Version.Minor $osBuild = [System.Environment]::OSVersion.Version.Build $osArch = if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" } Write-DriverLog -Message "Looking up driver pack for Model: $modelName, SystemID: $systemId, OS: $osMajor.$osMinor (Build $osBuild) $osArch" -Severity Info # Download the Dell Driver Pack Catalog (different from CatalogIndexPC.cab!) $cabPath = "$env:TEMP\DriverPackCatalog_$(Get-Date -Format 'yyyyMMdd').cab" $xmlPath = "$env:TEMP\DriverPackCatalog_$(Get-Date -Format 'yyyyMMdd').xml" try { if (-not (Test-Path $cabPath)) { Write-DriverLog -Message "Downloading Dell Driver Pack Catalog" -Severity Info Invoke-WebRequest -Uri "https://downloads.dell.com/catalog/DriverPackCatalog.cab" -OutFile $cabPath -UseBasicParsing -ErrorAction Stop } # Extract catalog XML if (-not (Test-Path $xmlPath)) { & expand.exe $cabPath $xmlPath 2>&1 | Out-Null } if (-not (Test-Path $xmlPath)) { throw "Failed to extract DriverPackCatalog.xml" } [xml]$catalogXml = Get-Content $xmlPath $baseLocation = $catalogXml.DriverPackManifest.baseLocation # Find driver pack by SystemID or Model Name, matching OS version and architecture $matchingPacks = $catalogXml.DriverPackManifest.DriverPackage | Where-Object { # Match by SystemID or Model Name $sysMatch = ($_.SupportedSystems.Brand.Model.systemID -eq $systemId) -or ($_.SupportedSystems.Brand.Model.name -eq $modelName) # Match OS (not WinPE) $osMatch = ($_.type -ne "WinPE") -and ($_.SupportedOperatingSystems.OperatingSystem.majorVersion -eq $osMajor) -and ($_.SupportedOperatingSystems.OperatingSystem.osArch -eq $osArch) $sysMatch -and $osMatch } if (-not $matchingPacks) { # Try matching just by model name without strict OS match $matchingPacks = $catalogXml.DriverPackManifest.DriverPackage | Where-Object { (($_.SupportedSystems.Brand.Model.systemID -eq $systemId) -or ($_.SupportedSystems.Brand.Model.name -eq $modelName)) -and ($_.type -ne "WinPE") } } if (-not $matchingPacks) { Write-DriverLog -Message "No driver pack found for Model: $modelName (SystemID: $systemId)" -Severity Warning return $null } # Get the first/best match $bestPack = $matchingPacks | Select-Object -First 1 $packUrl = "https://$baseLocation/$($bestPack.path)" $fileName = Split-Path $bestPack.path -Leaf Write-DriverLog -Message "Found driver pack: $fileName (Release: $($bestPack.releaseID))" -Severity Info return [PSCustomObject]@{ Url = $packUrl FileName = $fileName SystemID = $systemId Model = $modelName ReleaseID = $bestPack.releaseID DellVersion = $bestPack.dellVersion Size = $bestPack.size HashMD5 = $bestPack.hashMD5 } } catch { Write-DriverLog -Message "Failed to get driver pack URL: $($_.Exception.Message)" -Severity Error return $null } } function Install-DellDriverPackDirect { <# .SYNOPSIS Downloads and installs Dell drivers directly from the driver pack .DESCRIPTION For use during OOBE when DCU is blocked. Downloads the Dell Driver Pack EXE, extracts it to get CAB with drivers, then installs using pnputil.exe. Reference: https://www.dell.com/support/kbdoc/en-us/000122176/driver-pack-catalog .PARAMETER NoReboot Suppress automatic reboot .OUTPUTS DriverUpdateResult object #> [CmdletBinding(SupportsShouldProcess)] [OutputType('DriverUpdateResult')] param( [Parameter()] [switch]$NoReboot ) Assert-Elevation -Operation "Installing Dell drivers directly" $result = [DriverUpdateResult]::new() $result.CorrelationId = $script:CorrelationId Write-DriverLog -Message "DCU blocked by OOBE - using direct driver pack installation" -Severity Info try { # Get driver pack URL from Dell's Driver Pack Catalog $packInfo = Get-DellDriverPackUrl if (-not $packInfo) { $result.Success = $false $result.Message = "Could not find driver pack for this system in Dell's Driver Pack Catalog" $result.ExitCode = 1 return $result } # Setup paths $downloadPath = "$env:ProgramData\PSDriverManagement\DellDriverPack" $packPath = Join-Path $downloadPath $packInfo.FileName $extractPath = Join-Path $downloadPath "Extracted_$($packInfo.ReleaseID)" if (-not (Test-Path $downloadPath)) { New-Item -Path $downloadPath -ItemType Directory -Force | Out-Null } # Download driver pack if not cached if (-not (Test-Path $packPath)) { Write-DriverLog -Message "Downloading driver pack: $($packInfo.FileName) ($([math]::Round([long]$packInfo.Size / 1MB, 0)) MB)" -Severity Info Write-DriverLog -Message "URL: $($packInfo.Url)" -Severity Info # Try BITS first for reliable large file download try { Start-BitsTransfer -Source $packInfo.Url -Destination $packPath -ErrorAction Stop } catch { Write-DriverLog -Message "BITS failed, using WebRequest" -Severity Warning Invoke-WebRequest -Uri $packInfo.Url -OutFile $packPath -UseBasicParsing -ErrorAction Stop } } if (-not (Test-Path $packPath)) { throw "Driver pack download failed" } $packSize = (Get-Item $packPath).Length / 1MB Write-DriverLog -Message "Driver pack ready: $('{0:N0}' -f $packSize) MB" -Severity Info # Verify hash if available if ($packInfo.HashMD5) { $actualHash = (Get-FileHash -Path $packPath -Algorithm MD5).Hash if ($actualHash -ne $packInfo.HashMD5) { Write-DriverLog -Message "Hash mismatch! Expected: $($packInfo.HashMD5), Got: $actualHash" -Severity Warning # Continue anyway - hash might be outdated in catalog } } # Extract the driver pack EXE (Dell driver packs are self-extracting) if (-not (Test-Path $extractPath)) { New-Item -Path $extractPath -ItemType Directory -Force | Out-Null Write-DriverLog -Message "Extracting driver pack to: $extractPath" -Severity Info # Dell driver pack EXE supports /s (silent) and /e=<path> (extract to path) $extractProcess = Start-Process -FilePath $packPath -ArgumentList "/s /e=`"$extractPath`"" -Wait -PassThru -NoNewWindow if ($extractProcess.ExitCode -ne 0) { Write-DriverLog -Message "Extraction via EXE failed (exit: $($extractProcess.ExitCode)), trying expand.exe" -Severity Warning # Fallback: try expand.exe in case it's a CAB & expand.exe $packPath -F:* $extractPath 2>&1 | Out-Null } } # Find all INF files in the extracted folder $infFiles = Get-ChildItem -Path $extractPath -Filter "*.inf" -Recurse -ErrorAction SilentlyContinue if (-not $infFiles -or $infFiles.Count -eq 0) { throw "No driver INF files found in extracted pack" } Write-DriverLog -Message "Found $($infFiles.Count) driver INF files" -Severity Info $driversInstalled = 0 $driversFailed = 0 $driversSkipped = 0 # Install each driver using pnputil foreach ($inf in $infFiles) { if ($PSCmdlet.ShouldProcess($inf.Name, "Install driver")) { try { # Use pnputil to add and install the driver $pnpOutput = & pnputil.exe /add-driver $inf.FullName /install 2>&1 $pnpExit = $LASTEXITCODE if ($pnpExit -eq 0) { $driversInstalled++ } elseif ($pnpExit -eq 259) { # 259 = No more data available (driver already installed or not applicable) $driversSkipped++ } else { $driversFailed++ Write-DriverLog -Message "Failed to install $($inf.Name): exit code $pnpExit" -Severity Warning } } catch { $driversFailed++ Write-DriverLog -Message "Exception installing $($inf.Name): $($_.Exception.Message)" -Severity Warning } } } $result.Success = ($driversInstalled -gt 0) -or ($driversSkipped -eq $infFiles.Count) $result.UpdatesApplied = $driversInstalled $result.UpdatesFailed = $driversFailed $result.Message = "Direct driver pack install: $driversInstalled installed, $driversSkipped skipped (not applicable), $driversFailed failed" $result.ExitCode = if ($result.Success) { 0 } else { 1 } $result.RebootRequired = $driversInstalled -gt 0 $result.Details['DriverPack'] = $packInfo.FileName $result.Details['DirectInstall'] = $true Write-DriverLog -Message $result.Message -Severity Info } catch { $result.Success = $false $result.Message = "Direct driver installation failed: $($_.Exception.Message)" $result.ExitCode = 1 Write-DriverLog -Message $result.Message -Severity Error } return $result } function Clear-DellOOBEFlag { <# .SYNOPSIS Clears the Dell OOBE flag to allow DCU to run during Windows setup .DESCRIPTION Dell Command Update blocks operations when it detects the system is in OOBE (Out of Box Experience) state. This function clears the registry flag that indicates OOBE is pending, allowing DCU to run during provisioning scenarios. .PARAMETER Force Suppress confirmation prompts .EXAMPLE Clear-DellOOBEFlag .NOTES Requires elevation. This is a workaround for running DCU during Windows provisioning. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter()] [switch]$Force ) Assert-Elevation -Operation "Clearing Dell OOBE flag" $regPath = "HKLM:\SOFTWARE\Dell\UpdateService\Service\UpdateScheduler" $regName = "IsFirstScanAfterOOBEPending" if (-not (Test-Path $regPath)) { Write-DriverLog -Message "Dell UpdateService registry path not found" -Severity Warning return $false } try { $currentValue = (Get-ItemProperty -Path $regPath -Name $regName -ErrorAction SilentlyContinue).$regName if ($currentValue -eq 0) { Write-DriverLog -Message "OOBE flag already cleared" -Severity Info return $true } if ($Force -or $PSCmdlet.ShouldProcess("Dell OOBE Flag", "Clear to allow DCU during provisioning")) { Set-ItemProperty -Path $regPath -Name $regName -Value 0 -Type DWord -Force # Also restart the Dell Client Management Service to pick up the change $service = Get-Service -Name "DellClientManagementService" -ErrorAction SilentlyContinue if ($service -and $service.Status -eq 'Running') { Write-DriverLog -Message "Restarting Dell Client Management Service" -Severity Info Restart-Service -Name "DellClientManagementService" -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 } Write-DriverLog -Message "Dell OOBE flag cleared - DCU should now work during provisioning" -Severity Info return $true } return $false } catch { Write-DriverLog -Message "Failed to clear OOBE flag: $($_.Exception.Message)" -Severity Error return $false } } #endregion #region Core Functions function Initialize-DellModule { <# .SYNOPSIS Ensures Dell Command Update is available .DESCRIPTION Checks if Dell Command Update is installed. If not, automatically downloads and installs it from Dell's website. .OUTPUTS Path to dcu-cli.exe .EXAMPLE $dcuPath = Initialize-DellModule #> [CmdletBinding()] param() $dcuPath = Get-DellCommandUpdatePath if (-not $dcuPath) { Write-DriverLog -Message "Dell Command Update not found, installing..." -Severity Info try { Install-DellCommandUpdate # Wait a moment for installation to complete Start-Sleep -Seconds 2 # Re-check for DCU $dcuPath = Get-DellCommandUpdatePath if (-not $dcuPath) { throw "Dell Command Update installation completed but dcu-cli.exe not found" } Write-DriverLog -Message "Dell Command Update ready at: $dcuPath" -Severity Info } catch { Write-DriverLog -Message "Failed to initialize Dell Command Update: $($_.Exception.Message)" -Severity Error throw "Dell Command Update could not be installed: $($_.Exception.Message)" } } return $dcuPath } function Get-DellDriverUpdates { <# .SYNOPSIS Scans for available Dell driver updates .DESCRIPTION Uses Dell Command Update to scan for applicable updates .PARAMETER UpdateTypes Types of updates to scan for: Driver, BIOS, Firmware, All .PARAMETER Severity Severity levels: Critical, Recommended, Optional .EXAMPLE Get-DellDriverUpdates -UpdateTypes Driver -Severity Critical, Recommended .OUTPUTS Array of available update objects #> [CmdletBinding()] param( [Parameter()] [ValidateSet('Driver', 'BIOS', 'Firmware', 'All')] [string[]]$UpdateTypes = @('Driver'), [Parameter()] [ValidateSet('Critical', 'Recommended', 'Optional')] [string[]]$Severity = @('Critical', 'Recommended') ) try { $dcuCli = Initialize-DellModule } catch { Write-DriverLog -Message "Dell Command Update not available: $($_.Exception.Message)" -Severity Warning return @() } $reportPath = "$env:ProgramData\Dell\UpdateScan" if (-not (Test-Path $reportPath)) { New-Item -Path $reportPath -ItemType Directory -Force | Out-Null } Write-DriverLog -Message "Scanning for Dell updates" -Severity Info # Run scan $scanArgs = @('/scan', '-silent', "-report=$reportPath") $scanResult = & $dcuCli @scanArgs 2>&1 $scanExitCode = $LASTEXITCODE # Log exit code info $exitInfo = Get-DCUExitInfo -ExitCode $scanExitCode Write-DriverLog -Message "DCU scan completed: $($exitInfo.Description)" -Severity Info # Parse results $xmlPath = Join-Path $reportPath "DCUApplicableUpdates.xml" if (-not (Test-Path $xmlPath)) { Write-DriverLog -Message "No updates report generated" -Severity Info return @() } [xml]$updatesXml = Get-Content $xmlPath $updates = $updatesXml.updates.update | Where-Object { $typeMatch = switch ($_.type) { 'Driver' { 'Driver' -in $UpdateTypes -or 'All' -in $UpdateTypes } 'BIOS' { 'BIOS' -in $UpdateTypes -or 'All' -in $UpdateTypes } 'Firmware' { 'Firmware' -in $UpdateTypes -or 'All' -in $UpdateTypes } default { 'All' -in $UpdateTypes } } $typeMatch } | ForEach-Object { [PSCustomObject]@{ Name = $_.name Version = $_.version Type = $_.type Category = $_.category Urgency = $_.urgency ReleaseDate = $_.date Size = $_.size Description = $_.description } } Write-DriverLog -Message "Found $($updates.Count) Dell updates" -Severity Info ` -Context @{ Updates = ($updates | Select-Object Name, Version, Type) } return $updates } function Install-DellDriverUpdates { <# .SYNOPSIS Installs Dell driver updates .DESCRIPTION Uses Dell Command Update to install applicable updates .PARAMETER UpdateTypes Types of updates to install .PARAMETER Severity Severity levels to include .PARAMETER NoReboot Suppress automatic reboot .EXAMPLE Install-DellDriverUpdates -UpdateTypes Driver -NoReboot .OUTPUTS DriverUpdateResult object #> [CmdletBinding(SupportsShouldProcess)] [OutputType('DriverUpdateResult')] param( [Parameter()] [ValidateSet('Driver', 'BIOS', 'Firmware', 'All')] [string[]]$UpdateTypes = @('Driver'), [Parameter()] [ValidateSet('Critical', 'Recommended', 'Optional')] [string[]]$Severity = @('Critical', 'Recommended'), [Parameter()] [switch]$NoReboot ) Assert-Elevation -Operation "Installing Dell drivers" $result = [DriverUpdateResult]::new() $result.CorrelationId = $script:CorrelationId try { $dcuCli = Initialize-DellModule } catch { $result.Success = $false $result.Message = "Dell Command Update not available: $($_.Exception.Message)" $result.ExitCode = 1 Write-DriverLog -Message $result.Message -Severity Error return $result } # Configure DCU for silent operation $configArgs = @('/configure', '-userConsent=disable', '-autoSuspendBitLocker=enable', '-silent') & $dcuCli @configArgs 2>&1 | Out-Null # Map update types $typeParam = ($UpdateTypes | ForEach-Object { $_.ToLower() }) -join ',' if ('All' -in $UpdateTypes) { $typeParam = 'driver,bios,firmware,application' } # Build apply command $applyArgs = @( '/applyUpdates' "-updateType=$typeParam" '-updateSeverity=security,critical,recommended' '-autoSuspendBitLocker=enable' '-silent' "-outputLog=$env:ProgramData\Dell\Logs\DCU_$(Get-Date -Format 'yyyyMMdd_HHmmss').log" ) if ($NoReboot) { $applyArgs += '-reboot=disable' } if ($PSCmdlet.ShouldProcess("Dell drivers", "Install updates")) { Write-DriverLog -Message "Installing Dell updates: $typeParam" -Severity Info # Pre-flight diagnostics that help explain "privilege" failures even when elevated. $svcDiag = Ensure-DellClientManagementServiceInternal $programDataDell = Join-Path $env:ProgramData 'Dell' $programDataDellLogs = Join-Path $programDataDell 'Logs' $pdDellWritable = Test-WriteAccessInternal -Path $programDataDell $pdDellLogsWritable = Test-WriteAccessInternal -Path $programDataDellLogs $isElevated = Test-IsElevated $applyResult = & $dcuCli @applyArgs 2>&1 $exitCode = $LASTEXITCODE # Retry once on privilege-related exit codes after ensuring service/write access. if ($exitCode -in @(4, 5)) { Write-DriverLog -Message "DCU returned exit code $exitCode. Elevated=$isElevated. Retrying once after service/permission checks..." -Severity Warning ` -Context @{ DCUExitCode = $exitCode; Elevated = $isElevated; Service = $svcDiag; ProgramDataDellWritable = $pdDellWritable; ProgramDataDellLogsWritable = $pdDellLogsWritable } $svcDiag = Ensure-DellClientManagementServiceInternal $pdDellWritable = Test-WriteAccessInternal -Path $programDataDell $pdDellLogsWritable = Test-WriteAccessInternal -Path $programDataDellLogs $applyResult = & $dcuCli @applyArgs 2>&1 $exitCode = $LASTEXITCODE } # Get detailed exit info $exitInfo = Get-DCUExitInfo -ExitCode $exitCode # Interpret exit codes # Note: DCU sometimes returns exit code 5 (admin privilege) when the real issue # is a pending reboot. We parse the output to detect this and report accurately. $dcuOutputStr = $applyResult | Out-String switch ($exitCode) { 0 { $result.Success = $true $result.Message = "Updates applied successfully" $result.RebootRequired = $false $result.UpdatesApplied = 1 } 1 { $result.Success = $true $result.Message = "Updates applied - reboot required" $result.RebootRequired = $true $result.UpdatesApplied = 1 } { $_ -in @(500, 18) } { $result.Success = $true $result.Message = "No applicable updates" $result.RebootRequired = $false $result.UpdatesApplied = 0 } 5 { # Exit code 5 can mean admin privilege OR pending reboot (DCU bug) # Check actual output to determine the real issue if ($dcuOutputStr -match 'reboot|restart') { $result.Success = $false $result.Message = "System reboot pending - please restart the computer and try again" $result.RebootRequired = $true } else { $result.Success = $false $result.Message = "$($exitInfo.Description) - $($exitInfo.Resolution)" $result.RebootRequired = $false } } 3006 { # System is in Windows OOBE (Out of Box Experience) - DCU is blocked # Fall back to direct driver pack installation Write-DriverLog -Message "DCU blocked by OOBE state - falling back to direct driver pack installation" -Severity Warning $directResult = Install-DellDriverPackDirect -NoReboot:$NoReboot $result.Success = $directResult.Success $result.Message = $directResult.Message $result.UpdatesApplied = $directResult.UpdatesApplied $result.UpdatesFailed = $directResult.UpdatesFailed $result.RebootRequired = $directResult.RebootRequired $result.Details['DirectInstall'] = $true } default { $result.Success = $false # Include the actual exit code in the message for better debugging $result.Message = "$($exitInfo.Description) (DCU exit code: $exitCode) - $($exitInfo.Resolution)" $result.RebootRequired = $false $result.UpdatesFailed = 1 } } $result.ExitCode = if ($result.RebootRequired) { 3010 } elseif ($result.Success) { 0 } else { 1 } $result.Details = @{ DCUExitCode = $exitCode DCUExitInfo = $exitInfo Elevated = $isElevated Service = $svcDiag ProgramDataDellWritable = $pdDellWritable ProgramDataDellLogsWritable = $pdDellLogsWritable } $sev = if ($result.Success) { 'Info' } else { 'Error' } Write-DriverLog -Message "Dell update complete: $($result.Message)" -Severity $sev ` -Context $result.ToHashtable() } return $result } function Install-DellFullDriverPack { <# .SYNOPSIS Installs the complete Dell driver pack .DESCRIPTION Performs a full driver reinstallation using Dell Command Update .PARAMETER NoReboot Suppress automatic reboot .EXAMPLE Install-DellFullDriverPack -NoReboot #> [CmdletBinding(SupportsShouldProcess)] [OutputType('DriverUpdateResult')] param( [Parameter()] [switch]$NoReboot ) Assert-Elevation -Operation "Installing Dell full driver pack" $result = [DriverUpdateResult]::new() $result.CorrelationId = $script:CorrelationId try { $dcuCli = Initialize-DellModule } catch { $result.Success = $false $result.Message = "Dell Command Update not available: $($_.Exception.Message)" $result.ExitCode = 1 Write-DriverLog -Message $result.Message -Severity Error return $result } if ($PSCmdlet.ShouldProcess("Dell full driver pack", "Install")) { Write-DriverLog -Message "Starting Dell full driver pack install" -Severity Info $installArgs = @( '/driverInstall' '-autoSuspendBitLocker=enable' '-silent' ) if ($NoReboot) { $installArgs += '-reboot=disable' } & $dcuCli @installArgs 2>&1 | Out-Null $exitCode = $LASTEXITCODE $exitInfo = Get-DCUExitInfo -ExitCode $exitCode $result.Success = $exitCode -in @(0, 1, 500, 18) $result.Message = "$($exitInfo.Description)" $result.RebootRequired = $exitCode -eq 1 $result.ExitCode = if ($exitCode -eq 1) { 3010 } elseif ($exitCode -in @(0, 500, 18)) { 0 } else { 1 } $result.Details = @{ DCUExitCode = $exitCode; DCUExitInfo = $exitInfo } Write-DriverLog -Message $result.Message -Severity Info -Context $result.ToHashtable() } return $result } #endregion |