Public/Import-DPLibrary.ps1
|
function Import-DPLibrary { <# .SYNOPSIS Import DLLPickle dependency libraries. .DESCRIPTION Import all relevant DLL files for the appropriate target framework and PowerShell edition. The latest versions of all dependencies are automatically imported, providing backwards compatibility and avoiding version conflicts. This function implements dependency-aware loading with automatic retry logic. Some assemblies have transitive dependencies that must be loaded first. The function will retry failed assemblies up to 5 times to allow for dependency resolution. This eliminates false warnings on initial load attempts in Windows PowerShell when dependency load order cannot be predicted. .PARAMETER ShowLoaderExceptions Display detailed loader exception information when an assembly fails to load. This is useful for diagnosing why specific types within an assembly cannot be loaded. .PARAMETER SuppressLogo Suppress the display of the module logo during execution. .INPUTS None. This function does not accept pipeline input. .EXAMPLE Import-DPLibrary Imports all dependency DLLs from the appropriate TFM directory. .EXAMPLE Import-DPLibrary -ShowLoaderExceptions Imports DLLs and displays detailed diagnostic information for any failures. .OUTPUTS System.Management.Automation.PSCustomObject Returns information about each imported DLL. .NOTES Some assemblies may have partial compatibility issues in Windows PowerShell due to dependencies on types not available in .NET Framework 4.8. The function will continue loading other assemblies and provide detailed diagnostic information about failures. A status of 'Imported' indicates the assembly file was successfully loaded by the runtime, but this does not guarantee that all types within the assembly are usable. Some types may remain unavailable due to unresolved transitive dependencies. The function uses a retry strategy to handle transitive dependencies: on the first failed load attempt, a verbose message is written instead of a warning. If the assembly loads successfully on a retry (after dependencies are satisfied), the original verbose message is suppressed. Warnings are shown for assemblies that still fail after all retry attempts, and for non-retryable errors. #> [CmdletBinding()] [OutputType('DLLPickle.ImportDPLibraryResult')] param ( [Parameter()] [switch]$ShowLoaderExceptions, [Parameter()] [switch]$SuppressLogo ) $Settings = Get-DPConfig if ($Settings.ShowLogo -and -not $SuppressLogo) { Show-DPLogo } # Determine the module's base directory. $ModuleDirectory = if ($PSModuleRoot) { $PSModuleRoot } elseif ($PSScriptRoot) { Split-Path -Path $PSScriptRoot -Parent } else { $PWD.Path } # Determine the appropriate target framework moniker (TFM) based on PowerShell edition. $TargetFramework = if ($PSEdition -eq 'Core') { 'net8.0' } else { 'net48' } $BinDirectory = Join-Path -Path $ModuleDirectory -ChildPath 'bin' $TFMDirectory = Join-Path -Path $BinDirectory -ChildPath $TargetFramework Write-Verbose "Using PowerShell edition: $PSEdition" Write-Verbose "Using target framework: $TargetFramework" Write-Verbose "DLL directory: $TFMDirectory" if (-not (Test-Path -Path $TFMDirectory)) { throw "Binary directory not found for target framework '$TargetFramework' at: $TFMDirectory" } # Get all DLL files in the target framework moniker (TFM) directory. If no DLLs are found, throw an error to alert the user about potential installation issues. $DLLFiles = @(Get-ChildItem -Path $TFMDirectory -Filter '*.dll' -File -Recurse -ErrorAction Stop) if (-not $DLLFiles -or $DLLFiles.Count -eq 0) { throw "No DLL files found in '$TFMDirectory'. Ensure that the module is properly installed and the bin directory contains the expected assemblies." } # Skip libraries configured by the user. $SkipLibraries = @($Settings.SkipLibraries | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) if ($SkipLibraries.Count -gt 0) { $OriginalCount = $DLLFiles.Count $DLLFiles = @($DLLFiles | Where-Object { $_.Name -notin $SkipLibraries }) $SkippedByConfigCount = $OriginalCount - $DLLFiles.Count if ($SkippedByConfigCount -gt 0) { Write-Verbose "Skipped $SkippedByConfigCount libraries per config: $($SkipLibraries -join ', ')" } } # Import each DLL and record the results using dependency-aware loading with retry logic. # Some assemblies have transitive dependencies that must be loaded first. Retrying failed # assemblies allows dependencies to be satisfied from previous attempts. $DLLFileQueue = @($DLLFiles) $Results = [System.Collections.Generic.List[object]]::new() $ResultDLLNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $MaxRetries = 5 $RetryCount = 0 $LoadFailureDetailsByDLLName = @{} $InitiallyLoadedAssemblyKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($Loaded in [System.AppDomain]::CurrentDomain.GetAssemblies()) { $LoadedName = $Loaded.GetName() [void]$InitiallyLoadedAssemblyKeys.Add("$($LoadedName.Name)|$($LoadedName.Version)") } $RecordFinalFailure = { param ($DLLFile) if ($ResultDLLNames.Contains($DLLFile.Name)) { return } $FilePath = $DLLFile.FullName $FailureDetails = $LoadFailureDetailsByDLLName[$DLLFile.Name] if ($FailureDetails) { Write-Warning "Failed to import $($DLLFile.Name): $($FailureDetails.ErrorMessage)" if ($ShowLoaderExceptions -and $FailureDetails.LoaderExceptions) { Write-Warning "Loader Exceptions ($($FailureDetails.LoaderExceptions.Count) total):" foreach ($LoaderException in $FailureDetails.LoaderExceptions | Select-Object -First 5) { Write-Warning " - $($LoaderException.Message)" } if ($FailureDetails.LoaderExceptions.Count -gt 5) { Write-Warning " ... and $($FailureDetails.LoaderExceptions.Count - 5) more exceptions" } } } else { Write-Warning "Failed to import $($DLLFile.Name): Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information." } try { $AssemblyName = [System.Reflection.AssemblyName]::GetAssemblyName($FilePath) } catch { $AssemblyName = [PSObject]@{ Name = $DLLFile.BaseName } } if (-not $ResultDLLNames.Contains($DLLFile.Name)) { [void]$Results.Add([PSCustomObject]@{ PSTypeName = 'DLLPickle.ImportDPLibraryResult' DLLName = $DLLFile.Name AssemblyName = $AssemblyName.Name AssemblyVersion = $null Status = 'Failed' Error = 'Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.' }) [void]$ResultDLLNames.Add($DLLFile.Name) } } while ($DLLFileQueue.Count -gt 0 -and $RetryCount -lt $MaxRetries) { $UnresolvedDLLFiles = [System.Collections.Generic.List[object]]::new() foreach ($DLLFile in $DLLFileQueue) { $FilePath = $DLLFile.FullName try { # Check if assembly is already loaded $AssemblyName = [System.Reflection.AssemblyName]::GetAssemblyName($FilePath) $AssemblyKey = "$($AssemblyName.Name)|$($AssemblyName.Version)" $LoadedAssembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq $AssemblyName.Name -and $_.GetName().Version -eq $AssemblyName.Version } if ($LoadedAssembly) { $Status = if ($InitiallyLoadedAssemblyKeys.Contains($AssemblyKey)) { 'Already Loaded' } else { 'Imported' } if ($Status -eq 'Already Loaded') { Write-Verbose "Assembly already loaded: $($DLLFile.BaseName)" } else { Write-Verbose "Assembly was loaded during this invocation: $($DLLFile.BaseName)" } [void]$Results.Add([PSCustomObject]@{ PSTypeName = 'DLLPickle.ImportDPLibraryResult' DLLName = $DLLFile.Name AssemblyName = $AssemblyName.Name AssemblyVersion = $AssemblyName.Version.ToString() Status = $Status Error = $null }) [void]$ResultDLLNames.Add($DLLFile.Name) } else { Add-Type -Path $FilePath Write-Verbose "Successfully imported: $($DLLFile.BaseName)" [void]$Results.Add([PSCustomObject]@{ PSTypeName = 'DLLPickle.ImportDPLibraryResult' DLLName = $DLLFile.Name AssemblyName = $AssemblyName.Name AssemblyVersion = $AssemblyName.Version.ToString() Status = 'Imported' Error = $null }) [void]$ResultDLLNames.Add($DLLFile.Name) } } catch [System.Reflection.ReflectionTypeLoadException] { # Assembly failed to load; dependencies may not be loaded yet. Retry later. [void]$UnresolvedDLLFiles.Add($DLLFile) $LoaderExceptions = $_.Exception.LoaderExceptions $ErrorMessage = $_.Exception.Message $LoadFailureDetailsByDLLName[$DLLFile.Name] = [PSCustomObject]@{ ErrorMessage = $ErrorMessage LoaderExceptions = $LoaderExceptions } if ($RetryCount -eq 0) { # Only show verbose output on first attempt; dependencies may resolve on retry if ($ShowLoaderExceptions -and $LoaderExceptions) { Write-Verbose "Failed to import $($DLLFile.Name): $ErrorMessage" Write-Verbose "Loader Exceptions ($($LoaderExceptions.Count) total):" foreach ($LoaderException in $LoaderExceptions | Select-Object -First 5) { Write-Verbose " - $($LoaderException.Message)" } if ($LoaderExceptions.Count -gt 5) { Write-Verbose " ... and $($LoaderExceptions.Count - 5) more exceptions" } } else { Write-Verbose "Failed to import $($DLLFile.Name) (attempt $(($RetryCount + 1))/$MaxRetries): $ErrorMessage" if (-not $ShowLoaderExceptions) { Write-Verbose 'Use -ShowLoaderExceptions to see detailed loader exception information' } } } } catch { # Other error; do not retry Write-Warning "Failed to import $($DLLFile.Name): $_" [void]$Results.Add([PSCustomObject]@{ PSTypeName = 'DLLPickle.ImportDPLibraryResult' DLLName = $DLLFile.Name AssemblyName = $null AssemblyVersion = $null Status = 'Failed' Error = $_.Exception.Message }) [void]$ResultDLLNames.Add($DLLFile.Name) } } # If all assemblies loaded successfully, exit the retry loop if ($UnresolvedDLLFiles.Count -eq 0) { $DLLFileQueue = @() break } # If no progress was made (same failures as last iteration), record failures and exit if ($UnresolvedDLLFiles.Count -eq $DLLFileQueue.Count) { $RetryCount++ if ($RetryCount -ge $MaxRetries) { # Record remaining unresolved DLLs as failures foreach ($DLLFile in $UnresolvedDLLFiles) { & $RecordFinalFailure $DLLFile } $DLLFileQueue = @() break } } else { $RetryCount++ } $DLLFileQueue = $UnresolvedDLLFiles } if ($DLLFileQueue.Count -gt 0) { foreach ($DLLFile in $DLLFileQueue) { & $RecordFinalFailure $DLLFile } } $Results } |