mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Refactor module assembly loading logic to allow the whole module to be loaded in a custom ALC (#16889)
This commit is contained in:
+15
-7
@@ -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 = @()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user