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 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')" 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 { Invoke-WebRequest -Uri $SourceUrl -OutFile $DestinationPath -UseBasicParsing -ErrorAction Stop } -MaxAttempts 3 -ExponentialBackoff return @{ Success = $true; Path = $DestinationPath; UsedFallback = $true } } } 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." } } |