Private/Utilities.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Utility functions for DriverManagement module #> function Invoke-WithRetry { <# .SYNOPSIS Executes a script block with retry logic .DESCRIPTION Implements exponential backoff retry pattern .PARAMETER ScriptBlock The code to execute .PARAMETER MaxAttempts Maximum number of attempts .PARAMETER InitialDelayMs Initial delay between retries in milliseconds .PARAMETER ExponentialBackoff Use exponential backoff for delays #> [CmdletBinding()] param( [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [Parameter()] [int]$MaxAttempts = 5, [Parameter()] [int]$InitialDelayMs = 2000, [Parameter()] [switch]$ExponentialBackoff ) $attempt = 0 $lastError = $null while ($attempt -lt $MaxAttempts) { $attempt++ try { $ErrorActionPreference = 'Stop' return Invoke-Command -ScriptBlock $ScriptBlock } catch { $lastError = $_ if ($attempt -ge $MaxAttempts) { Write-DriverLog -Message "Operation failed after $MaxAttempts attempts: $($_.Exception.Message)" -Severity Error throw } $delay = if ($ExponentialBackoff) { [Math]::Min($InitialDelayMs * [Math]::Pow(2, $attempt - 1), 60000) } else { $InitialDelayMs } $jitter = Get-Random -Minimum 0 -Maximum 1000 $totalDelay = $delay + $jitter Write-DriverLog -Message "Attempt $attempt failed. Retrying in $($totalDelay)ms..." -Severity Warning ` -Context @{ Error = $_.Exception.Message; Attempt = $attempt } Start-Sleep -Milliseconds $totalDelay } } } function Initialize-TlsForDownloads { <# .SYNOPSIS Ensures modern TLS protocols are enabled for outbound HTTPS requests. .DESCRIPTION Some endpoints (including Intel download hosts) reject older TLS defaults, which can cause: "The underlying connection was closed: An unexpected error occurred on a send." This function enables TLS 1.2 in the current PowerShell process. #> [CmdletBinding()] param() try { $current = [System.Net.ServicePointManager]::SecurityProtocol # Always include TLS 1.2 if available if ([enum]::GetNames([System.Net.SecurityProtocolType]) -contains 'Tls12') { $current = $current -bor [System.Net.SecurityProtocolType]::Tls12 } # Best-effort: keep TLS 1.1 if present (some older middleboxes) if ([enum]::GetNames([System.Net.SecurityProtocolType]) -contains 'Tls11') { $current = $current -bor [System.Net.SecurityProtocolType]::Tls11 } [System.Net.ServicePointManager]::SecurityProtocol = $current # Reduce edge-case HTTP behavior problems [System.Net.ServicePointManager]::Expect100Continue = $false } catch { # Non-fatal; continue without TLS tuning } } function Test-PendingReboot { <# .SYNOPSIS Checks if a system reboot is pending .DESCRIPTION Checks multiple registry locations for pending reboot flags .EXAMPLE if (Test-PendingReboot) { Write-Host "Reboot required" } #> [CmdletBinding()] param() $rebootPaths = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' ) foreach ($path in $rebootPaths) { if (Test-Path $path) { return $true } } # Check PendingFileRenameOperations $sessionManager = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' $pendingRenames = (Get-ItemProperty -Path $sessionManager -Name 'PendingFileRenameOperations' -ErrorAction SilentlyContinue).PendingFileRenameOperations if ($pendingRenames) { return $true } return $false } function Test-IsElevated { <# .SYNOPSIS Checks if current process is running elevated #> [CmdletBinding()] param() $principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Start-DownloadWithVerification { <# .SYNOPSIS Downloads a file with BITS and optional hash verification .PARAMETER SourceUrl URL to download from .PARAMETER DestinationPath Local path to save file .PARAMETER ExpectedHash Optional SHA256 hash to verify #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$SourceUrl, [Parameter(Mandatory)] [string]$DestinationPath, [Parameter()] [string]$ExpectedHash, [Parameter()] [ValidateSet('SHA256', 'SHA1', 'MD5')] [string]$HashAlgorithm = 'SHA256' ) $jobName = "DriverDownload-$(Get-Date -Format 'yyyyMMddHHmmss')" # Ensure TLS is modern enough for common download hosts Initialize-TlsForDownloads try { # Use BITS for resilient download $job = Start-BitsTransfer -Source $SourceUrl -Destination $DestinationPath -Asynchronous ` -Priority Normal -RetryInterval 600 -RetryTimeout 86400 -DisplayName $jobName -ErrorAction Stop # Monitor transfer while ($job.JobState -in @('Transferring', 'Connecting')) { if ($job.BytesTotal -gt 0) { $pct = [int](($job.BytesTransferred / $job.BytesTotal) * 100) Write-Progress -Activity "Downloading" -Status "$pct% Complete" -PercentComplete $pct } Start-Sleep -Seconds 2 $job = Get-BitsTransfer -JobId $job.JobId } Write-Progress -Activity "Downloading" -Completed if ($job.JobState -eq 'Transferred') { Complete-BitsTransfer -BitsJob $job # Verify hash if provided if ($ExpectedHash) { $actualHash = (Get-FileHash -Path $DestinationPath -Algorithm $HashAlgorithm).Hash if ($actualHash -ne $ExpectedHash) { Remove-Item $DestinationPath -Force throw "Hash verification failed. Expected: $ExpectedHash, Got: $actualHash" } } return @{ Success = $true; Path = $DestinationPath } } else { Remove-BitsTransfer -BitsJob $job -ErrorAction SilentlyContinue throw "BITS transfer failed with state: $($job.JobState)" } } catch { # Fallback to direct download Write-DriverLog -Message "BITS failed, falling back to Invoke-WebRequest" -Severity Warning Invoke-WithRetry -ScriptBlock { Initialize-TlsForDownloads Invoke-WebRequest -Uri $SourceUrl -OutFile $DestinationPath -UseBasicParsing -ErrorAction Stop } -MaxAttempts 3 -ExponentialBackoff return @{ Success = $true; Path = $DestinationPath; UsedFallback = $true } } } function Test-WinGetAvailableInternal { [CmdletBinding()] param() try { return [bool](Get-Command winget.exe -ErrorAction SilentlyContinue) } catch { return $false } } function Install-WinGetInternal { <# .SYNOPSIS Best-effort WinGet installation (App Installer MSIXBundle). .DESCRIPTION Downloads the App Installer MSIXBundle from the official aka.ms redirect and installs it via Add-AppxPackage. This may fail on some images (Server/LTSC/Store-disabled) and will log and return $false in that case. .OUTPUTS [bool] success #> [CmdletBinding()] param() # If already present, nothing to do if (Test-WinGetAvailableInternal) { return $true } # Use a community-maintained installer script which handles common WinGet install edge cases # (dependencies, App Installer packaging, etc.) $installerScriptUrl = 'https://raw.githubusercontent.com/asheroto/winget-installer/master/winget-install.ps1' $scriptPath = Join-Path $env:TEMP "winget-install_$(Get-Date -Format 'yyyyMMddHHmmss').ps1" try { Write-DriverLog -Message "WinGet not found. Attempting WinGet installation via asheroto/winget-installer script" -Severity Warning ` -Context @{ Url = $installerScriptUrl } Start-DownloadWithVerification -SourceUrl $installerScriptUrl -DestinationPath $scriptPath | Out-Null if (-not (Test-Path $scriptPath)) { throw "Download failed - winget-install.ps1 not found" } Write-DriverLog -Message "Running winget installer script (ExecutionPolicy Bypass)" -Severity Info # Run in a separate PowerShell process to avoid policy/state issues in the current session $psExe = (Get-Command powershell.exe -ErrorAction Stop).Source $p = Start-Process -FilePath $psExe -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File', $scriptPath) -Wait -PassThru -NoNewWindow if ($p.ExitCode -ne 0) { throw "winget installer script failed with exit code $($p.ExitCode)" } Start-Sleep -Seconds 2 if (-not (Test-WinGetAvailableInternal)) { throw "WinGet still not available after installer script" } Write-DriverLog -Message "WinGet installed successfully" -Severity Info return $true } catch { Write-DriverLog -Message "Failed to auto-install WinGet: $($_.Exception.Message)" -Severity Warning return $false } finally { Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue } } function Ensure-WinGetInternal { [CmdletBinding()] param( [Parameter()] [switch]$AutoInstall ) if (Test-WinGetAvailableInternal) { return $true } if (-not $AutoInstall) { return $false } return (Install-WinGetInternal) } function Get-InstalledDrivers { <# .SYNOPSIS Gets installed third-party drivers .PARAMETER DeviceClasses Filter by device classes .PARAMETER ThirdPartyOnly Exclude Microsoft drivers #> [CmdletBinding()] param( [Parameter()] [string[]]$DeviceClasses = @('Display', 'Net', 'MEDIA', 'USB', 'SYSTEM'), [Parameter()] [switch]$ThirdPartyOnly ) $filter = if ($DeviceClasses) { $classFilter = ($DeviceClasses | ForEach-Object { "DeviceClass='$_'" }) -join ' OR ' "($classFilter)" } else { $null } $drivers = Get-CimInstance -ClassName Win32_PnPSignedDriver -Filter $filter -ErrorAction SilentlyContinue | Where-Object { $_.DriverVersion } | Select-Object @{N='DeviceName';E={$_.DeviceName}}, @{N='HardwareID';E={$_.HardWareID}}, @{N='DriverVersion';E={$_.DriverVersion}}, @{N='DriverDate';E={$_.DriverDate}}, @{N='Provider';E={$_.DriverProviderName}}, @{N='DeviceClass';E={$_.DeviceClass}}, @{N='InfName';E={$_.InfName}} if ($ThirdPartyOnly) { $drivers = $drivers | Where-Object { $_.Provider -ne 'Microsoft' } } return $drivers } function Assert-Elevation { <# .SYNOPSIS Throws if not running elevated #> [CmdletBinding()] param( [Parameter()] [string]$Operation = "This operation" ) if (-not (Test-IsElevated)) { throw "$Operation requires elevation. Please run as Administrator." } } |