Bsn.Common.psm1
|
#Requires -Version 5.1 <# Module loader for Bsn.Common. Defines the shared BsnOperatingSystem enum and Get-BsnOperatingSystem in this single file on purpose: PowerShell keyword-defined types (enum/class) are only resolvable as bare names within the compilation unit that defines them. Consumers must import this module and, when they need the type in a separately compiled file, add `using module Bsn.Common` at the top of that file. Never place a bare [BsnOperatingSystem] literal inside an attribute (e.g. [OutputType([BsnOperatingSystem])]) in a dot-sourced file - attribute binding happens at compile time and cannot resolve dynamic enums. #> Set-StrictMode -Version 'Latest' enum BsnOperatingSystem { Windows Linux macOS Unknown } <# .SYNOPSIS Detects the operating system PowerShell is running on. .DESCRIPTION Uses the native .NET platform API ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform, available in .NET Core and .NET Framework 4.7.1+, which Windows 10 1809+ ships with) and falls back to $env:OS on older Windows PowerShell hosts. .OUTPUTS BsnOperatingSystem One of Windows, Linux, macOS or Unknown. #> function Get-BsnOperatingSystem { [CmdletBinding()] [OutputType('BsnOperatingSystem')] param() try { if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { return [BsnOperatingSystem]::Windows } if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { return [BsnOperatingSystem]::Linux } if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::OSX)) { return [BsnOperatingSystem]::macOS } return [BsnOperatingSystem]::Unknown } catch { # Windows PowerShell on .NET Framework < 4.7.1 lacks RuntimeInformation; # $env:OS is only defined on Windows. if ($env:OS -eq 'Windows_NT') { return [BsnOperatingSystem]::Windows } return [BsnOperatingSystem]::Unknown } } <# .SYNOPSIS Loads a module interop assembly with same-session reimport tolerance. .DESCRIPTION PowerShell's assembly loading dedupes by simple assembly name, so a module that ships its DLL through RequiredAssemblies (or plain Add-Type) cannot be re-imported from a different installed version in the same session: the second load fails with FileLoadException ("Assembly with same name is already loaded"). The Bsn.* modules load their interop assemblies through this helper instead of RequiredAssemblies. The DLL is resolved from the calling module's own lib directory ('<ModuleRoot>/lib/<AssemblyName>.dll'): - when an assembly with the same name AND version is already loaded (Import-Module -Force of the same version), it is reused silently; - when a DIFFERENT version is already loaded (Import-Module after Update-Module), it fails with actionable guidance, because PowerShell cannot host two versions of the same assembly in one process - this is why the interop assemblies are stamped with the module version at build time (build.ps1 passes -p:Version from the manifest; guard tests assert the equality); - otherwise the assembly is loaded from the resolved path. https://github.com/BSVN/Lmt.Commons/issues/15 Kept private (consumer module loaders invoke it through this module's scope) so it does not extend Bsn.Common's public command surface. #> function Import-BsnInteropAssembly { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ModuleRoot, [Parameter(Mandatory = $true)] [string]$AssemblyName ) $assemblyPath = Join-Path -Path (Join-Path -Path $ModuleRoot -ChildPath 'lib') -ChildPath ($AssemblyName + '.dll') if (-not (Test-Path -LiteralPath $assemblyPath)) { throw ("The interop assembly is missing at '{0}'. Run './build.ps1' to build it, or reinstall the module." -f $assemblyPath) } $fileVersion = [System.Reflection.AssemblyName]::GetAssemblyName($assemblyPath).Version $loadedAssemblies = @(Get-BsnLoadedInteropAssembly -AssemblyName $AssemblyName) if ($loadedAssemblies.Count -gt 0) { $loadedVersion = $loadedAssemblies[0].GetName().Version if ($loadedVersion -ne $fileVersion) { throw ("Cannot load {0} version {1}: version {2} is already loaded in this session (from '{3}'). PowerShell cannot host two versions of the same assembly in one process; restart the session (or open a new one) and import the module again. https://github.com/BSVN/Lmt.Commons/issues/15" -f $AssemblyName, $fileVersion, $loadedVersion, $loadedAssemblies[0].Location) } # Same version already loaded (e.g. Import-Module -Force): reuse it. return } Add-Type -Path $assemblyPath } <# .SYNOPSIS Lists the loaded assemblies with the given simple assembly name. .DESCRIPTION Private seam between Import-BsnInteropAssembly and the AppDomain. Isolated into a function so unit tests can Mock it (-ModuleName Bsn.Common) to simulate a differently-versioned assembly already being loaded - the static AppDomain state itself cannot be faked. #> function Get-BsnLoadedInteropAssembly { [CmdletBinding()] [OutputType('System.Reflection.Assembly')] param( [Parameter(Mandatory = $true)] [string]$AssemblyName ) @([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq $AssemblyName }) } |