MSAL.Essentials.psm1
|
# MSAL.Essentials.psm1 # Load the correct pre-built assemblies for this PowerShell edition. # PS 7+ (Core) needs the netstandard2.0 build; Windows PowerShell 5.1 (Desktop) needs net472. $DllFolder = if ($PSEdition -eq 'Core') { 'netstandard2.0' } else { 'net472' } $LibPath = Join-Path -Path $PSScriptRoot -ChildPath ('lib/{0}' -f $DllFolder) # --------------------------------------------------------------------------- # Dependency resolver # # Only the entry-point assemblies are loaded explicitly (below). Everything # else in lib\ (System.Memory, System.Text.Json, ...) is resolved on demand # by a handler that runs ONLY when the runtime can't bind the assembly itself: # # Desktop (5.1): AppDomain.AssemblyResolve. Windows PowerShell has no binding # redirects, so exact assembly versions rarely line up (see the warnings # from Update-MsalLibDependencies). The handler ignores the requested # version, prefers an assembly with that name that is ALREADY loaded # (e.g. from Az/Graph modules, avoiding duplicate copies), and otherwise # loads the file from lib\net472. # # Core (7+): AssemblyLoadContext.Default.Resolving. Fires only if the default # context can't find the assembly, so anything the .NET shared framework # already provides (System.Text.Json, System.Memory, ...) always wins over # the copies in lib\netstandard2.0. # # Both handlers only accept plain assembly names (no path separators), so a # request can never make them load a file from outside lib\. # # This is compiled C# rather than a scriptblock on purpose: the runtime can # raise these events on threads that have no PowerShell runspace, which would # crash a scriptblock handler. # --------------------------------------------------------------------------- if (-not ('MsalEssentials.AssemblyResolver' -as [type])) { if ($PSEdition -eq 'Core') { $resolverSource = @' using System; using System.IO; using System.Reflection; using System.Runtime.Loader; namespace MsalEssentials { public static class AssemblyResolver { private static string _libPath; private static Func<AssemblyLoadContext, AssemblyName, Assembly> _handler; public static void Register(string libPath) { _libPath = libPath; if (_handler != null) { return; } _handler = Resolve; AssemblyLoadContext.Default.Resolving += _handler; } public static void Unregister() { if (_handler == null) { return; } AssemblyLoadContext.Default.Resolving -= _handler; _handler = null; } private static Assembly Resolve(AssemblyLoadContext context, AssemblyName name) { string simpleName = name.Name; // Only plain assembly names: never anything that could point outside lib\. if (string.IsNullOrEmpty(simpleName) || simpleName.IndexOfAny(new char[] { '\\', '/', ':' }) >= 0) { return null; } string candidate = Path.Combine(_libPath, simpleName + ".dll"); if (File.Exists(candidate)) { return context.LoadFromAssemblyPath(candidate); } return null; } } } '@ } else { # Windows PowerShell 5.1 compiles with C# 5 - keep this source C# 5 compatible. $resolverSource = @' using System; using System.IO; using System.Reflection; namespace MsalEssentials { public static class AssemblyResolver { private static string _libPath; private static ResolveEventHandler _handler; public static void Register(string libPath) { _libPath = libPath; if (_handler != null) { return; } _handler = new ResolveEventHandler(Resolve); AppDomain.CurrentDomain.AssemblyResolve += _handler; } public static void Unregister() { if (_handler == null) { return; } AppDomain.CurrentDomain.AssemblyResolve -= _handler; _handler = null; } private static Assembly Resolve(object sender, ResolveEventArgs args) { string simpleName = new AssemblyName(args.Name).Name; // Only plain assembly names: never anything that could point outside lib\. if (string.IsNullOrEmpty(simpleName) || simpleName.IndexOfAny(new char[] { '\\', '/', ':' }) >= 0) { return null; } if (simpleName.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) { return null; } // Prefer a copy that is already loaded in this session. foreach (Assembly loaded in AppDomain.CurrentDomain.GetAssemblies()) { if (string.Equals(loaded.GetName().Name, simpleName, StringComparison.OrdinalIgnoreCase)) { return loaded; } } string candidate = Path.Combine(_libPath, simpleName + ".dll"); if (File.Exists(candidate)) { return Assembly.LoadFrom(candidate); } return null; } } } '@ } Add-Type -TypeDefinition $resolverSource -Language 'CSharp' } [MsalEssentials.AssemblyResolver]::Register($LibPath) # Stop the handler from outliving the module (Remove-Module / Import-Module -Force). $ExecutionContext.SessionState.Module.OnRemove = { [MsalEssentials.AssemblyResolver]::Unregister() } # --------------------------------------------------------------------------- # Entry-point assemblies # # Everything else is pulled in lazily by the resolver above. Add-Type cannot # unload an assembly, so skip anything already loaded (re-import with -Force, # or another module that ships the same assembly) - but warn if the loaded # copy is a different version than the one this module was built against. # --------------------------------------------------------------------------- $EntryAssemblies = @( 'Microsoft.IdentityModel.Abstractions' 'Microsoft.Identity.Client' ) foreach ($assemblyName in $EntryAssemblies) { $dllPath = Join-Path -Path $LibPath -ChildPath ('{0}.dll' -f $assemblyName) if (-not (Test-Path -Path $dllPath)) { throw ('Required assembly not found: {0}. The module installation is incomplete; reinstall the module. (Developers: dot-source build/Update-MsalLibDependencies.ps1 and run Update-MsalLibDependencies.)' -f $dllPath) } $alreadyLoaded = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object -FilterScript { $_.GetName().Name -eq $assemblyName } | Select-Object -First 1 if ($alreadyLoaded) { $expectedVersion = [System.Reflection.AssemblyName]::GetAssemblyName($dllPath).Version $loadedVersion = $alreadyLoaded.GetName().Version if ($loadedVersion -ne $expectedVersion) { Write-Warning ('{0} {1} is already loaded in this session (from {2}); this module was built against {3}.' -f $assemblyName, $loadedVersion, $alreadyLoaded.Location, $expectedVersion) } continue } try { Add-Type -Path $dllPath } catch { throw ('Failed to load assembly {0}: {1}' -f $dllPath, $_) } } # Dot-source every function file so it becomes available in this module's scope. # The folder names are lowercase and use forward slashes, so this also works on # case-sensitive file systems. $PublicFunctions = @(Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'public/*.ps1') -ErrorAction 'Stop') $PrivateFunctions = @(Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'private/*.ps1') -ErrorAction 'Stop') # A module that silently exports nothing is worse than one that fails to import. if ($PublicFunctions.Count -eq 0) { throw ('No public function files found in {0}. The module installation is incomplete; reinstall the module.' -f (Join-Path -Path $PSScriptRoot -ChildPath 'public')) } foreach ($function in @($PublicFunctions + $PrivateFunctions)) { try { . $function.FullName } catch { throw ('Failed to import function {0}: {1}' -f $function.FullName, $_) } } # Only public/ functions become part of the module's exposed surface; # private/ functions stay dot-sourced but internal. Export-ModuleMember -Function $PublicFunctions.BaseName |