Refactor module assembly loading logic to allow the whole module to be loaded in a custom ALC (#16889)

This commit is contained in:
Dongbo Wang
2022-02-23 15:01:12 -08:00
committed by GitHub
parent 102e8285bb
commit e1f52dda77
16 changed files with 779 additions and 415 deletions
+15 -7
View File
@@ -1046,9 +1046,10 @@ function Publish-PSTestTools {
Find-Dotnet
$tools = @(
@{Path="${PSScriptRoot}/test/tools/TestExe";Output="testexe"}
@{Path="${PSScriptRoot}/test/tools/WebListener";Output="WebListener"}
@{Path="${PSScriptRoot}/test/tools/TestService";Output="TestService"}
@{ Path="${PSScriptRoot}/test/tools/TestAlc"; Output="library" }
@{ Path="${PSScriptRoot}/test/tools/TestExe"; Output="exe" }
@{ Path="${PSScriptRoot}/test/tools/WebListener"; Output="exe" }
@{ Path="${PSScriptRoot}/test/tools/TestService"; Output="exe" }
)
$Options = Get-PSOptions -DefaultToNew
@@ -1069,12 +1070,19 @@ function Publish-PSTestTools {
Remove-Item -Path $objPath -Recurse -Force
}
if (-not $runtime) {
dotnet publish --output bin --configuration $Options.Configuration --framework $Options.Framework --runtime $Options.Runtime
} else {
dotnet publish --output bin --configuration $Options.Configuration --framework $Options.Framework --runtime $runtime
if ($tool.Output -eq 'library') {
## Handle building and publishing assemblies.
dotnet publish --configuration $Options.Configuration --framework $Options.Framework
continue
}
## Handle building and publishing executables.
if (-not $runtime) {
$runtime = $Options.Runtime
}
dotnet publish --output bin --configuration $Options.Configuration --framework $Options.Framework --runtime $runtime --self-contained
if ( -not $env:PATH.Contains($toolPath) ) {
$env:PATH = $toolPath+$TestModulePathSeparator+$($env:PATH)
}
@@ -1269,55 +1269,150 @@ namespace System.Management.Automation
internal PSTransactionManager transactionManager;
internal Assembly AddAssembly(string name, string filename, out Exception error)
/// <summary>
/// This method is used for assembly loading requests stemmed from 'InitialSessionState' binding and module loading.
/// </summary>
/// <param name="source">Source of the assembly loading request, should be a module name when specified.</param>
/// <param name="assemblyName">Name of the assembly to be loaded.</param>
/// <param name="filePath">Path of the assembly to be loaded.</param>
/// <param name="error">Exception that is caught when the loading fails.</param>
internal Assembly AddAssembly(string source, string assemblyName, string filePath, out Exception error)
{
Assembly loadedAssembly = LoadAssembly(name, filename, out error);
if (loadedAssembly == null)
return null;
if (AssemblyCache.ContainsKey(loadedAssembly.FullName))
// Search the cache by the path, and return the assembly if we find it.
// It's common to have two loading requests for the same assembly when loading a module -- the first time for
// resolving a binary module path, and the second time for actually processing that module.
//
// That's not a problem when all the module assemblies are loaded into the default ALC. But in a scenario where
// a module tries to hide its nested/root binary modules in a custom ALC, that will become a problem. This is
// because:
// in that scenario, the module will usually setup a handler to load the specific assemblies to the custom ALC,
// and that will be how the first loading request gets served. However, after the module path is resolved with
// the first loading, the path will be used for the second loading upon real module processing. Since we prefer
// loading-by-path over loading-by-name in the 'LoadAssembly' call, we will end up loading the same assembly in
// the default ALC (because we use 'Assembly.LoadFrom' which always loads an assembly to the default ALC) if we
// do not search in the cache first. That will break the scenario, because the module means to isolate all its
// dependencies from the default ALC, and it failed to do so.
//
// Therefore, we need to search the cache first. The reason we use path as the key is to make sure the request
// is for exactly the same assembly. The same assembly file should not be loaded into different ALC's by module
// loading within the same PowerShell session (Runspace).
//
// An example module targeting the abovementioned scenario will likely have the following file structure:
// IsolatedModule
// │ IsolatedModule.psd1 (has 'NestedModules = @('Test.Isolated.Init.dll', 'Test.Isolated.Nested.dll')')
// │ Test.Isolated.Init.dll (contains the custom ALC and code to setup 'Resolving' handler)
// └───Dependencies (folder under module base)
// Newtonsoft.Json.dll (version 10.0.0.0 dependency)
// Test.Isolated.Nested.dll (nested binary module referencing the particular dependency)
//
// In this example, the following events will happen in sequence:
// 1. PowerShell is able to find 'Test.Isolated.Init.dll' under module base folder, so it will be loaded into
// the default ALC as expected and setup the 'Resolving' handler via the 'OnImport' call.
// 2. PowerShell cannot find 'Test.Isolated.Nested.dll' under the module base folder, so it will call the method
// 'FixFileName(.., bool canLoadAssembly)' to resolve the path of this binary module.
// This particular overload will attempt to load the assembly by name, which will be served by the 'Resolving'
// handler that was setup in the step 1. So, the assembly will be loaded into the custom ALC and insert to the
// assembly cache.
// 3. Path of the nested module 'Test.Isolated.Init.dll' now has been resolved by the step 2 (assembly.Location).
// Now it's time to actually load this binary module for processing in the method 'LoadBinaryModule', which
// will make a call to this method with the resolved assembly file path.
// At this poin, we will have to query the cache first, instead of calling 'LoadAssembly' directly, to make sure
// that the assembly instance loaded in the custom ALC in step 2 gets returned back. Otherwise, the same assembly
// file will be loaded in the default ALC because 'Assembly.LoadFrom' is used in 'LoadAssembly' and that API will
// always load an assembly file to the default ALC, and that will break this scenario.
if (TryGetFromAssemblyCache(source, filePath, out Assembly loadedAssembly))
{
// we should ignore this assembly.
return loadedAssembly;
}
// We will cache the assembly by both full name and
// file name
AssemblyCache.Add(loadedAssembly.FullName, loadedAssembly);
if (AssemblyCache.ContainsKey(loadedAssembly.GetName().Name))
{
// we should ignore this assembly.
error = null;
return loadedAssembly;
}
AssemblyCache.Add(loadedAssembly.GetName().Name, loadedAssembly);
// Attempt to load the requested assembly, first by path then by name.
loadedAssembly = LoadAssembly(assemblyName, filePath, out error);
if (loadedAssembly is not null)
{
AddToAssemblyCache(source, loadedAssembly);
}
return loadedAssembly;
}
internal void RemoveAssembly(string name)
/// <summary>
/// Add a loaded assembly to the 'AssemblyCache'.
/// The <paramref name="source"/> is used as a prefix for the key to make it easy to remove all associated
/// assemblies from the cache when a module gets unloaded.
/// </summary>
/// <param name="source">The source where the assembly comes from, should be a module name when specified.</param>
/// <param name="assembly">The assembly we try to cache.</param>
internal void AddToAssemblyCache(string source, Assembly assembly)
{
Assembly loadedAssembly;
if (AssemblyCache.TryGetValue(name, out loadedAssembly) && loadedAssembly != null)
{
AssemblyCache.Remove(name);
// Try caching the assembly by its location if possible.
// When it's a dynamic assembly, we use it's full name. This could happen with 'Import-Module -Assembly'.
string key = string.IsNullOrEmpty(assembly.Location) ? assembly.FullName : assembly.Location;
AssemblyCache.Remove(loadedAssembly.GetName().Name);
// When the assembly is from a module loading, we prefix the key with the source,
// so we can remove it from the cache when the module gets unloaded.
if (!string.IsNullOrEmpty(source))
{
// Both 'source' and 'key' are of the string type, so no need to specify 'InvariantCulture'.
key = $"{source}@{key}";
}
AssemblyCache.TryAdd(key, assembly);
}
/// <summary>
/// Remove all cache entries that are associated with the specified source.
/// </summary>
internal void RemoveFromAssemblyCache(string source)
{
if (string.IsNullOrEmpty(source))
{
return;
}
var keysToRemove = new List<string>();
string prefix = $"{source}@";
foreach (string key in AssemblyCache.Keys)
{
if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
keysToRemove.Add(key);
}
}
foreach (string key in keysToRemove)
{
AssemblyCache.Remove(key);
}
}
[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadWithPartialName")]
[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom")]
internal static Assembly LoadAssembly(string name, string filename, out Exception error)
/// <summary>
/// Try to get an assembly from the cache.
/// </summary>
private bool TryGetFromAssemblyCache(string source, string filePath, out Assembly assembly)
{
if (string.IsNullOrEmpty(filePath))
{
assembly = null;
return false;
}
// Both 'source' and 'filePath' are of the string type, so no need to specify 'InvariantCulture'.
string key = string.IsNullOrEmpty(source) ? filePath : $"{source}@{filePath}";
return AssemblyCache.TryGetValue(key, out assembly);
}
private static Assembly LoadAssembly(string name, string filePath, out Exception error)
{
// First we try to load the assembly based on the filename
Assembly loadedAssembly = null;
error = null;
if (!string.IsNullOrEmpty(filename))
if (!string.IsNullOrEmpty(filePath))
{
try
{
loadedAssembly = Assembly.LoadFrom(filename);
loadedAssembly = Assembly.LoadFrom(filePath);
return loadedAssembly;
}
catch (FileNotFoundException fileNotFound)
@@ -1562,7 +1657,7 @@ namespace System.Management.Automation
EngineHostInterface = hostInterface as InternalHost ?? new InternalHost(hostInterface, this);
// Hook up the assembly cache
AssemblyCache = new Dictionary<string, Assembly>();
AssemblyCache = new Dictionary<string, Assembly>(StringComparer.OrdinalIgnoreCase);
// Initialize the fixed toplevel session state and the current session state
TopLevelSessionState = EngineSessionState = new SessionStateInternal(this);
@@ -410,7 +410,7 @@ namespace System.Management.Automation.Runspaces
/// <returns>The cloned object.</returns>
public override InitialSessionStateEntry Clone()
{
SessionStateAssemblyEntry entry = new SessionStateAssemblyEntry(Name, FileName);
var entry = new SessionStateAssemblyEntry(Name, FileName);
entry.SetPSSnapIn(this.PSSnapIn);
entry.SetModule(this.Module);
return entry;
@@ -2405,9 +2405,14 @@ namespace System.Management.Automation.Runspaces
// Load the assemblies and initialize the assembly cache...
foreach (SessionStateAssemblyEntry ssae in Assemblies)
{
if (etwEnabled) RunspaceEventSource.Log.LoadAssemblyStart(ssae.Name, ssae.FileName);
Exception error = null;
Assembly asm = context.AddAssembly(ssae.Name, ssae.FileName, out error);
if (etwEnabled)
{
RunspaceEventSource.Log.LoadAssemblyStart(ssae.Name, ssae.FileName);
}
// Specify the source only if this is for module loading.
// The source is used for porper cleaning of the assembly cache when a module is unloaded.
Assembly asm = context.AddAssembly(ssae.Module?.Name, ssae.Name, ssae.FileName, out Exception error);
if (asm == null || error != null)
{
@@ -3374,105 +3379,6 @@ namespace System.Management.Automation.Runspaces
context.EngineSessionState.SetVariableAtScope(qv, "global", true, CommandOrigin.Internal);
}
/// <summary>
/// Remove anything that would have been bound by this ISS instance.
/// At this point, it removes assemblies and cmdlet entries at the top level.
/// It also removes types and formats.
/// The other entry types - functions, variables, aliases
/// are not removed by this function.
/// </summary>
/// <param name="context"></param>
internal void Unbind(ExecutionContext context)
{
lock (_syncObject)
{
SessionStateInternal ss = context.EngineSessionState;
// Remove the assemblies from the assembly cache...
foreach (SessionStateAssemblyEntry ssae in Assemblies)
{
context.RemoveAssembly(ssae.Name);
}
// Remove all of the commands from the top-level session state.
foreach (SessionStateCommandEntry cmd in Commands)
{
SessionStateCmdletEntry ssce = cmd as SessionStateCmdletEntry;
if (ssce != null)
{
List<CmdletInfo> matches;
if (context.TopLevelSessionState.GetCmdletTable().TryGetValue(ssce.Name, out matches))
{
// Remove the name from the list...
for (int i = matches.Count - 1; i >= 0; i--)
{
if (matches[i].ModuleName.Equals(cmd.PSSnapIn.Name))
{
string name = matches[i].Name;
matches.RemoveAt(i);
context.TopLevelSessionState.RemoveCmdlet(name, i, /*force*/ true);
}
}
// And remove the entry if the list is now empty...
if (matches.Count == 0)
{
context.TopLevelSessionState.RemoveCmdletEntry(ssce.Name, true);
}
}
continue;
}
}
// Remove all of the providers from the top-level provider table.
if (_providers != null && _providers.Count > 0)
{
Dictionary<string, List<ProviderInfo>> providerTable = context.TopLevelSessionState.Providers;
foreach (SessionStateProviderEntry sspe in _providers)
{
List<ProviderInfo> pl;
if (providerTable.TryGetValue(sspe.Name, out pl))
{
Diagnostics.Assert(pl != null, "There should never be a null list of entries in the provider table");
// For each provider with the same name...
for (int i = pl.Count - 1; i >= 0; i--)
{
ProviderInfo pi = pl[i];
// If it was implemented by this entry, remove it
if (pi.ImplementingType == sspe.ImplementingType)
{
RemoveAllDrivesForProvider(pi, context.TopLevelSessionState);
pl.RemoveAt(i);
}
}
// If there are no providers left with this name, remove the key.
if (pl.Count == 0)
{
providerTable.Remove(sspe.Name);
}
}
}
}
List<string> formatFilesToRemove = new List<string>();
if (this.Formats != null)
{
formatFilesToRemove.AddRange(this.Formats.Select(static f => f.FileName));
}
List<string> typeFilesToRemove = new List<string>();
if (this.Types != null)
{
typeFilesToRemove.AddRange(this.Types.Select(static t => t.FileName));
}
RemoveTypesAndFormats(context, formatFilesToRemove, typeFilesToRemove);
}
}
internal static void RemoveTypesAndFormats(ExecutionContext context, IList<string> formatFilesToRemove, IList<string> typeFilesToRemove)
{
// The formats and types tables are implemented in such a way that
@@ -3904,11 +3810,9 @@ namespace System.Management.Automation.Runspaces
this.Formats.Add(formatEntry);
}
SessionStateAssemblyEntry assemblyEntry = new SessionStateAssemblyEntry(psSnapInInfo.AssemblyName, psSnapInInfo.AbsoluteModulePath);
var assemblyEntry = new SessionStateAssemblyEntry(psSnapInInfo.AssemblyName, psSnapInInfo.AbsoluteModulePath);
assemblyEntry.SetPSSnapIn(psSnapInInfo);
this.Assemblies.Add(assemblyEntry);
Assemblies.Add(assemblyEntry);
// entry from types.ps1xml references a type (Microsoft.PowerShell.Commands.SecurityDescriptorCommandsBase) in this assembly
if (psSnapInInfo.Name.Equals(CoreSnapin, StringComparison.OrdinalIgnoreCase))
@@ -4005,20 +3909,16 @@ namespace System.Management.Automation.Runspaces
throw e;
}
Dictionary<string, SessionStateCmdletEntry> cmdlets = null;
Dictionary<string, List<SessionStateAliasEntry>> aliases = null;
Dictionary<string, SessionStateProviderEntry> providers = null;
string assemblyPath = assembly.Location;
PSSnapInHelpers.AnalyzePSSnapInAssembly(assembly, assemblyPath, psSnapInInfo: null, module, out cmdlets, out aliases, out providers, helpFile: out _);
// If this is an in-memory assembly, don't added it to the list of AssemblyEntries
// since it can't be loaded by path or name
if (!string.IsNullOrEmpty(assembly.Location))
{
SessionStateAssemblyEntry assemblyEntry = new SessionStateAssemblyEntry(assembly.FullName, assemblyPath);
this.Assemblies.Add(assemblyEntry);
}
PSSnapInHelpers.AnalyzePSSnapInAssembly(
assembly,
assemblyPath,
psSnapInInfo: null,
module,
out Dictionary<string, SessionStateCmdletEntry> cmdlets,
out Dictionary<string, List<SessionStateAliasEntry>> aliases,
out Dictionary<string, SessionStateProviderEntry> providers,
helpFile: out _);
if (cmdlets != null)
{
@@ -548,32 +548,15 @@ namespace Microsoft.PowerShell.Commands
private void ImportModule_ViaAssembly(ImportModuleOptions importModuleOptions, Assembly suppliedAssembly)
{
bool moduleLoaded = false;
string moduleName = "dynamic_code_module_" + suppliedAssembly.FullName;
// Loop through Module Cache to ensure that the module is not already imported.
if (suppliedAssembly != null && Context.Modules.ModuleTable != null)
foreach (KeyValuePair<string, PSModuleInfo> pair in Context.Modules.ModuleTable)
{
foreach (KeyValuePair<string, PSModuleInfo> pair in Context.Modules.ModuleTable)
if (pair.Value.Path == string.Empty)
{
// if the module in the moduleTable is an assembly module without path, the moduleName is the key.
string moduleName = "dynamic_code_module_" + suppliedAssembly;
if (pair.Value.Path == string.Empty)
{
if (pair.Key.Equals(moduleName, StringComparison.OrdinalIgnoreCase))
{
moduleLoaded = true;
if (BasePassThru)
{
WriteObject(pair.Value);
}
break;
}
else
{
continue;
}
}
if (pair.Value.Path.Equals(suppliedAssembly.Location, StringComparison.OrdinalIgnoreCase))
// If the module in the moduleTable is an assembly module without path, the moduleName is the key.
if (pair.Key.Equals(moduleName, StringComparison.OrdinalIgnoreCase))
{
moduleLoaded = true;
if (BasePassThru)
@@ -583,12 +566,26 @@ namespace Microsoft.PowerShell.Commands
break;
}
continue;
}
if (pair.Value.Path.Equals(suppliedAssembly.Location, StringComparison.OrdinalIgnoreCase))
{
moduleLoaded = true;
if (BasePassThru)
{
WriteObject(pair.Value);
}
break;
}
}
if (!moduleLoaded)
{
PSModuleInfo module = LoadBinaryModule(
parentModule: null,
moduleName: null,
fileName: null,
suppliedAssembly,
@@ -596,15 +593,13 @@ namespace Microsoft.PowerShell.Commands
ss: null,
importModuleOptions,
ManifestProcessingFlags.LoadElements | ManifestProcessingFlags.WriteErrors | ManifestProcessingFlags.NullOnFirstError,
this.BasePrefix,
loadTypes: false,
loadFormats: false,
BasePrefix,
out bool found);
if (found && module != null)
if (found && module is not null)
{
// Add it to all module tables ...
AddModuleToModuleTables(this.Context, this.TargetSessionState.Internal, module);
AddModuleToModuleTables(Context, TargetSessionState.Internal, module);
if (BasePassThru)
{
WriteObject(module);
@@ -1380,14 +1375,25 @@ namespace Microsoft.PowerShell.Commands
private bool IsPs1xmlFileHelper_IsPresentInEntries(RemoteDiscoveryHelper.CimModuleFile cimModuleFile, IEnumerable<string> manifestEntries)
{
if (manifestEntries.Any(s => s.EndsWith(cimModuleFile.FileName, StringComparison.OrdinalIgnoreCase)))
const string ps1xmlExt = ".ps1xml";
string fileName = cimModuleFile.FileName;
foreach (string entry in manifestEntries)
{
return true;
if (entry.EndsWith(fileName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
if (manifestEntries.Any(s => FixupFileName(string.Empty, s, ".ps1xml", isImportingModule: true).EndsWith(cimModuleFile.FileName, StringComparison.OrdinalIgnoreCase)))
foreach (string entry in manifestEntries)
{
return true;
string tempName = entry.EndsWith(ps1xmlExt, StringComparison.OrdinalIgnoreCase) ? entry : entry + ps1xmlExt;
string resolvedPath = ResolveRootedFilePath(tempName, Context);
if (resolvedPath is not null && resolvedPath.EndsWith(fileName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
@@ -1894,13 +1900,10 @@ namespace Microsoft.PowerShell.Commands
else if (this.ParameterSetName.Equals(ParameterSet_Assembly, StringComparison.OrdinalIgnoreCase))
{
// Now load all of the supplied assemblies...
if (Assembly != null)
foreach (Assembly suppliedAssembly in Assembly)
{
foreach (Assembly suppliedAssembly in Assembly)
{
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, suppliedAssembly.GetName().Name);
ImportModule_ViaAssembly(importModuleOptions, suppliedAssembly);
}
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, suppliedAssembly.GetName().Name);
ImportModule_ViaAssembly(importModuleOptions, suppliedAssembly);
}
}
else if (this.ParameterSetName.Equals(ParameterSet_Name, StringComparison.OrdinalIgnoreCase))
@@ -682,9 +682,19 @@ namespace Microsoft.PowerShell.Commands
return result;
}
private PSModuleInfo LoadModuleNamedInManifest(PSModuleInfo parentModule, ModuleSpecification moduleSpecification, string moduleBase, bool searchModulePath,
string prefix, SessionState ss, ImportModuleOptions options, ManifestProcessingFlags manifestProcessingFlags, bool loadTypes,
bool loadFormats, object privateData, out bool found, string shortModuleName, PSLanguageMode? manifestLanguageMode)
private PSModuleInfo LoadModuleNamedInManifest(
PSModuleInfo parentModule,
ModuleSpecification moduleSpecification,
string moduleBase,
bool searchModulePath,
string prefix,
SessionState ss,
ImportModuleOptions options,
ManifestProcessingFlags manifestProcessingFlags,
object privateData,
out bool found,
string shortModuleName,
PSLanguageMode? manifestLanguageMode)
{
PSModuleInfo module = null;
PSModuleInfo tempModuleInfoFromVerification = null;
@@ -698,11 +708,16 @@ namespace Microsoft.PowerShell.Commands
var importingModule = manifestProcessingFlags.HasFlag(ManifestProcessingFlags.LoadElements);
string extension = Path.GetExtension(moduleSpecification.Name);
// First check for fully-qualified paths - either absolute or relative
string rootedPath = ResolveRootedFilePath(moduleSpecification.Name, this.Context);
if (string.IsNullOrEmpty(rootedPath))
{
rootedPath = FixupFileName(moduleBase, moduleSpecification.Name, extension, importingModule);
// Use the name of the parent module if it's specified, otherwise, use the current module name.
// - If the current module is a nested module, then the parent module will be specifeid.
// - If the current module is a root module, then the parent module will not be specified.
string moduleName = parentModule?.Name ?? ModuleIntrinsics.GetModuleName(moduleSpecification.Name);
rootedPath = FixFileName(moduleName, moduleBase, moduleSpecification.Name, extension: null, canLoadAssembly: importingModule);
}
else
{
@@ -925,8 +940,6 @@ namespace Microsoft.PowerShell.Commands
options,
manifestProcessingFlags,
prefix,
loadTypes,
loadFormats,
out found,
shortModuleName,
disableFormatUpdates: false);
@@ -1524,6 +1537,7 @@ namespace Microsoft.PowerShell.Commands
Dbg.Assert(moduleManifestPath != null, "moduleManifestPath for module (.psd1) can't be null");
string moduleBase = Path.GetDirectoryName(moduleManifestPath);
string moduleName = ModuleIntrinsics.GetModuleName(moduleManifestPath);
if ((manifestProcessingFlags &
(ManifestProcessingFlags.LoadElements | ManifestProcessingFlags.WriteErrors |
@@ -1624,24 +1638,32 @@ namespace Microsoft.PowerShell.Commands
invalidOperation.SetErrorId("Modules_WildCardNotAllowedInModuleToProcessAndInNestedModules");
throw invalidOperation;
}
// See if this module is already loaded. Since the manifest entry may not
// have an extension and the module table is indexed by full names, we
// may have search through all the extensions.
PSModuleInfo loadedModule = null;
string rootedPath = this.FixupFileName(moduleBase, actualRootModule, extension: null, importingModule);
string mtpExtension = Path.GetExtension(rootedPath);
if (!string.IsNullOrEmpty(mtpExtension) && ModuleIntrinsics.IsPowerShellModuleExtension(mtpExtension))
string rootedPath = null;
// For a root module, we use its own module name instead of the manifest module name when calling 'FixFileName'.
// This is because when actually loading the root module later, it won't have access to the parent manifest module,
// and we will use its own name to query for already loaded assemblies from 'Context.AssemblyCache'.
string rootModuleName = ModuleIntrinsics.GetModuleName(actualRootModule);
string extension = Path.GetExtension(actualRootModule);
if (!string.IsNullOrEmpty(extension) && ModuleIntrinsics.IsPowerShellModuleExtension(extension))
{
rootedPath = FixFileName(rootModuleName, moduleBase, actualRootModule, extension: null, canLoadAssembly: importingModule);
TryGetFromModuleTable(rootedPath, out loadedModule);
}
else
{
foreach (string extensionToTry in ModuleIntrinsics.PSModuleExtensions)
{
rootedPath = this.FixupFileName(moduleBase, actualRootModule, extensionToTry, importingModule);
TryGetFromModuleTable(rootedPath, out loadedModule);
if (loadedModule != null)
rootedPath = FixFileName(rootModuleName, moduleBase, actualRootModule, extensionToTry, canLoadAssembly: importingModule);
if (TryGetFromModuleTable(rootedPath, out loadedModule))
{
break;
}
}
}
@@ -2049,7 +2071,6 @@ namespace Microsoft.PowerShell.Commands
{
bool nameMissingOrEmpty = false;
var invalidNames = new List<string>();
string moduleName = ModuleIntrinsics.GetModuleName(moduleManifestPath);
expFeatureList = new List<ExperimentalFeature>(features.Length);
foreach (Hashtable feature in features)
@@ -2162,61 +2183,72 @@ namespace Microsoft.PowerShell.Commands
// Indicates the ISS.Bind() should be called...
bool doBind = false;
// Set up to load any required assemblies that have been specified...
List<string> tmpAssemblyList;
List<string> assemblyList = new List<string>();
List<string> fixedUpAssemblyPathList = new List<string>();
if (
!GetListOfStringsFromData(data, moduleManifestPath, "RequiredAssemblies", manifestProcessingFlags,
out tmpAssemblyList))
if (!GetListOfStringsFromData(
data,
moduleManifestPath,
"RequiredAssemblies",
manifestProcessingFlags,
out List<string> assemblyList))
{
containedErrors = true;
if (bailOnFirstError) return null;
}
else
{
if (tmpAssemblyList != null && tmpAssemblyList.Count > 0)
if (bailOnFirstError)
{
foreach (string assembly in tmpAssemblyList)
{
assemblyList.Add(assembly);
}
return null;
}
if ((assemblyList != null) && importingModule)
}
else if (assemblyList != null && importingModule)
{
foreach (string assembly in assemblyList)
{
foreach (string assembly in assemblyList)
if (WildcardPattern.ContainsWildcardCharacters(assembly))
{
if (WildcardPattern.ContainsWildcardCharacters(assembly))
PSInvalidOperationException invalidOperation = PSTraceSource.NewInvalidOperationException(
Modules.WildCardNotAllowedInRequiredAssemblies,
moduleManifestPath);
invalidOperation.SetErrorId("Modules_WildCardNotAllowedInRequiredAssemblies");
throw invalidOperation;
}
else
{
string fileName = null;
string ext = Path.GetExtension(assembly);
// Note that we don't need to load the required assemblies eagerly because they will be loaded before
// processing type and format data. So, when calling 'FixupFileName', we only attempt to resolve the
// path, and avoid triggering the loading of the assembly.
if (ModuleIntrinsics.ProcessableAssemblyExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase))
{
PSInvalidOperationException invalidOperation = PSTraceSource.NewInvalidOperationException(
Modules.WildCardNotAllowedInRequiredAssemblies,
moduleManifestPath);
invalidOperation.SetErrorId("Modules_WildCardNotAllowedInRequiredAssemblies");
throw invalidOperation;
fileName = FixFileNameWithoutLoadingAssembly(moduleBase, assembly, extension: null);
}
else
{
string fileName = FixupFileName(moduleBase, assembly, StringLiterals.PowerShellNgenAssemblyExtension, importingModule, out bool pathIsResolved);
if (!pathIsResolved)
bool isPathResolved = false;
foreach (string extToTry in ModuleIntrinsics.ProcessableAssemblyExtensions)
{
fileName = FixupFileName(moduleBase, assembly, StringLiterals.PowerShellILAssemblyExtension, importingModule);
fileName = FixFileNameWithoutLoadingAssembly(moduleBase, assembly, extToTry, out isPathResolved);
if (isPathResolved)
{
break;
}
}
string loadMessage = StringUtil.Format(Modules.LoadingFile, "Assembly", fileName);
WriteVerbose(loadMessage);
iss.Assemblies.Add(new SessionStateAssemblyEntry(assembly, fileName));
fixedUpAssemblyPathList.Add(fileName);
fileName = FixupFileName(moduleBase, assembly, StringLiterals.PowerShellILExecutableExtension, importingModule);
loadMessage = StringUtil.Format(Modules.LoadingFile, "Executable", fileName);
WriteVerbose(loadMessage);
iss.Assemblies.Add(new SessionStateAssemblyEntry(assembly, fileName));
fixedUpAssemblyPathList.Add(fileName);
doBind = true;
if (!isPathResolved)
{
// We didn't resolve the assembly path, so remove the '.exe' extension that was added in the
// last iteration of the above loop.
int index = fileName.LastIndexOf('.');
fileName = fileName.Substring(0, index);
}
}
WriteVerbose(StringUtil.Format(Modules.LoadingFile, "Assembly", fileName));
// Set a fake PSModuleInfo object to indicate the module it comes from.
var assemblyEntry = new SessionStateAssemblyEntry(assembly, fileName);
assemblyEntry.SetModule(new PSModuleInfo(moduleName, path: null, context: null, sessionState: null));
iss.Assemblies.Add(assemblyEntry);
doBind = true;
}
}
}
@@ -2253,8 +2285,7 @@ namespace Microsoft.PowerShell.Commands
continue;
}
string resolvedEntryFileName = ResolveRootedFilePath(entry.FileName, Context) ??
entry.FileName;
string resolvedEntryFileName = ResolveRootedFilePath(entry.FileName, Context) ?? entry.FileName;
if (resolvedEntryFileName.Equals(resolvedFileName, StringComparison.OrdinalIgnoreCase))
{
isAlreadyLoaded = true;
@@ -2377,7 +2408,7 @@ namespace Microsoft.PowerShell.Commands
key: "FileList",
manifestProcessingFlags,
moduleBase,
extension: string.Empty,
extension: null,
// Don't check file existence - don't want to change current behavior without feature team discussion.
verifyFilesExist: false,
out List<string> fileList))
@@ -2517,10 +2548,18 @@ namespace Microsoft.PowerShell.Commands
// If there is a session state, set up to import/export commands and variables
if (ss != null)
{
ss.Internal.SetVariable(SpecialVariables.PSScriptRootVarPath, Path.GetDirectoryName(moduleManifestPath),
true, CommandOrigin.Internal);
ss.Internal.SetVariable(SpecialVariables.PSCommandPathVarPath, moduleManifestPath, true,
ss.Internal.SetVariable(
SpecialVariables.PSScriptRootVarPath,
moduleBase,
asValue: true,
CommandOrigin.Internal);
ss.Internal.SetVariable(
SpecialVariables.PSCommandPathVarPath,
moduleManifestPath,
asValue: true,
CommandOrigin.Internal);
ss.Internal.Module = manifestInfo;
// without ModuleToProcess a manifest will export everything by default
@@ -2974,8 +3013,6 @@ namespace Microsoft.PowerShell.Commands
ss: null,
options: nestedModuleOptions,
manifestProcessingFlags: manifestProcessingFlags,
loadTypes: true,
loadFormats: true,
privateData: privateData,
found: out found,
shortModuleName: null,
@@ -3077,8 +3114,6 @@ namespace Microsoft.PowerShell.Commands
ss: ss,
options: options,
manifestProcessingFlags: manifestProcessingFlags,
loadTypes: (exportedTypeFiles == null || exportedTypeFiles.Count == 0), // If types files already loaded, don't load snapin files
loadFormats: (exportedFormatFiles == null || exportedFormatFiles.Count == 0), // if format files already loaded, don't load snapin files
privateData: privateData,
found: out found,
shortModuleName: null,
@@ -4403,8 +4438,6 @@ namespace Microsoft.PowerShell.Commands
out List<string> list)
{
list = null;
bool importingModule = manifestProcessingFlags.HasFlag(ManifestProcessingFlags.LoadElements);
if (!GetListOfStringsFromData(data, moduleManifestPath, key, manifestProcessingFlags, out List<string> listOfStrings))
{
return false;
@@ -4426,7 +4459,7 @@ namespace Microsoft.PowerShell.Commands
{
try
{
string fixedFileName = FixupFileName(moduleBase, s, extension, importingModule, skipLoading: true);
string fixedFileName = FixFileNameWithoutLoadingAssembly(moduleBase, s, extension);
var dir = Path.GetDirectoryName(fixedFileName);
if (string.Equals(psHome, dir, StringComparison.OrdinalIgnoreCase) ||
@@ -4581,12 +4614,22 @@ namespace Microsoft.PowerShell.Commands
}
}
private string FixFileNameWithoutLoadingAssembly(string moduleBase, string fileName, string extension)
{
return FixFileName(moduleName: null, moduleBase, fileName, extension, canLoadAssembly: false, pathIsResolved: out _);
}
private string FixFileNameWithoutLoadingAssembly(string moduleBase, string fileName, string extension, out bool pathIsResolved)
{
return FixFileName(moduleName: null, moduleBase, fileName, extension, canLoadAssembly: false, out pathIsResolved);
}
/// <summary>
/// A utility routine to fix up a file name so it's rooted and has an extension.
/// </summary>
internal string FixupFileName(string moduleBase, string name, string extension, bool isImportingModule, bool skipLoading = false)
private string FixFileName(string moduleName, string moduleBase, string fileName, string extension, bool canLoadAssembly)
{
return FixupFileName(moduleBase, name, extension, isImportingModule, pathIsResolved: out _, skipLoading);
return FixFileName(moduleName, moduleBase, fileName, extension, canLoadAssembly, pathIsResolved: out _);
}
/// <summary>
@@ -4596,24 +4639,33 @@ namespace Microsoft.PowerShell.Commands
/// When fixing up an assembly file, this method loads the resovled assembly if it's in the process of actually loading a module.
/// Read the comments in the method for the detailed information.
/// </remarks>
/// <param name="moduleName">Name of the module that we are processing, used for caching purpose when we need to load an assembly.</param>
/// <param name="moduleBase">The base path to use if the file is not rooted.</param>
/// <param name="name">The file name to resolve.</param>
/// <param name="extension">The extension to use in case the given name has no extension.</param>
/// <param name="isImportingModule">Indicate if we are loading a module.</param>
/// <param name="fileName">The file name to resolve.</param>
/// <param name="extension">The extension to use for the look up.</param>
/// <param name="canLoadAssembly">Indicate if we can load assembly for the resolution.</param>
/// <param name="pathIsResolved">Indicate if the returned path is fully resolved.</param>
/// <param name="skipLoading">Indicate if the resolved module should be loaded.</param>
/// <returns>
/// The resolved file path. Or, the combined path of <paramref name="moduleBase"/> and <paramref name="name"/> when the file path cannot be resolved.
/// The resolved file path. Or, the combined path of <paramref name="moduleBase"/> and <paramref name="fileName"/> when the file path cannot be resolved.
/// </returns>
internal string FixupFileName(string moduleBase, string name, string extension, bool isImportingModule, out bool pathIsResolved, bool skipLoading = false)
private string FixFileName(string moduleName, string moduleBase, string fileName, string extension, bool canLoadAssembly, out bool pathIsResolved)
{
pathIsResolved = false;
string originalName = name;
string originalExt = Path.GetExtension(name);
if (string.IsNullOrEmpty(originalExt))
string originalName = fileName;
string originalExt = Path.GetExtension(fileName);
if (string.IsNullOrEmpty(extension))
{
name += extension;
// When 'extension' is not explicitly specified, we honor the original extension.
extension = originalExt;
}
else if (!extension.Equals(originalExt, StringComparison.OrdinalIgnoreCase))
{
// When 'extension' is explicitly specified, append it if the original extension is different.
// Note: the original extension could actually be part of the file name. For example, the name
// is `Microsoft.PowerShell.Command.Utility`, in which case the extension is `.Utility`.
fileName += extension;
}
// Try to get the resolved fully qualified path to the file.
@@ -4625,24 +4677,24 @@ namespace Microsoft.PowerShell.Commands
// Check for combinedPath in this case will get us the normalized rooted path 'C:\Windows\System32\WindowsPowerShell\v1.0\WSMan.format.ps1xml'.
// The 'Microsoft.WSMan.Management' module in PowerShell was updated to not use the relative path for 'FormatsToProcess' entry,
// but it's safer to keep the original behavior to avoid unexpected breaking changes.
string combinedPath = Path.Combine(moduleBase, name);
string resolvedPath = IsRooted(name)
? ResolveRootedFilePath(name, Context) ?? ResolveRootedFilePath(combinedPath, Context)
string combinedPath = Path.Combine(moduleBase, fileName);
string resolvedPath = IsRooted(fileName)
? ResolveRootedFilePath(fileName, Context) ?? ResolveRootedFilePath(combinedPath, Context)
: ResolveRootedFilePath(combinedPath, Context);
// Return the path if successfully resolved.
if (resolvedPath != null)
if (resolvedPath is not null)
{
if (isImportingModule && resolvedPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && !skipLoading)
if (canLoadAssembly && resolvedPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
{
// If we are fixing up an assembly file path and we are actually loading the module, then we load the resolved assembly file here.
// This is because we process type/format ps1xml files before 'RootModule' and 'NestedModules' entries during the module loading.
// A types.ps1xml file could refer to a type defined in the assembly that is specified in the 'RootModule' or 'NestedModule', and
// in that case, processing the types.ps1xml file would fail because it happens before processing the 'RootModule', which loads
// the assembly. We cannot move the processing of types.ps1xml file after processing 'RootModule' either, because the 'RootModule'
// might refer to members defined in the types.ps1xml file. In order to make it work for this paradox, we have to load the resolved
// assembly when we are actually loading the module. However, when it's module analysis, there is no need to load the assembly.
ExecutionContext.LoadAssembly(name: null, filename: resolvedPath, error: out _);
// This is because we process type/format ps1xml files before 'RootModule' during the module loading. A types.ps1xml file could
// refer to a type defined in the assembly that is specified in the 'RootModule', and in that case, processing the types.ps1xml file
// would fail because it happens before processing the 'RootModule', which loads the assembly.
// We cannot move the processing of types.ps1xml file after processing 'RootModule' either, because the 'RootModule' might refer to
// members defined in the types.ps1xml file. In order to make it work for this paradox, we have to load the resolved assembly when
// we are actually loading the module. However, when it's module analysis, there is no need to load the assembly.
Context.AddAssembly(source: moduleName, assemblyName: null, filePath: resolvedPath, error: out _);
}
pathIsResolved = true;
@@ -4655,12 +4707,12 @@ namespace Microsoft.PowerShell.Commands
// For dlls, we cannot get the path from the provider.
// We need to load the assembly and then get the path.
// If the module is already loaded, this is not expensive since the assembly is already loaded in the AppDomain
if (!string.IsNullOrEmpty(extension) &&
if (canLoadAssembly && !string.IsNullOrEmpty(extension) &&
(extension.Equals(StringLiterals.PowerShellILAssemblyExtension, StringComparison.OrdinalIgnoreCase) ||
extension.Equals(StringLiterals.PowerShellILExecutableExtension, StringComparison.OrdinalIgnoreCase)))
extension.Equals(StringLiterals.PowerShellILExecutableExtension, StringComparison.OrdinalIgnoreCase)))
{
Assembly assembly = ExecutionContext.LoadAssembly(name: originalName, filename: null, error: out _);
if (assembly != null)
Assembly assembly = Context.AddAssembly(source: moduleName, assemblyName: originalName, filePath: null, error: out _);
if (assembly is not null)
{
pathIsResolved = true;
result = assembly.Location;
@@ -4974,8 +5026,6 @@ namespace Microsoft.PowerShell.Commands
/// <param name="moduleNameInRemoveModuleCmdlet">Module name specified in the cmdlet.</param>
internal void RemoveModule(PSModuleInfo module, string moduleNameInRemoveModuleCmdlet)
{
bool isTopLevelModule = false;
// if the module path is empty string, means it is a dynamically generated assembly.
// We have set the module path to be module name as key to make it unique, we need update here as well in case the module can be removed.
if (module.Path == string.Empty)
@@ -4983,7 +5033,7 @@ namespace Microsoft.PowerShell.Commands
module.Path = module.Name;
}
bool shouldModuleBeRemoved = ShouldModuleBeRemoved(module, moduleNameInRemoveModuleCmdlet, out isTopLevelModule);
bool shouldModuleBeRemoved = ShouldModuleBeRemoved(module, moduleNameInRemoveModuleCmdlet, out bool isTopLevelModule);
if (shouldModuleBeRemoved)
{
@@ -5214,6 +5264,15 @@ namespace Microsoft.PowerShell.Commands
// And the appdomain level module path cache.
PSModuleInfo.RemoveFromAppDomainLevelCache(module.Name);
// And remove the module assembly entries that may have been added from the assembly cache.
Context.RemoveFromAssemblyCache(source: module.Name);
if (module.ModuleType == ModuleType.Binary && !string.IsNullOrEmpty(module.RootModule))
{
// We also need to clean up the cache entries that are possibly referenced by the root module in this case.
string rootModuleName = ModuleIntrinsics.GetModuleName(module.RootModule);
Context.RemoveFromAssemblyCache(source: rootModuleName);
}
}
}
}
@@ -5903,6 +5962,7 @@ namespace Microsoft.PowerShell.Commands
ext.Equals(StringLiterals.PowerShellILExecutableExtension, StringComparison.OrdinalIgnoreCase))
{
module = LoadBinaryModule(
parentModule,
ModuleIntrinsics.GetModuleName(fileName),
fileName,
assemblyToLoad: null,
@@ -5911,8 +5971,6 @@ namespace Microsoft.PowerShell.Commands
options,
manifestProcessingFlags,
prefix,
loadTypes: true,
loadFormats: true,
out found);
if (found && module != null)
@@ -6384,6 +6442,7 @@ namespace Microsoft.PowerShell.Commands
/// <summary>
/// Load a binary module. A binary module is an assembly that should contain cmdlets.
/// </summary>
/// <param name="parentModule">The parent module for which this module is a nested module.</param>
/// <param name="moduleName">The name of the snapin or assembly to load.</param>
/// <param name="fileName">The path to the assembly to load.</param>
/// <param name="assemblyToLoad">The assembly to load so no lookup need be done.</param>
@@ -6395,12 +6454,11 @@ namespace Microsoft.PowerShell.Commands
/// </param>
/// <param name="options">The set of options that are used while importing a module.</param>
/// <param name="manifestProcessingFlags">The manifest processing flags to use when processing the module.</param>
/// <param name="loadTypes">Load the types files mentioned in the snapin registration.</param>
/// <param name="loadFormats">Load the formst files mentioned in the snapin registration.</param>
/// <param name="prefix">Command name prefix.</param>
/// <param name="found">Sets this to true if an assembly was found.</param>
/// <returns>THe module info object that was created...</returns>
internal PSModuleInfo LoadBinaryModule(
PSModuleInfo parentModule,
string moduleName,
string fileName,
Assembly assemblyToLoad,
@@ -6409,12 +6467,10 @@ namespace Microsoft.PowerShell.Commands
ImportModuleOptions options,
ManifestProcessingFlags manifestProcessingFlags,
string prefix,
bool loadTypes,
bool loadFormats,
out bool found)
{
return LoadBinaryModule(
parentModule: null,
parentModule,
moduleName,
fileName,
assemblyToLoad,
@@ -6423,8 +6479,6 @@ namespace Microsoft.PowerShell.Commands
options,
manifestProcessingFlags,
prefix,
loadTypes,
loadFormats,
out found,
shortModuleName: null,
disableFormatUpdates: false);
@@ -6446,8 +6500,6 @@ namespace Microsoft.PowerShell.Commands
/// <param name="options">The set of options that are used while importing a module.</param>
/// <param name="manifestProcessingFlags">The manifest processing flags to use when processing the module.</param>
/// <param name="prefix">Command name prefix.</param>
/// <param name="loadTypes">Load the types files mentioned in the snapin registration.</param>
/// <param name="loadFormats">Load the formst files mentioned in the snapin registration.</param>
/// <param name="found">Sets this to true if an assembly was found.</param>
/// <param name="shortModuleName">Short name for module.</param>
/// <param name="disableFormatUpdates"></param>
@@ -6462,44 +6514,36 @@ namespace Microsoft.PowerShell.Commands
ImportModuleOptions options,
ManifestProcessingFlags manifestProcessingFlags,
string prefix,
bool loadTypes,
bool loadFormats,
out bool found,
string shortModuleName,
bool disableFormatUpdates)
{
PSModuleInfo module = null;
if (string.IsNullOrEmpty(moduleName) && string.IsNullOrEmpty(fileName) && assemblyToLoad == null)
{
throw PSTraceSource.NewArgumentNullException("moduleName,fileName,assemblyToLoad");
}
bool isParentEngineModule = parentModule != null && InitialSessionState.IsEngineModule(parentModule.Name);
// Load the dll and process any cmdlets it might contain...
InitialSessionState iss = InitialSessionState.Create();
List<string> detectedCmdlets = null;
List<Tuple<string, string>> detectedAliases = null;
Assembly assembly = null;
Exception error = null;
string modulePath = string.Empty;
Version assemblyVersion = new Version(0, 0, 0, 0);
var importingModule = (manifestProcessingFlags & ManifestProcessingFlags.LoadElements) != 0;
bool importingModule = (manifestProcessingFlags & ManifestProcessingFlags.LoadElements) != 0;
// See if we're loading a straight assembly...
if (assemblyToLoad != null)
{
// Figure out what to use for a module path...
if (!string.IsNullOrEmpty(fileName))
{
modulePath = fileName;
}
else
{
modulePath = assemblyToLoad.Location;
}
modulePath = string.IsNullOrEmpty(fileName) ? assemblyToLoad.Location : fileName;
// And what to use for a module name...
if (string.IsNullOrEmpty(moduleName))
{
moduleName = "dynamic_code_module_" + assemblyToLoad.GetName();
moduleName = "dynamic_code_module_" + assemblyToLoad.FullName;
}
if (importingModule)
@@ -6507,59 +6551,41 @@ namespace Microsoft.PowerShell.Commands
// Passing module as a parameter here so that the providers can have the module property populated.
// For engine providers, the module should point to top-level module name
// For FileSystem, the module is Microsoft.PowerShell.Core and not System.Management.Automation
if (parentModule != null && InitialSessionState.IsEngineModule(parentModule.Name))
{
iss.ImportCmdletsFromAssembly(assemblyToLoad, parentModule);
}
else
{
iss.ImportCmdletsFromAssembly(assemblyToLoad, null);
}
iss.ImportCmdletsFromAssembly(assemblyToLoad, isParentEngineModule ? parentModule : null);
}
assemblyVersion = GetAssemblyVersionNumber(assemblyToLoad);
assembly = assemblyToLoad;
// If this is an in-memory only assembly, add it directly to the assembly cache if
// it isn't already there.
if (string.IsNullOrEmpty(assembly.Location))
{
if (!Context.AssemblyCache.ContainsKey(assembly.FullName))
{
Context.AssemblyCache.Add(assembly.FullName, assembly);
}
}
// Use the parent module name for caching if there is one.
string source = parentModule?.Name ?? moduleName;
// Add it to the assembly cache if it isn't already there.
Context.AddToAssemblyCache(source, assembly);
}
else if (importingModule)
{
assembly = Context.AddAssembly(moduleName, fileName, out error);
// Use the parent module name for caching if there is one.
string source = parentModule?.Name ?? moduleName;
assembly = Context.AddAssembly(source, moduleName, fileName, out Exception error);
if (assembly == null)
{
if (error != null)
{
throw error;
}
found = false;
return null;
}
assemblyVersion = GetAssemblyVersionNumber(assembly);
if (string.IsNullOrEmpty(fileName))
modulePath = assembly.Location;
else
modulePath = fileName;
modulePath = string.IsNullOrEmpty(fileName) ? assembly.Location : fileName;
// Passing module as a parameter here so that the providers can have the module property populated.
// For engine providers, the module should point to top-level module name
// For FileSystem, the module is Microsoft.PowerShell.Core and not System.Management.Automation
if (parentModule != null && InitialSessionState.IsEngineModule(parentModule.Name))
{
iss.ImportCmdletsFromAssembly(assembly, parentModule);
}
else
{
iss.ImportCmdletsFromAssembly(assembly, null);
}
iss.ImportCmdletsFromAssembly(assembly, isParentEngineModule ? parentModule : null);
}
else
{
@@ -6576,58 +6602,25 @@ namespace Microsoft.PowerShell.Commands
}
found = true;
if (string.IsNullOrEmpty(shortModuleName))
module = new PSModuleInfo(moduleName, modulePath, Context, ss);
else
module = new PSModuleInfo(shortModuleName, modulePath, Context, ss);
string nameToUse = string.IsNullOrEmpty(shortModuleName) ? moduleName : shortModuleName;
PSModuleInfo module = new PSModuleInfo(nameToUse, modulePath, Context, ss);
module.SetModuleType(ModuleType.Binary);
module.SetModuleBase(moduleBase);
module.SetVersion(assemblyVersion);
module.ImplementingAssembly = assemblyToLoad ?? assembly;
module.ImplementingAssembly = assembly;
if (importingModule)
{
SetModuleLoggingInformation(module);
}
// Add the types table entries
List<string> typesFileNames = new List<string>();
foreach (SessionStateTypeEntry sste in iss.Types)
{
typesFileNames.Add(sste.FileName);
}
if (typesFileNames.Count > 0)
{
module.SetExportedTypeFiles(new ReadOnlyCollection<string>(typesFileNames));
}
// Add the format file entries
List<string> formatsFileNames = new List<string>();
foreach (SessionStateFormatEntry ssfe in iss.Formats)
{
formatsFileNames.Add(ssfe.FileName);
}
if (formatsFileNames.Count > 0)
{
module.SetExportedFormatFiles(new ReadOnlyCollection<string>(formatsFileNames));
}
// Add the module info the providers...
foreach (SessionStateProviderEntry sspe in iss.Providers)
{
// For engine providers, the module should point to top-level module name
// For FileSystem, the module is Microsoft.PowerShell.Core and not System.Management.Automation
if (parentModule != null && InitialSessionState.IsEngineModule(parentModule.Name))
{
sspe.SetModule(parentModule);
}
else
{
sspe.SetModule(module);
}
sspe.SetModule(isParentEngineModule ? parentModule : module);
}
// Add all of the exported cmdlets to the module object...
@@ -6741,16 +6734,7 @@ namespace Microsoft.PowerShell.Commands
iss.Bind(Context, updateOnly: true, module, options.NoClobber, options.Local, setLocation: false);
// Scan all of the types in the assembly to register JobSourceAdapters.
IEnumerable<Type> allTypes = Array.Empty<Type>();
if (assembly != null)
{
allTypes = assembly.ExportedTypes;
}
else if (assemblyToLoad != null)
{
allTypes = assemblyToLoad.ExportedTypes;
}
IEnumerable<Type> allTypes = assembly?.ExportedTypes ?? Array.Empty<Type>();
foreach (Type type in allTypes)
{
// If it derives from JobSourceAdapter and it's not already registered, register it...
@@ -885,25 +885,35 @@ namespace System.Management.Automation
}
// The extensions of all of the files that can be processed with Import-Module, put the ni.dll in front of .dll to have higher priority to be loaded.
internal static readonly string[] PSModuleProcessableExtensions = new string[] {
StringLiterals.PowerShellDataFileExtension,
StringLiterals.PowerShellScriptFileExtension,
StringLiterals.PowerShellModuleFileExtension,
StringLiterals.PowerShellCmdletizationFileExtension,
StringLiterals.PowerShellNgenAssemblyExtension,
StringLiterals.PowerShellILAssemblyExtension,
StringLiterals.PowerShellILExecutableExtension,
};
internal static readonly string[] PSModuleProcessableExtensions = new string[]
{
StringLiterals.PowerShellDataFileExtension,
StringLiterals.PowerShellScriptFileExtension,
StringLiterals.PowerShellModuleFileExtension,
StringLiterals.PowerShellCmdletizationFileExtension,
StringLiterals.PowerShellNgenAssemblyExtension,
StringLiterals.PowerShellILAssemblyExtension,
StringLiterals.PowerShellILExecutableExtension,
};
// A list of the extensions to check for implicit module loading and discovery, put the ni.dll in front of .dll to have higher priority to be loaded.
internal static readonly string[] PSModuleExtensions = new string[] {
StringLiterals.PowerShellDataFileExtension,
StringLiterals.PowerShellModuleFileExtension,
StringLiterals.PowerShellCmdletizationFileExtension,
StringLiterals.PowerShellNgenAssemblyExtension,
StringLiterals.PowerShellILAssemblyExtension,
StringLiterals.PowerShellILExecutableExtension,
};
internal static readonly string[] PSModuleExtensions = new string[]
{
StringLiterals.PowerShellDataFileExtension,
StringLiterals.PowerShellModuleFileExtension,
StringLiterals.PowerShellCmdletizationFileExtension,
StringLiterals.PowerShellNgenAssemblyExtension,
StringLiterals.PowerShellILAssemblyExtension,
StringLiterals.PowerShellILExecutableExtension,
};
// A list of the extensions to check for required assemblies.
internal static readonly string[] ProcessableAssemblyExtensions = new string[]
{
StringLiterals.PowerShellNgenAssemblyExtension,
StringLiterals.PowerShellILAssemblyExtension,
StringLiterals.PowerShellILExecutableExtension
};
/// <summary>
/// Returns true if the extension is one of the module extensions...
@@ -915,7 +925,9 @@ namespace System.Management.Automation
foreach (string ext in PSModuleProcessableExtensions)
{
if (extension.Equals(ext, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Describe "Isolated module scenario - load the whole module in custom ALC" {
It "Loading 'IsolatedModule' should work as expected" {
## The 'IsolatedModule' module can be found at '<repo-root>\test\tools\Modules'.
## The module assemblies are created and deployed by '<repo-root>\test\tools\TestAlc'.
## The module defines its own custom ALC and has its module structure organized in a special way that allows the module to be loaded in that custom ALC.
## The file structure of this module is as follows:
## │ IsolatedModule.psd1
## │ Test.Isolated.Init.dll (contains the custom ALC and code to setup 'Resolving' handler)
## │
## └───Dependencies
## Newtonsoft.Json.dll (version 10.0.0.0 dependency)
## Test.Isolated.Nested.dll (nested binary module)
## Test.Isolated.Root.dll (root binary module)
$module = Import-Module IsolatedModule -PassThru
$nestedCmd = Get-Command Test-NestedCommand
$rootCmd = Get-Command Test-RootCommand
$module.ModuleType | Should -Be "Binary"
$module.RootModule | Should -Not -BeNullOrEmpty
## The type 'Test.Isolated.Nested.Foo' from the nested module can be resolved and should be from the same load context.
$context1 = [System.Runtime.Loader.AssemblyLoadContext]::GetLoadContext($nestedCmd.ImplementingType.Assembly)
$context2 = [System.Runtime.Loader.AssemblyLoadContext]::GetLoadContext([Test.Isolated.Nested.Foo].Assembly)
$context1.Name | Should -BeExactly "MyCustomALC"
$context1 | Should -Be $context2
## Test-NestedCommand depends on NewtonSoft.Json 10.0.0.0 while PowerShell depends on 13.0.0.0 or higher.
## The exact version of NewtonSoft.Json should be loaded to the custom ALC.
$foo = [Test.Isolated.Nested.Foo]::new("Hello", "World")
Test-NestedCommand -Param $foo | Should -BeExactly "Hello-World-Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed"
## The type 'Test.Isolated.Root.Red' from the root module can be resolved and should be from the same load context.
$context3 = [System.Runtime.Loader.AssemblyLoadContext]::GetLoadContext($rootCmd.ImplementingType.Assembly)
$context4 = [System.Runtime.Loader.AssemblyLoadContext]::GetLoadContext([Test.Isolated.Root.Red].Assembly)
$context3.Name | Should -BeExactly "MyCustomALC"
$context3 | Should -Be $context4
$context3 | Should -Be $context1
## No type identity issue in parameter binding because they are from the same assembly instance.
$red = [Test.Isolated.Root.Red]::new("RED!")
Test-RootCommand -Param $red | Should -BeExactly "RED!"
## Removing the module should have its assemblies removed from 'Context.AssemblyCache' and results in the 'TypeNotFound'
## error when trying to resolve the following 2 types:
## - 'Test.Isolated.Nested.Bar', from 'Test.Isolated.Nested.dll'
## - 'Test.Isolated.Root.Yellow', from 'Test.Isolated.Root.dll'
## The types cannot be found because:
## 1. they are from a load context that is not visible to PowerShell,
## 2. those two assemblies have been removed from the cache when unloading the module.
## [Test.Isolated.Nested.Foo] and [Test.Isolated.Root.Red] can still be found because they were added to type cache when
## successfully resolved above.
Remove-Module IsolatedModule
{ [Test.Isolated.Nested.Bar] } | Should -Throw -ErrorId "TypeNotFound"
{ [Test.Isolated.Root.Yellow] } | Should -Throw -ErrorId "TypeNotFound"
}
It "WSMan and Certificate providers should reference the manifest module instead of the nested module" -Skip:(!$IsWindows) {
$wsManModule = Import-Module Microsoft.WSMan.Management -PassThru
$securityModule = Import-Module Microsoft.PowerShell.Security -PassThru
$wsManModule.ModuleType | Should -Be "Manifest"
$securityModule.ModuleType | Should -Be "Manifest"
## For engine providers, the 'Module' property should point to top-level module, instead of the nested module.
$wsManProvider = Get-PSProvider WSMan
$certificateProvider = Get-PSProvider Certificate
$wsManProvider.Module | Should -Be $wsManModule
$certificateProvider.Module | Should -Be $securityModule
}
}
@@ -18,12 +18,21 @@ Describe "Test-ModuleManifest tests" -tags "CI" {
New-Item -ItemType Directory -Path testdrive:/module/foo > $null
New-Item -ItemType Directory -Path testdrive:/module/bar > $null
New-Item -ItemType File -Path testdrive:/module/foo/bar.psm1 > $null
New-Item -ItemType File -Path testdrive:/module/foo/bar.ps1 > $null
New-Item -ItemType File -Path testdrive:/module/foo/bar.ps1xml > $null
New-Item -ItemType File -Path testdrive:/module/bar/foo.psm1 > $null
New-Item -ItemType File -Path testdrive:/module/bar/foo.ps1 > $null
New-Item -ItemType File -Path testdrive:/module/bar/foo.ps1xml > $null
$testModulePath = "testdrive:/module/test.psd1"
$fileList = "foo\bar.psm1","bar/foo.psm1"
$scripts = "foo\/bar.ps1","bar/\foo.ps1"
$ps1xml = "foo//bar.ps1xml","bar\\foo.ps1xml"
New-ModuleManifest -NestedModules $fileList -RootModule foo\bar.psm1 -RequiredAssemblies $fileList -Path $testModulePath -TypesToProcess $fileList -FormatsToProcess $fileList -ScriptsToProcess $fileList -FileList $fileList -ModuleList $fileList
New-ModuleManifest -NestedModules $fileList -RootModule foo\bar.psm1 -RequiredAssemblies $fileList -Path $testModulePath -TypesToProcess $ps1xml -FormatsToProcess $ps1xml -ScriptsToProcess $scripts -FileList $fileList -ModuleList $fileList
Test-Path $testModulePath | Should -BeTrue
@@ -0,0 +1,16 @@
#
# Module manifest for module 'IsolatedModule'
#
@{
ModuleVersion = '0.0.1'
GUID = '20d4742b-b17d-4ce8-b8da-29b25433cd18'
Author = 'Microsoft Corporation'
RootModule = 'Test.Isolated.Root.dll'
NestedModules = @('Test.Isolated.Init.dll', 'Test.Isolated.Nested.dll')
FunctionsToExport = @()
CmdletsToExport = @('Test-NestedCommand', 'Test-RootCommand')
VariablesToExport = '*'
AliasesToExport = @()
}
+34
View File
@@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.Isolated.Init", "init\Test.Isolated.Init.csproj", "{87AFE044-D21A-4ECE-A8F7-9A31678811D9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.Isolated.Nested", "nested\Test.Isolated.Nested.csproj", "{702DAF12-2A79-4756-8E2A-E18B46639AD3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.Isolated.Root", "root\Test.Isolated.Root.csproj", "{9D0B1FAD-52F7-436A-AB9D-5A53E370C72B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{87AFE044-D21A-4ECE-A8F7-9A31678811D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87AFE044-D21A-4ECE-A8F7-9A31678811D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87AFE044-D21A-4ECE-A8F7-9A31678811D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87AFE044-D21A-4ECE-A8F7-9A31678811D9}.Release|Any CPU.Build.0 = Release|Any CPU
{702DAF12-2A79-4756-8E2A-E18B46639AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{702DAF12-2A79-4756-8E2A-E18B46639AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{702DAF12-2A79-4756-8E2A-E18B46639AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{702DAF12-2A79-4756-8E2A-E18B46639AD3}.Release|Any CPU.Build.0 = Release|Any CPU
{9D0B1FAD-52F7-436A-AB9D-5A53E370C72B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9D0B1FAD-52F7-436A-AB9D-5A53E370C72B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9D0B1FAD-52F7-436A-AB9D-5A53E370C72B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9D0B1FAD-52F7-436A-AB9D-5A53E370C72B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+83
View File
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using System.Reflection;
using System.Runtime.Loader;
namespace Test.Isolated.Init
{
internal class CustomLoadContext : AssemblyLoadContext
{
private readonly string _dependencyDirPath;
public CustomLoadContext(string dependencyDirPath)
: base("MyCustomALC", isCollectible: false)
{
_dependencyDirPath = dependencyDirPath;
}
protected override Assembly Load(AssemblyName assemblyName)
{
// We do the simple logic here of looking for an assembly of the given name
// in the configured dependency directory.
string assemblyPath = Path.Combine(_dependencyDirPath, $"{assemblyName.Name}.dll");
if (File.Exists(assemblyPath))
{
// The ALC must use inherited methods to load assemblies.
// Assembly.Load*() won't work here.
return LoadFromAssemblyPath(assemblyPath);
}
// For other assemblies, return null to allow other resolutions to continue.
return null;
}
}
public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
{
private static readonly CustomLoadContext s_context;
private static readonly HashSet<string> s_moduleAssemblies;
static Init()
{
string dependencyDirPath = Path.Combine(Path.GetDirectoryName(typeof(Init).Assembly.Location), "Dependencies");
s_context = new CustomLoadContext(dependencyDirPath);
s_moduleAssemblies = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Test.Isolated.Nested",
"Test.Isolated.Root"
};
}
public void OnImport()
{
// Add the Resolving event handler here.
AssemblyLoadContext.Default.Resolving += ResolveAlcEngine;
}
public void OnRemove(PSModuleInfo psModuleInfo)
{
// Remove the Resolving event handler here.
AssemblyLoadContext.Default.Resolving -= ResolveAlcEngine;
}
private static Assembly ResolveAlcEngine(AssemblyLoadContext defaultAlc, AssemblyName assemblyToResolve)
{
// We only want to resolve our module assemblies here.
if (s_moduleAssemblies.Contains(assemblyToResolve.Name))
{
// This is where the nested module 'Test.Isolated.Nested.dll' and the root module 'Test.Isolated.Root.dll'
// gets loaded into our custom ALC and then passed through into PowerShell's ALC.
return s_context.LoadFromAssemblyName(assemblyToResolve);
}
// Let the resolution chain continue for other assemblies.
return null;
}
}
}
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Test.Common.props"/>
<PropertyGroup>
<!-- Disable PDB generation -->
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<!-- Disable deps.json generation -->
<GenerateDependencyFile>false</GenerateDependencyFile>
<!-- Deploy the produced assembly -->
<PublishDir>..\..\Modules\IsolatedModule</PublishDir>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PowerShellStandard.Library" Version="5.1.0" PrivateAssets="All" />
</ItemGroup>
</Project>
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Management.Automation;
using Newtonsoft.Json;
namespace Test.Isolated.Nested
{
[Cmdlet("Test", "NestedCommand")]
public class TestNestedCommand : PSCmdlet
{
[Parameter(Mandatory = true)]
public Foo Param { get; set; }
protected override void ProcessRecord()
{
WriteObject($"{Param.Name}-{Param.Path}-{typeof(StringEscapeHandling).Assembly.FullName}");
}
}
public class Foo
{
public string Name { get; }
public string Path { get; }
public Foo(string name, string path)
{
Name = name;
Path = path;
}
}
public class Bar
{
public string Id { get; }
public Bar(string id)
{
Id = id;
}
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Test.Common.props"/>
<PropertyGroup>
<!-- Disable PDB generation -->
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<!-- Disable deps.json generation -->
<GenerateDependencyFile>false</GenerateDependencyFile>
<!-- Deploy the produced assembly -->
<PublishDir>..\..\Modules\IsolatedModule\Dependencies</PublishDir>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PowerShellStandard.Library" Version="5.1.0" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.1" />
</ItemGroup>
</Project>
+39
View File
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Management.Automation;
namespace Test.Isolated.Root
{
[Cmdlet("Test", "RootCommand")]
public class TestRootCommand : PSCmdlet
{
[Parameter(Mandatory = true)]
public Red Param { get; set; }
protected override void ProcessRecord()
{
WriteObject(Param.Name);
}
}
public class Red
{
public string Name { get; }
public Red(string name)
{
Name = name;
}
}
public class Yellow
{
public string Id { get; }
public Yellow(string id)
{
Id = id;
}
}
}
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Test.Common.props"/>
<PropertyGroup>
<!-- Disable PDB generation -->
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
<!-- Disable deps.json generation -->
<GenerateDependencyFile>false</GenerateDependencyFile>
<!-- Deploy the produced assembly -->
<PublishDir>..\..\Modules\IsolatedModule\Dependencies</PublishDir>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PowerShellStandard.Library" Version="5.1.0" PrivateAssets="All" />
</ItemGroup>
</Project>