mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
This round of cleanup focuses on the following areas - Tab completion code - InitialSessionState - Compiler/MiscOps - Utils/SessionStateFunctionAPIs Changes in the rest affected files are mainly some corresponding changes due to the cleanup work in the above areas.
1344 lines
66 KiB
C#
1344 lines
66 KiB
C#
/********************************************************************++
|
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
--********************************************************************/
|
|
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Management.Automation.Internal;
|
|
using System.Management.Automation.Language;
|
|
using Microsoft.PowerShell.Commands;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using Dbg = System.Management.Automation.Diagnostics;
|
|
|
|
namespace System.Management.Automation
|
|
{
|
|
internal static class Constants
|
|
{
|
|
public const string PSModulePathEnvVar = "PSModulePath";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encapsulates the basic module operations for a PowerShell engine instance...
|
|
/// </summary>
|
|
public class ModuleIntrinsics
|
|
{
|
|
/// <summary>
|
|
/// Tracer for module analysis
|
|
/// </summary>
|
|
[TraceSource("Modules", "Module loading and analysis")]
|
|
internal static PSTraceSource Tracer = PSTraceSource.GetTracer("Modules", "Module loading and analysis");
|
|
|
|
internal ModuleIntrinsics(ExecutionContext context)
|
|
{
|
|
_context = context;
|
|
|
|
// And initialize the module path...
|
|
SetModulePath();
|
|
}
|
|
private readonly ExecutionContext _context;
|
|
|
|
// Holds the module collection...
|
|
internal Dictionary<string, PSModuleInfo> ModuleTable { get; } = new Dictionary<string, PSModuleInfo>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private const int MaxModuleNestingDepth = 10;
|
|
|
|
internal void IncrementModuleNestingDepth(PSCmdlet cmdlet, string path)
|
|
{
|
|
if (++ModuleNestingDepth > MaxModuleNestingDepth)
|
|
{
|
|
string message = StringUtil.Format(Modules.ModuleTooDeeplyNested, path, MaxModuleNestingDepth);
|
|
InvalidOperationException ioe = new InvalidOperationException(message);
|
|
ErrorRecord er = new ErrorRecord(ioe, "Modules_ModuleTooDeeplyNested",
|
|
ErrorCategory.InvalidOperation, path);
|
|
// NOTE: this call will throw
|
|
cmdlet.ThrowTerminatingError(er);
|
|
}
|
|
}
|
|
internal void DecrementModuleNestingCount()
|
|
{
|
|
--ModuleNestingDepth;
|
|
}
|
|
|
|
internal int ModuleNestingDepth { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Create a new module object from a scriptblock specifying the path to set for the module
|
|
/// </summary>
|
|
/// <param name="name">The name of the module</param>
|
|
/// <param name="path">The path where the module is rooted</param>
|
|
/// <param name="scriptBlock">
|
|
/// ScriptBlock that is executed to initialize the module...
|
|
/// </param>
|
|
/// <param name="arguments">
|
|
/// The arguments to pass to the scriptblock used to initialize the module
|
|
/// </param>
|
|
/// <param name="ss">The session state instance to use for this module - may be null</param>
|
|
/// <param name="results">The results produced from evaluating the scriptblock</param>
|
|
/// <returns>The newly created module info object</returns>
|
|
internal PSModuleInfo CreateModule(string name, string path, ScriptBlock scriptBlock, SessionState ss, out List<object> results, params object[] arguments)
|
|
{
|
|
return CreateModuleImplementation(name, path, scriptBlock, null, ss, null, out results, arguments);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new module object from a ScriptInfo object
|
|
/// </summary>
|
|
/// <param name="path">The path where the module is rooted</param>
|
|
/// <param name="scriptInfo">The script info to use to create the module</param>
|
|
/// <param name="scriptPosition">The position for the command that loaded this module</param>
|
|
/// <param name="arguments">Optional arguments to pass to the script while executing</param>
|
|
/// <param name="ss">The session state instance to use for this module - may be null</param>
|
|
/// <param name="privateData">The private data to use for this module - may be null</param>
|
|
/// <returns>The constructed module object</returns>
|
|
internal PSModuleInfo CreateModule(string path, ExternalScriptInfo scriptInfo, IScriptExtent scriptPosition, SessionState ss, object privateData, params object[] arguments)
|
|
{
|
|
List<object> result;
|
|
return CreateModuleImplementation(ModuleIntrinsics.GetModuleName(path), path, scriptInfo, scriptPosition, ss, privateData, out result, arguments);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new module object from code specifying the path to set for the module
|
|
/// </summary>
|
|
/// <param name="name">The name of the module</param>
|
|
/// <param name="path">The path to use for the module root</param>
|
|
/// <param name="moduleCode">
|
|
/// The code to use to create the module. This can be one of ScriptBlock, string
|
|
/// or ExternalScriptInfo
|
|
/// </param>
|
|
/// <param name="arguments">
|
|
/// Arguments to pass to the module scriptblock during evaluation.
|
|
/// </param>
|
|
/// <param name="result">
|
|
/// The results of the evaluation of the scriptblock.
|
|
/// </param>
|
|
/// <param name="scriptPosition">
|
|
/// The position of the caller of this function so you can tell where the call
|
|
/// to Import-Module (or whatever) occurred. This can be null.
|
|
/// </param>
|
|
/// <param name="ss">The session state instance to use for this module - may be null</param>
|
|
/// <param name="privateData">The private data to use for this module - may be null</param>
|
|
/// <returns>The created module</returns>
|
|
private PSModuleInfo CreateModuleImplementation(string name, string path, object moduleCode, IScriptExtent scriptPosition, SessionState ss, object privateData, out List<object> result, params object[] arguments)
|
|
{
|
|
ScriptBlock sb;
|
|
|
|
// By default the top-level scope in a session state object is the global scope for the instance.
|
|
// For modules, we need to set its global scope to be another scope object and, chain the top
|
|
// level scope for this sessionstate instance to be the parent. The top level scope for this ss is the
|
|
// script scope for the ss.
|
|
|
|
// Allocate the session state instance for this module.
|
|
if (ss == null)
|
|
{
|
|
ss = new SessionState(_context, true, true);
|
|
}
|
|
|
|
// Now set up the module's session state to be the current session state
|
|
SessionStateInternal oldSessionState = _context.EngineSessionState;
|
|
PSModuleInfo module = new PSModuleInfo(name, path, _context, ss);
|
|
ss.Internal.Module = module;
|
|
module.PrivateData = privateData;
|
|
|
|
bool setExitCode = false;
|
|
int exitCode = 0;
|
|
|
|
try
|
|
{
|
|
_context.EngineSessionState = ss.Internal;
|
|
|
|
// Build the scriptblock at this point so the references to the module
|
|
// context are correct...
|
|
ExternalScriptInfo scriptInfo = moduleCode as ExternalScriptInfo;
|
|
if (scriptInfo != null)
|
|
{
|
|
sb = scriptInfo.ScriptBlock;
|
|
|
|
_context.Debugger.RegisterScriptFile(scriptInfo);
|
|
}
|
|
else
|
|
{
|
|
sb = moduleCode as ScriptBlock;
|
|
if (sb != null)
|
|
{
|
|
PSLanguageMode? moduleLanguageMode = sb.LanguageMode;
|
|
sb = sb.Clone();
|
|
sb.LanguageMode = moduleLanguageMode;
|
|
|
|
sb.SessionState = ss;
|
|
}
|
|
else
|
|
{
|
|
var sbText = moduleCode as string;
|
|
if (sbText != null)
|
|
sb = ScriptBlock.Create(_context, sbText);
|
|
}
|
|
}
|
|
if (sb == null)
|
|
throw PSTraceSource.NewInvalidOperationException();
|
|
|
|
sb.SessionStateInternal = ss.Internal;
|
|
|
|
InvocationInfo invocationInfo = new InvocationInfo(scriptInfo, scriptPosition);
|
|
|
|
// Save the module string
|
|
module._definitionExtent = sb.Ast.Extent;
|
|
var ast = sb.Ast;
|
|
while (ast.Parent != null)
|
|
{
|
|
ast = ast.Parent;
|
|
}
|
|
|
|
// The variables set in the interpreted case get set by InvokeWithPipe in the compiled case.
|
|
Diagnostics.Assert(_context.SessionState.Internal.CurrentScope.LocalsTuple == null,
|
|
"No locals tuple should have been created yet.");
|
|
|
|
List<object> resultList = new List<object>();
|
|
|
|
try
|
|
{
|
|
Pipe outputPipe = new Pipe(resultList);
|
|
|
|
// And run the scriptblock...
|
|
sb.InvokeWithPipe(
|
|
useLocalScope: false,
|
|
errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToCurrentErrorPipe,
|
|
dollarUnder: AutomationNull.Value,
|
|
input: AutomationNull.Value,
|
|
scriptThis: AutomationNull.Value,
|
|
outputPipe: outputPipe,
|
|
invocationInfo: invocationInfo,
|
|
args: arguments ?? Utils.EmptyArray<object>());
|
|
}
|
|
catch (ExitException ee)
|
|
{
|
|
exitCode = (int)ee.Argument;
|
|
setExitCode = true;
|
|
}
|
|
result = resultList;
|
|
}
|
|
finally
|
|
{
|
|
_context.EngineSessionState = oldSessionState;
|
|
}
|
|
|
|
if (setExitCode)
|
|
{
|
|
_context.SetVariable(SpecialVariables.LastExitCodeVarPath, exitCode);
|
|
}
|
|
|
|
module.ImplementingAssembly = sb.AssemblyDefiningPSTypes;
|
|
// We force re-population of ExportedTypeDefinitions, now with the actual RuntimeTypes, created above.
|
|
module.CreateExportedTypeDefinitions(sb.Ast as ScriptBlockAst);
|
|
|
|
return module;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allocate a new dynamic module then return a new scriptblock
|
|
/// bound to the module instance.
|
|
/// </summary>
|
|
/// <param name="context">Context to use to create bounded script.</param>
|
|
/// <param name="sb">The scriptblock to bind</param>
|
|
/// <param name="linkToGlobal">Whether it should be linked to the global session state or not</param>
|
|
/// <returns>A new scriptblock</returns>
|
|
internal ScriptBlock CreateBoundScriptBlock(ExecutionContext context, ScriptBlock sb, bool linkToGlobal)
|
|
{
|
|
PSModuleInfo module = new PSModuleInfo(context, linkToGlobal);
|
|
return module.NewBoundScriptBlock(sb, context);
|
|
}
|
|
|
|
internal List<PSModuleInfo> GetModules(string[] patterns, bool all)
|
|
{
|
|
return GetModuleCore(patterns, all, false);
|
|
}
|
|
|
|
internal List<PSModuleInfo> GetExactMatchModules(string moduleName, bool all, bool exactMatch)
|
|
{
|
|
if (moduleName == null) { moduleName = String.Empty; }
|
|
return GetModuleCore(new string[] { moduleName }, all, exactMatch);
|
|
}
|
|
|
|
private List<PSModuleInfo> GetModuleCore(string[] patterns, bool all, bool exactMatch)
|
|
{
|
|
string targetModuleName = null;
|
|
List<WildcardPattern> wcpList = new List<WildcardPattern>();
|
|
|
|
if (exactMatch)
|
|
{
|
|
Dbg.Assert(patterns.Length == 1, "The 'patterns' should only contain one element when it is for an exact match");
|
|
targetModuleName = patterns[0];
|
|
}
|
|
else
|
|
{
|
|
if (patterns == null)
|
|
{
|
|
patterns = new string[] { "*" };
|
|
}
|
|
|
|
foreach (string pattern in patterns)
|
|
{
|
|
wcpList.Add(WildcardPattern.Get(pattern, WildcardOptions.IgnoreCase));
|
|
}
|
|
}
|
|
|
|
List<PSModuleInfo> modulesMatched = new List<PSModuleInfo>();
|
|
|
|
if (all)
|
|
{
|
|
foreach (PSModuleInfo module in ModuleTable.Values)
|
|
{
|
|
// See if this is the requested module...
|
|
if ((exactMatch && module.Name.Equals(targetModuleName, StringComparison.OrdinalIgnoreCase)) ||
|
|
(!exactMatch && SessionStateUtilities.MatchesAnyWildcardPattern(module.Name, wcpList, false)))
|
|
{
|
|
modulesMatched.Add(module);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Create a joint list of local and global modules. Only report a module once.
|
|
// Local modules are reported before global modules...
|
|
Dictionary<string, bool> found = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var pair in _context.EngineSessionState.ModuleTable)
|
|
{
|
|
string path = pair.Key;
|
|
PSModuleInfo module = pair.Value;
|
|
// See if this is the requested module...
|
|
if ((exactMatch && module.Name.Equals(targetModuleName, StringComparison.OrdinalIgnoreCase)) ||
|
|
(!exactMatch && SessionStateUtilities.MatchesAnyWildcardPattern(module.Name, wcpList, false)))
|
|
{
|
|
modulesMatched.Add(module);
|
|
found[path] = true;
|
|
}
|
|
}
|
|
if (_context.EngineSessionState != _context.TopLevelSessionState)
|
|
{
|
|
foreach (var pair in _context.TopLevelSessionState.ModuleTable)
|
|
{
|
|
string path = pair.Key;
|
|
if (!found.ContainsKey(path))
|
|
{
|
|
PSModuleInfo module = pair.Value;
|
|
// See if this is the requested module...
|
|
if ((exactMatch && module.Name.Equals(targetModuleName, StringComparison.OrdinalIgnoreCase)) ||
|
|
(!exactMatch && SessionStateUtilities.MatchesAnyWildcardPattern(module.Name, wcpList, false)))
|
|
{
|
|
modulesMatched.Add(module);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return modulesMatched.OrderBy(m => m.Name).ToList();
|
|
}
|
|
|
|
internal List<PSModuleInfo> GetModules(ModuleSpecification[] fullyQualifiedName, bool all)
|
|
{
|
|
List<PSModuleInfo> modulesMatched = new List<PSModuleInfo>();
|
|
|
|
if (all)
|
|
{
|
|
foreach (var moduleSpec in fullyQualifiedName)
|
|
{
|
|
foreach (PSModuleInfo module in ModuleTable.Values)
|
|
{
|
|
// See if this is the requested module...
|
|
if (IsModuleMatchingModuleSpec(module, moduleSpec))
|
|
{
|
|
modulesMatched.Add(module);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (var moduleSpec in fullyQualifiedName)
|
|
{
|
|
// Create a joint list of local and global modules. Only report a module once.
|
|
// Local modules are reported before global modules...
|
|
Dictionary<string, bool> found = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var pair in _context.EngineSessionState.ModuleTable)
|
|
{
|
|
string path = pair.Key;
|
|
PSModuleInfo module = pair.Value;
|
|
// See if this is the requested module...
|
|
if (IsModuleMatchingModuleSpec(module, moduleSpec))
|
|
{
|
|
modulesMatched.Add(module);
|
|
found[path] = true;
|
|
}
|
|
}
|
|
|
|
if (_context.EngineSessionState != _context.TopLevelSessionState)
|
|
{
|
|
foreach (var pair in _context.TopLevelSessionState.ModuleTable)
|
|
{
|
|
string path = pair.Key;
|
|
if (!found.ContainsKey(path))
|
|
{
|
|
PSModuleInfo module = pair.Value;
|
|
// See if this is the requested module...
|
|
if (IsModuleMatchingModuleSpec(module, moduleSpec))
|
|
{
|
|
modulesMatched.Add(module);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return modulesMatched.OrderBy(m => m.Name).ToList();
|
|
}
|
|
|
|
internal static bool IsModuleMatchingModuleSpec(PSModuleInfo moduleInfo, ModuleSpecification moduleSpec)
|
|
{
|
|
if (moduleInfo != null && moduleSpec != null &&
|
|
moduleInfo.Name.Equals(moduleSpec.Name, StringComparison.OrdinalIgnoreCase) &&
|
|
(!moduleSpec.Guid.HasValue || moduleSpec.Guid.Equals(moduleInfo.Guid)) &&
|
|
((moduleSpec.Version == null && moduleSpec.RequiredVersion == null && moduleSpec.MaximumVersion == null)
|
|
|| (moduleSpec.RequiredVersion != null && moduleSpec.RequiredVersion.Equals(moduleInfo.Version))
|
|
|| (moduleSpec.MaximumVersion == null && moduleSpec.Version != null && moduleSpec.RequiredVersion == null && moduleSpec.Version <= moduleInfo.Version)
|
|
|| (moduleSpec.MaximumVersion != null && moduleSpec.Version == null && moduleSpec.RequiredVersion == null && ModuleCmdletBase.GetMaximumVersion(moduleSpec.MaximumVersion) >= moduleInfo.Version)
|
|
|| (moduleSpec.MaximumVersion != null && moduleSpec.Version != null && moduleSpec.RequiredVersion == null && ModuleCmdletBase.GetMaximumVersion(moduleSpec.MaximumVersion) >= moduleInfo.Version && moduleSpec.Version <= moduleInfo.Version)))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
internal static Version GetManifestModuleVersion(string manifestPath)
|
|
{
|
|
if (manifestPath != null &&
|
|
manifestPath.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
try
|
|
{
|
|
var dataFileSetting =
|
|
PsUtils.GetModuleManifestProperties(
|
|
manifestPath,
|
|
PsUtils.ManifestModuleVersionPropertyName);
|
|
|
|
var versionValue = dataFileSetting["ModuleVersion"];
|
|
if (versionValue != null)
|
|
{
|
|
Version moduleVersion;
|
|
if (LanguagePrimitives.TryConvertTo(versionValue, out moduleVersion))
|
|
{
|
|
return moduleVersion;
|
|
}
|
|
}
|
|
}
|
|
catch (PSInvalidOperationException)
|
|
{
|
|
}
|
|
}
|
|
|
|
return new Version(0, 0);
|
|
}
|
|
|
|
internal static Guid GetManifestGuid(string manifestPath)
|
|
{
|
|
if (manifestPath != null &&
|
|
manifestPath.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
try
|
|
{
|
|
var dataFileSetting =
|
|
PsUtils.GetModuleManifestProperties(
|
|
manifestPath,
|
|
PsUtils.ManifestGuidPropertyName);
|
|
|
|
var guidValue = dataFileSetting["GUID"];
|
|
if (guidValue != null)
|
|
{
|
|
Guid guidID;
|
|
if (LanguagePrimitives.TryConvertTo(guidValue, out guidID))
|
|
{
|
|
return guidID;
|
|
}
|
|
}
|
|
}
|
|
catch (PSInvalidOperationException)
|
|
{
|
|
}
|
|
}
|
|
|
|
return new Guid();
|
|
}
|
|
|
|
// 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 string[] PSModuleProcessableExtensions = new string[] {
|
|
StringLiterals.PowerShellDataFileExtension,
|
|
StringLiterals.PowerShellScriptFileExtension,
|
|
StringLiterals.PowerShellModuleFileExtension,
|
|
StringLiterals.PowerShellCmdletizationFileExtension,
|
|
StringLiterals.WorkflowFileExtension,
|
|
StringLiterals.PowerShellNgenAssemblyExtension,
|
|
StringLiterals.PowerShellILAssemblyExtension};
|
|
|
|
// 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 string[] PSModuleExtensions = new string[] {
|
|
StringLiterals.PowerShellDataFileExtension,
|
|
StringLiterals.PowerShellModuleFileExtension,
|
|
StringLiterals.PowerShellCmdletizationFileExtension,
|
|
StringLiterals.WorkflowFileExtension,
|
|
StringLiterals.PowerShellNgenAssemblyExtension,
|
|
StringLiterals.PowerShellILAssemblyExtension};
|
|
|
|
/// <summary>
|
|
/// Returns true if the extension is one of the module extensions...
|
|
/// </summary>
|
|
/// <param name="extension">The extension to check</param>
|
|
/// <returns>True if it was a module extension...</returns>
|
|
internal static bool IsPowerShellModuleExtension(string extension)
|
|
{
|
|
foreach (string ext in PSModuleProcessableExtensions)
|
|
{
|
|
if (extension.Equals(ext, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the module name from module path.
|
|
/// </summary>
|
|
/// <param name="path">The path to the module</param>
|
|
/// <returns>The module name</returns>
|
|
internal static string GetModuleName(string path)
|
|
{
|
|
string fileName = path == null ? string.Empty : Path.GetFileName(path);
|
|
string ext;
|
|
if (fileName.EndsWith(StringLiterals.PowerShellNgenAssemblyExtension, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ext = StringLiterals.PowerShellNgenAssemblyExtension;
|
|
}
|
|
else
|
|
{
|
|
ext = Path.GetExtension(fileName);
|
|
}
|
|
if (!string.IsNullOrEmpty(ext) && IsPowerShellModuleExtension(ext))
|
|
{
|
|
return fileName.Substring(0, fileName.Length - ext.Length);
|
|
}
|
|
else
|
|
{
|
|
return fileName;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the personal module path
|
|
/// </summary>
|
|
/// <returns>personal module path</returns>
|
|
internal static string GetPersonalModulePath()
|
|
{
|
|
#if UNIX
|
|
return Platform.SelectProductNameForDirectory(Platform.XDG_Type.USER_MODULES);
|
|
#else
|
|
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), Utils.ModuleDirectory);
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the PSHome module path, as known as the "system wide module path" in windows powershell.
|
|
/// </summary>
|
|
/// <returns>The PSHome module path</returns>
|
|
internal static string GetPSHomeModulePath()
|
|
{
|
|
if (s_psHomeModulePath != null)
|
|
return s_psHomeModulePath;
|
|
|
|
try
|
|
{
|
|
string psHome = Utils.DefaultPowerShellAppBase;
|
|
if (!string.IsNullOrEmpty(psHome))
|
|
{
|
|
// Win8: 584267 Powershell Modules are listed twice in x86, and cannot be removed
|
|
// This happens because ModuleTable uses Path as the key and CBS installer
|
|
// expands the path to include "SysWOW64" (for
|
|
// HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\PowerShell\3\PowerShellEngine ApplicationBase).
|
|
// Because of this, the module that is getting loaded during startup (through LocalRunspace)
|
|
// is using "SysWow64" in the key. Later, when Import-Module is called, it loads the
|
|
// module using ""System32" in the key.
|
|
#if !UNIX
|
|
psHome = psHome.ToLowerInvariant().Replace("\\syswow64\\", "\\system32\\");
|
|
#endif
|
|
Interlocked.CompareExchange(ref s_psHomeModulePath, Path.Combine(psHome, "Modules"), null);
|
|
}
|
|
}
|
|
catch (System.Security.SecurityException) { }
|
|
|
|
return s_psHomeModulePath;
|
|
}
|
|
|
|
private static string s_psHomeModulePath;
|
|
|
|
/// <summary>
|
|
/// Get the module path that is shared among different users.
|
|
/// It's known as "Program Files" module path in windows powershell.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private static string GetSharedModulePath()
|
|
{
|
|
#if UNIX
|
|
return Platform.SelectProductNameForDirectory(Platform.XDG_Type.SHARED_MODULES);
|
|
#else
|
|
string sharedModulePath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
|
|
|
if (!string.IsNullOrEmpty(sharedModulePath))
|
|
{
|
|
sharedModulePath = Path.Combine(sharedModulePath, Utils.ModuleDirectory);
|
|
}
|
|
return sharedModulePath;
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Combine the PS system-wide module path and the DSC module path
|
|
/// to get the system module paths.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private static string CombineSystemModulePaths()
|
|
{
|
|
string psHomeModulePath = GetPSHomeModulePath();
|
|
string sharedModulePath = GetSharedModulePath();
|
|
|
|
bool isPSHomePathNullOrEmpty = string.IsNullOrEmpty(psHomeModulePath);
|
|
bool isSharedPathNullOrEmpty = string.IsNullOrEmpty(sharedModulePath);
|
|
|
|
if (!isPSHomePathNullOrEmpty && !isSharedPathNullOrEmpty)
|
|
{
|
|
return (sharedModulePath + Path.PathSeparator + psHomeModulePath);
|
|
}
|
|
|
|
if (!isPSHomePathNullOrEmpty || !isSharedPathNullOrEmpty)
|
|
{
|
|
return isPSHomePathNullOrEmpty ? sharedModulePath : psHomeModulePath;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
internal static string GetExpandedEnvironmentVariable(string name, EnvironmentVariableTarget target)
|
|
{
|
|
string result = Environment.GetEnvironmentVariable(name, target);
|
|
if (!string.IsNullOrEmpty(result))
|
|
{
|
|
result = Environment.ExpandEnvironmentVariables(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a particular string (path) is a member of 'combined path' string (like %Path% or %PSModulePath%)
|
|
/// </summary>
|
|
/// <param name="pathToScan">'Combined path' string to analyze; can not be null.</param>
|
|
/// <param name="pathToLookFor">Path to search for; can not be another 'combined path' (semicolon-separated); can not be null.</param>
|
|
/// <returns>Index of pathToLookFor in pathToScan; -1 if not found.</returns>
|
|
private static int PathContainsSubstring(string pathToScan, string pathToLookFor)
|
|
{
|
|
// we don't support if any of the args are null - parent function should ensure this; empty values are ok
|
|
Diagnostics.Assert(pathToScan != null, "pathToScan should not be null according to contract of the function");
|
|
Diagnostics.Assert(pathToLookFor != null, "pathToLookFor should not be null according to contract of the function");
|
|
|
|
int pos = 0; // position of the current substring in pathToScan
|
|
string[] substrings = pathToScan.Split(Utils.Separators.PathSeparator, StringSplitOptions.None); // we want to process empty entries
|
|
string goodPathToLookFor = pathToLookFor.Trim().TrimEnd(Path.DirectorySeparatorChar); // trailing backslashes and white-spaces will mess up equality comparison
|
|
foreach (string substring in substrings)
|
|
{
|
|
string goodSubstring = substring.Trim().TrimEnd(Path.DirectorySeparatorChar); // trailing backslashes and white-spaces will mess up equality comparison
|
|
|
|
// We have to use equality comparison on individual substrings (as opposed to simple 'string.IndexOf' or 'string.Contains')
|
|
// because of cases like { pathToScan="C:\Temp\MyDir\MyModuleDir", pathToLookFor="C:\Temp" }
|
|
if (string.Equals(goodSubstring, goodPathToLookFor, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return pos; // match found - return index of it in the 'pathToScan' string
|
|
}
|
|
else
|
|
{
|
|
pos += substring.Length + 1; // '1' is for trailing semicolon
|
|
}
|
|
}
|
|
// if we are here, that means a match was not found
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds paths to a 'combined path' string (like %Path% or %PSModulePath%) if they are not already there.
|
|
/// </summary>
|
|
/// <param name="basePath">Path string (like %Path% or %PSModulePath%).</param>
|
|
/// <param name="pathToAdd">Collection of individual paths to add.</param>
|
|
/// <param name="insertPosition">-1 to append to the end; 0 to insert in the beginning of the string; etc...</param>
|
|
/// <returns>Result string.</returns>
|
|
private static string AddToPath(string basePath, string pathToAdd, int insertPosition)
|
|
{
|
|
// we don't support if any of the args are null - parent function should ensure this; empty values are ok
|
|
Diagnostics.Assert(basePath != null, "basePath should not be null according to contract of the function");
|
|
Diagnostics.Assert(pathToAdd != null, "pathToAdd should not be null according to contract of the function");
|
|
|
|
StringBuilder result = new StringBuilder(basePath);
|
|
|
|
if (!string.IsNullOrEmpty(pathToAdd)) // we don't want to append empty paths
|
|
{
|
|
foreach (string subPathToAdd in pathToAdd.Split(Utils.Separators.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) // in case pathToAdd is a 'combined path' (semicolon-separated)
|
|
{
|
|
int position = PathContainsSubstring(result.ToString(), subPathToAdd); // searching in effective 'result' value ensures that possible duplicates in pathsToAdd are handled correctly
|
|
if (-1 == position) // subPathToAdd not found - add it
|
|
{
|
|
if (-1 == insertPosition) // append subPathToAdd to the end
|
|
{
|
|
bool endsWithPathSeparator = false;
|
|
if (result.Length > 0) endsWithPathSeparator = (result[result.Length - 1] == Path.PathSeparator);
|
|
|
|
if (endsWithPathSeparator)
|
|
result.Append(subPathToAdd);
|
|
else
|
|
result.Append(Path.PathSeparator + subPathToAdd);
|
|
}
|
|
else // insert at the requested location (this is used by DSC (<Program Files> location) and by 'user-specific location' (SpecialFolder.MyDocuments or EVT.User))
|
|
{
|
|
result.Insert(insertPosition, subPathToAdd + Path.PathSeparator);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the current powershell is likely running in following scenarios:
|
|
/// - sxs ps started on windows [machine-wide env:PSModulePath will influence]
|
|
/// - sxs ps started from full ps
|
|
/// - sxs ps started from inbox nano/iot ps
|
|
/// - full ps started from sxs ps
|
|
/// - inbox nano/iot ps started from sxs ps
|
|
/// If it's likely one of them, then we need to clear the current process module path.
|
|
/// </summary>
|
|
private static bool NeedToClearProcessModulePath(string currentProcessModulePath, string personalModulePath, string sharedModulePath, bool runningSxS)
|
|
{
|
|
#if UNIX
|
|
return false;
|
|
#else
|
|
Dbg.Assert(!string.IsNullOrEmpty(personalModulePath), "caller makes sure personalModulePath not null or empty");
|
|
Dbg.Assert(sharedModulePath != null, "caller makes sure sharedModulePath is not null");
|
|
|
|
const string winSxSModuleDirectory = @"PowerShell\Modules";
|
|
const string winLegacyModuleDirectory = @"WindowsPowerShell\Modules";
|
|
|
|
if (runningSxS)
|
|
{
|
|
// The machine-wide and user-wide environment variables are only meaningful for full ps,
|
|
// so if the current process module path contains any of them, it's likely that the sxs
|
|
// ps was started directly on windows, or from full ps. The same goes for the legacy personal
|
|
// and shared module paths.
|
|
string hklmModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Machine);
|
|
string hkcuModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.User);
|
|
string legacyPersonalModulePath = personalModulePath.Replace(winSxSModuleDirectory, winLegacyModuleDirectory);
|
|
string legacyProgramFilesModulePath = sharedModulePath.Replace(winSxSModuleDirectory, winLegacyModuleDirectory);
|
|
|
|
return (!string.IsNullOrEmpty(hklmModulePath) && currentProcessModulePath.IndexOf(hklmModulePath, StringComparison.OrdinalIgnoreCase) != -1) ||
|
|
(!string.IsNullOrEmpty(hkcuModulePath) && currentProcessModulePath.IndexOf(hkcuModulePath, StringComparison.OrdinalIgnoreCase) != -1) ||
|
|
currentProcessModulePath.IndexOf(legacyPersonalModulePath, StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
currentProcessModulePath.IndexOf(legacyProgramFilesModulePath, StringComparison.OrdinalIgnoreCase) != -1;
|
|
}
|
|
|
|
// The sxs personal and shared module paths are only meaningful for sxs ps, so if they appear
|
|
// in the current process module path, it's likely the running ps was started from a sxs ps.
|
|
string sxsPersonalModulePath = personalModulePath.Replace(winLegacyModuleDirectory, winSxSModuleDirectory);
|
|
string sxsProgramFilesModulePath = sharedModulePath.Replace(winLegacyModuleDirectory, winSxSModuleDirectory);
|
|
|
|
return currentProcessModulePath.IndexOf(sxsPersonalModulePath, StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
currentProcessModulePath.IndexOf(sxsProgramFilesModulePath, StringComparison.OrdinalIgnoreCase) != -1;
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// When sxs ps instance B got started from sxs ps instance A, A's pshome module path might
|
|
/// show up in current process module path. It doesn't make sense for B to load modules from
|
|
/// A's pshome module path, so remove it in such case.
|
|
/// </summary>
|
|
private static string RemoveSxSPsHomeModulePath(string currentProcessModulePath, string personalModulePath, string sharedModulePath, string psHomeModulePath)
|
|
{
|
|
#if UNIX
|
|
const string powershellExeName = "powershell";
|
|
#else
|
|
const string powershellExeName = "powershell.exe";
|
|
#endif
|
|
StringBuilder modulePathString = new StringBuilder(currentProcessModulePath.Length);
|
|
char[] invalidPathChars = Path.GetInvalidPathChars();
|
|
|
|
foreach (var path in currentProcessModulePath.Split(Utils.Separators.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
string trimedPath = path.Trim().TrimEnd(Path.DirectorySeparatorChar);
|
|
if (trimedPath.IndexOfAny(invalidPathChars) != -1 || !Path.IsPathRooted(trimedPath))
|
|
{
|
|
// Path contains invalid characters or it's not an absolute path. Ignore it.
|
|
continue;
|
|
}
|
|
|
|
if (!trimedPath.Equals(personalModulePath, StringComparison.OrdinalIgnoreCase) &&
|
|
!trimedPath.Equals(sharedModulePath, StringComparison.OrdinalIgnoreCase) &&
|
|
!trimedPath.Equals(psHomeModulePath, StringComparison.OrdinalIgnoreCase) &&
|
|
trimedPath.EndsWith("Modules", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string parentDir = Path.GetDirectoryName(trimedPath);
|
|
string psExePath = Path.Combine(parentDir, powershellExeName);
|
|
string psDepsPath = Path.Combine(parentDir, "powershell.deps.json");
|
|
if (File.Exists(psExePath) && File.Exists(psDepsPath))
|
|
{
|
|
// Path is a PSHome module path from a different powershell core instance. Ignore it.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (modulePathString.Length > 0)
|
|
{
|
|
modulePathString.Append(Path.PathSeparator);
|
|
}
|
|
modulePathString.Append(trimedPath);
|
|
}
|
|
|
|
return modulePathString.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks the various PSModulePath environment string and returns PSModulePath string as appropriate. Note - because these
|
|
/// strings go through the provider, we need to escape any wildcards before passing them
|
|
/// along.
|
|
/// </summary>
|
|
public static string GetModulePath(string currentProcessModulePath, string hklmMachineModulePath, string hkcuUserModulePath)
|
|
{
|
|
string personalModulePath = GetPersonalModulePath();
|
|
string sharedModulePath = GetSharedModulePath(); // aka <Program Files> location
|
|
string psHomeModulePath = GetPSHomeModulePath(); // $PSHome\Modules location
|
|
bool runningSxS = Platform.IsInbox ? false : true;
|
|
|
|
if (!string.IsNullOrEmpty(currentProcessModulePath) &&
|
|
NeedToClearProcessModulePath(currentProcessModulePath, personalModulePath, sharedModulePath, runningSxS))
|
|
{
|
|
// Clear the current process module path in the following cases
|
|
// - start sxs ps on windows [machine-wide env:PSModulePath will influence]
|
|
// - start sxs ps from full ps
|
|
// - start sxs ps from inbox nano/iot ps
|
|
// - start full ps from sxs ps
|
|
// - start inbox nano/iot ps from sxs ps
|
|
currentProcessModulePath = null;
|
|
}
|
|
|
|
// If the variable isn't set, then set it to the default value
|
|
if (currentProcessModulePath == null) // EVT.Process does Not exist - really corner case
|
|
{
|
|
// Handle the default case...
|
|
if (string.IsNullOrEmpty(hkcuUserModulePath)) // EVT.User does Not exist -> set to <SpecialFolder.MyDocuments> location
|
|
{
|
|
currentProcessModulePath = personalModulePath; // = SpecialFolder.MyDocuments + Utils.ProductNameForDirectory + Utils.ModuleDirectory
|
|
}
|
|
else // EVT.User exists -> set to EVT.User
|
|
{
|
|
currentProcessModulePath = hkcuUserModulePath; // = EVT.User
|
|
}
|
|
|
|
currentProcessModulePath += Path.PathSeparator;
|
|
if (string.IsNullOrEmpty(hklmMachineModulePath)) // EVT.Machine does Not exist
|
|
{
|
|
currentProcessModulePath += CombineSystemModulePaths(); // += (SharedModulePath + $PSHome\Modules)
|
|
}
|
|
else
|
|
{
|
|
currentProcessModulePath += hklmMachineModulePath; // += EVT.Machine
|
|
}
|
|
}
|
|
// EVT.Process exists
|
|
// Now handle the case where the environment variable is already set.
|
|
else if (runningSxS) // The running powershell is an SxS PS instance
|
|
{
|
|
// When SxS PS instance A starts SxS PS instance B, A's PSHome module path might be inherited by B. We need to remove that path from B
|
|
currentProcessModulePath = RemoveSxSPsHomeModulePath(currentProcessModulePath, personalModulePath, sharedModulePath, psHomeModulePath);
|
|
|
|
string personalModulePathToUse = string.IsNullOrEmpty(hkcuUserModulePath) ? personalModulePath : hkcuUserModulePath;
|
|
string systemModulePathToUse = string.IsNullOrEmpty(hklmMachineModulePath) ? psHomeModulePath : hklmMachineModulePath;
|
|
|
|
currentProcessModulePath = AddToPath(currentProcessModulePath, personalModulePathToUse, 0);
|
|
currentProcessModulePath = AddToPath(currentProcessModulePath, systemModulePathToUse, -1);
|
|
}
|
|
else // The running powershell is Full PS or inbox Core PS
|
|
{
|
|
// If there is no personal path key, then if the env variable doesn't match the system variable,
|
|
// the user modified it somewhere, else prepend the default personal module path
|
|
if (hklmMachineModulePath != null) // EVT.Machine exists
|
|
{
|
|
if (hkcuUserModulePath == null) // EVT.User does Not exist
|
|
{
|
|
if (!(hklmMachineModulePath).Equals(currentProcessModulePath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// before returning, use <presence of Windows module path> heuristic to conditionally add <Program Files> location
|
|
int psHomePosition = PathContainsSubstring(currentProcessModulePath, psHomeModulePath); // index of $PSHome\Modules in currentProcessModulePath
|
|
if (psHomePosition >= 0) // if $PSHome\Modules IS found - insert <Program Files> location before $PSHome\Modules
|
|
{
|
|
return AddToPath(currentProcessModulePath, sharedModulePath, psHomePosition);
|
|
} // if $PSHome\Modules NOT found = <scenario 4> = 'PSModulePath has been constrained by a user to create a sand boxed environment without including System Modules'
|
|
|
|
return null;
|
|
}
|
|
currentProcessModulePath = personalModulePath + Path.PathSeparator + hklmMachineModulePath; // <SpecialFolder.MyDocuments> + EVT.Machine + inserted <ProgramFiles> later in this function
|
|
}
|
|
else // EVT.User exists
|
|
{
|
|
// PSModulePath is designed to have behaviour like 'Path' var in a sense that EVT.User + EVT.Machine are merged to get final value of PSModulePath
|
|
string combined = string.Concat(hkcuUserModulePath, Path.PathSeparator, hklmMachineModulePath); // EVT.User + EVT.Machine
|
|
if (!((combined).Equals(currentProcessModulePath, StringComparison.OrdinalIgnoreCase) ||
|
|
(hklmMachineModulePath).Equals(currentProcessModulePath, StringComparison.OrdinalIgnoreCase) ||
|
|
(hkcuUserModulePath).Equals(currentProcessModulePath, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
// before returning, use <presence of Windows module path> heuristic to conditionally add <Program Files> location
|
|
int psHomePosition = PathContainsSubstring(currentProcessModulePath, psHomeModulePath); // index of $PSHome\Modules in currentProcessModulePath
|
|
if (psHomePosition >= 0) // if $PSHome\Modules IS found - insert <Program Files> location before $PSHome\Modules
|
|
{
|
|
return AddToPath(currentProcessModulePath, sharedModulePath, psHomePosition);
|
|
} // if $PSHome\Modules NOT found = <scenario 4> = 'PSModulePath has been constrained by a user to create a sand boxed environment without including System Modules'
|
|
|
|
return null;
|
|
}
|
|
currentProcessModulePath = combined; // = EVT.User + EVT.Machine + inserted <ProgramFiles> later in this function
|
|
}
|
|
}
|
|
else // EVT.Machine does Not exist
|
|
{
|
|
// If there is no system path key, then if the env variable doesn't match the user variable,
|
|
// the user modified it somewhere, otherwise append the default system path
|
|
if (hkcuUserModulePath != null) // EVT.User exists
|
|
{
|
|
if (hkcuUserModulePath.Equals(currentProcessModulePath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
currentProcessModulePath = hkcuUserModulePath + Path.PathSeparator + CombineSystemModulePaths(); // = EVT.User + (SharedModulePath + $PSHome\Modules)
|
|
}
|
|
else
|
|
{
|
|
// before returning, use <presence of Windows module path> heuristic to conditionally add <Program Files> location
|
|
int psHomePosition = PathContainsSubstring(currentProcessModulePath, psHomeModulePath); // index of $PSHome\Modules in currentProcessModulePath
|
|
if (psHomePosition >= 0) // if $PSHome\Modules IS found - insert <Program Files> location before $PSHome\Modules
|
|
{
|
|
return AddToPath(currentProcessModulePath, sharedModulePath, psHomePosition);
|
|
} // if $PSHome\Modules NOT found = <scenario 4> = 'PSModulePath has been constrained by a user to create a sand boxed environment without including System Modules'
|
|
|
|
return null;
|
|
}
|
|
}
|
|
else // EVT.User does Not exist
|
|
{
|
|
// before returning, use <presence of Windows module path> heuristic to conditionally add <Program Files> location
|
|
int psHomePosition = PathContainsSubstring(currentProcessModulePath, psHomeModulePath); // index of $PSHome\Modules in currentProcessModulePath
|
|
if (psHomePosition >= 0) // if $PSHome\Modules IS found - insert <Program Files> location before $PSHome\Modules
|
|
{
|
|
return AddToPath(currentProcessModulePath, sharedModulePath, psHomePosition);
|
|
} // if $PSHome\Modules NOT found = <scenario 4> = 'PSModulePath has been constrained by a user to create a sand boxed environment without including System Modules'
|
|
|
|
// Neither key is set so go with what the environment variable is already set to
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we reached this point - always add <Program Files> location to EVT.Process
|
|
// everything below is the same behaviour as WMF 4 code
|
|
int indexOfPSHomeModulePath = PathContainsSubstring(currentProcessModulePath, psHomeModulePath); // index of $PSHome\Modules in currentProcessModulePath
|
|
// if $PSHome\Modules not found (psHomePosition == -1) - append <Program Files> location to the end;
|
|
// if $PSHome\Modules IS found (psHomePosition >= 0) - insert <Program Files> location before $PSHome\Modules
|
|
currentProcessModulePath = AddToPath(currentProcessModulePath, sharedModulePath, indexOfPSHomeModulePath);
|
|
|
|
return currentProcessModulePath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if $env:PSModulePath is not set and sets it as appropriate. Note - because these
|
|
/// strings go through the provider, we need to escape any wildcards before passing them
|
|
/// along.
|
|
/// </summary>
|
|
internal static string GetModulePath()
|
|
{
|
|
string currentModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Process);
|
|
return currentModulePath;
|
|
}
|
|
/// <summary>
|
|
/// Checks if $env:PSModulePath is not set and sets it as appropriate. Note - because these
|
|
/// strings go through the provider, we need to escape any wildcards before passing them
|
|
/// along.
|
|
/// </summary>
|
|
internal static string SetModulePath()
|
|
{
|
|
string currentModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Process);
|
|
string systemWideModulePath = ConfigPropertyAccessor.Instance.GetModulePath(ConfigPropertyAccessor.PropertyScope.SystemWide);
|
|
string personalModulePath = ConfigPropertyAccessor.Instance.GetModulePath(ConfigPropertyAccessor.PropertyScope.CurrentUser);
|
|
|
|
string newModulePathString = GetModulePath(currentModulePath, systemWideModulePath, personalModulePath);
|
|
|
|
if (!string.IsNullOrEmpty(newModulePathString))
|
|
{
|
|
// Set the environment variable...
|
|
Environment.SetEnvironmentVariable(Constants.PSModulePathEnvVar, newModulePathString);
|
|
}
|
|
|
|
return newModulePathString;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the current module path setting.
|
|
/// </summary>
|
|
/// <param name="includeSystemModulePath">
|
|
/// Include The system wide module path ($PSHOME\Modules) even if it's not in PSModulePath.
|
|
/// In V3-V5, we prepended this path during module auto-discovery which incorrectly preferred
|
|
/// $PSHOME\Modules over user installed modules that might have a command that overrides
|
|
/// a product-supplied command.
|
|
/// For 5.1, we append $PSHOME\Modules in this case to avoid the rare case where PSModulePath
|
|
/// does not contain the path, but a script depends on previous behavior.
|
|
/// Note that appending is still a potential breaking change, but necessary to update in-box
|
|
/// modules long term - e.g. when open sourcing a module and installing from the gallery.
|
|
/// </param>
|
|
/// <param name="context"></param>
|
|
/// <returns>The module path as an array of strings</returns>
|
|
internal static IEnumerable<string> GetModulePath(bool includeSystemModulePath, ExecutionContext context)
|
|
{
|
|
string modulePathString = Environment.GetEnvironmentVariable(Constants.PSModulePathEnvVar) ?? SetModulePath();
|
|
|
|
HashSet<string> processedPathSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (!string.IsNullOrWhiteSpace(modulePathString))
|
|
{
|
|
foreach (string envPath in modulePathString.Split(Utils.Separators.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
var processedPath = ProcessOneModulePath(context, envPath, processedPathSet);
|
|
if (processedPath != null)
|
|
yield return processedPath;
|
|
}
|
|
}
|
|
|
|
if (includeSystemModulePath)
|
|
{
|
|
var processedPath = ProcessOneModulePath(context, GetPSHomeModulePath(), processedPathSet);
|
|
if (processedPath != null)
|
|
yield return processedPath;
|
|
}
|
|
}
|
|
|
|
private static string ProcessOneModulePath(ExecutionContext context, string envPath, HashSet<string> processedPathSet)
|
|
{
|
|
string trimmedenvPath = envPath.Trim();
|
|
|
|
bool isUnc = Utils.PathIsUnc(trimmedenvPath);
|
|
if (!isUnc)
|
|
{
|
|
// if the path start with "filesystem::", remove it so we can test for URI and
|
|
// also Directory.Exists (if the file system provider isn't actually loaded.)
|
|
if (trimmedenvPath.StartsWith("filesystem::", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
trimmedenvPath = trimmedenvPath.Remove(0, 12 /*"filesystem::".Length*/);
|
|
}
|
|
|
|
isUnc = Utils.PathIsUnc(trimmedenvPath);
|
|
}
|
|
|
|
// If we have an unc, just return the value as resolving the path is expensive.
|
|
if (isUnc)
|
|
{
|
|
return trimmedenvPath;
|
|
}
|
|
|
|
// We prefer using the file system provider to resolve paths so callers can avoid processing
|
|
// duplicates, e.g. the following are all the same:
|
|
// a\b
|
|
// a\.\b
|
|
// a\b\
|
|
// But if the file system provider isn't loaded, we will just check if the directory exists.
|
|
if (context.EngineSessionState.IsProviderLoaded(context.ProviderNames.FileSystem))
|
|
{
|
|
ProviderInfo provider = null;
|
|
IEnumerable<string> resolvedPaths = null;
|
|
try
|
|
{
|
|
resolvedPaths = context.SessionState.Path.GetResolvedProviderPathFromPSPath(
|
|
WildcardPattern.Escape(trimmedenvPath), out provider);
|
|
}
|
|
catch (ItemNotFoundException)
|
|
{
|
|
// silently skip directories that are not found
|
|
}
|
|
catch (DriveNotFoundException)
|
|
{
|
|
// silently skip drives that are not found
|
|
}
|
|
catch (NotSupportedException)
|
|
{
|
|
// silently skip invalid path
|
|
// NotSupportedException is thrown if path contains a colon (":") that is not part of a
|
|
// volume identifier (for example, "c:\" is Supported but not "c:\temp\Z:\invalidPath")
|
|
}
|
|
|
|
if (provider != null && resolvedPaths != null && provider.NameEquals(context.ProviderNames.FileSystem))
|
|
{
|
|
var result = resolvedPaths.FirstOrDefault();
|
|
if (processedPathSet.Add(result))
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
else if (Directory.Exists(trimmedenvPath))
|
|
{
|
|
return trimmedenvPath;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static void SortAndRemoveDuplicates<T>(List<T> input, Func<T, string> keyGetter)
|
|
{
|
|
Dbg.Assert(input != null, "Caller should verify that input != null");
|
|
|
|
input.Sort(
|
|
delegate (T x, T y)
|
|
{
|
|
string kx = keyGetter(x);
|
|
string ky = keyGetter(y);
|
|
return string.Compare(kx, ky, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
);
|
|
|
|
bool firstItem = true;
|
|
string previousKey = null;
|
|
List<T> output = new List<T>(input.Count);
|
|
foreach (T item in input)
|
|
{
|
|
string currentKey = keyGetter(item);
|
|
if ((firstItem) || !currentKey.Equals(previousKey, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
output.Add(item);
|
|
}
|
|
|
|
previousKey = currentKey;
|
|
firstItem = false;
|
|
}
|
|
|
|
input.Clear();
|
|
input.AddRange(output);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mark stuff to be exported from the current environment using the various patterns
|
|
/// </summary>
|
|
/// <param name="cmdlet">The cmdlet calling this method</param>
|
|
/// <param name="sessionState">The session state instance to do the exports on</param>
|
|
/// <param name="functionPatterns">Patterns describing the functions to export</param>
|
|
/// <param name="cmdletPatterns">Patterns describing the cmdlets to export</param>
|
|
/// <param name="aliasPatterns">Patterns describing the aliases to export</param>
|
|
/// <param name="variablePatterns">Patterns describing the variables to export</param>
|
|
/// <param name="doNotExportCmdlets">List of Cmdlets that will not be exported,
|
|
/// even if they match in cmdletPatterns.</param>
|
|
internal static void ExportModuleMembers(PSCmdlet cmdlet, SessionStateInternal sessionState,
|
|
List<WildcardPattern> functionPatterns, List<WildcardPattern> cmdletPatterns,
|
|
List<WildcardPattern> aliasPatterns, List<WildcardPattern> variablePatterns, List<string> doNotExportCmdlets)
|
|
{
|
|
// If this cmdlet is called, then mark that the export list should be used for exporting
|
|
// module members...
|
|
|
|
sessionState.UseExportList = true;
|
|
|
|
if (functionPatterns != null)
|
|
{
|
|
IDictionary<string, FunctionInfo> ft = sessionState.ModuleScope.FunctionTable;
|
|
|
|
foreach (KeyValuePair<string, FunctionInfo> entry in ft)
|
|
{
|
|
// Skip AllScope functions
|
|
if ((entry.Value.Options & ScopedItemOptions.AllScope) != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (SessionStateUtilities.MatchesAnyWildcardPattern(entry.Key, functionPatterns, false))
|
|
{
|
|
string message;
|
|
|
|
if (entry.Value.CommandType == CommandTypes.Workflow)
|
|
{
|
|
message = StringUtil.Format(Modules.ExportingWorkflow, entry.Key);
|
|
sessionState.ExportedWorkflows.Add((WorkflowInfo)entry.Value);
|
|
}
|
|
else
|
|
{
|
|
message = StringUtil.Format(Modules.ExportingFunction, entry.Key);
|
|
sessionState.ExportedFunctions.Add(entry.Value);
|
|
}
|
|
|
|
cmdlet.WriteVerbose(message);
|
|
}
|
|
}
|
|
SortAndRemoveDuplicates(sessionState.ExportedFunctions, delegate (FunctionInfo ci) { return ci.Name; });
|
|
SortAndRemoveDuplicates(sessionState.ExportedWorkflows, delegate (WorkflowInfo ci) { return ci.Name; });
|
|
}
|
|
|
|
if (cmdletPatterns != null)
|
|
{
|
|
IDictionary<string, List<CmdletInfo>> ft = sessionState.ModuleScope.CmdletTable;
|
|
|
|
// Subset the existing cmdlet exports if there are any. This will be the case
|
|
// if we're using ModuleToProcess to import a binary module which has nested modules.
|
|
if (sessionState.Module.CompiledExports.Count > 0)
|
|
{
|
|
CmdletInfo[] copy = sessionState.Module.CompiledExports.ToArray();
|
|
sessionState.Module.CompiledExports.Clear();
|
|
|
|
foreach (CmdletInfo element in copy)
|
|
{
|
|
if (doNotExportCmdlets == null
|
|
|| !doNotExportCmdlets.Exists(cmdletName => string.Equals(element.FullName, cmdletName, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
if (SessionStateUtilities.MatchesAnyWildcardPattern(element.Name, cmdletPatterns, false))
|
|
{
|
|
string message = StringUtil.Format(Modules.ExportingCmdlet, element.Name);
|
|
cmdlet.WriteVerbose(message);
|
|
// Copy the cmdlet info, changing the module association to be the current module...
|
|
CmdletInfo exportedCmdlet = new CmdletInfo(element.Name, element.ImplementingType,
|
|
element.HelpFile, null, element.Context)
|
|
{ Module = sessionState.Module };
|
|
Dbg.Assert(sessionState.Module != null, "sessionState.Module should not be null by the time we're exporting cmdlets");
|
|
sessionState.Module.CompiledExports.Add(exportedCmdlet);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// And copy in any cmdlets imported from the nested modules...
|
|
foreach (KeyValuePair<string, List<CmdletInfo>> entry in ft)
|
|
{
|
|
CmdletInfo cmdletToImport = entry.Value[0];
|
|
if (doNotExportCmdlets == null
|
|
|| !doNotExportCmdlets.Exists(cmdletName => string.Equals(cmdletToImport.FullName, cmdletName, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
if (SessionStateUtilities.MatchesAnyWildcardPattern(entry.Key, cmdletPatterns, false))
|
|
{
|
|
string message = StringUtil.Format(Modules.ExportingCmdlet, entry.Key);
|
|
cmdlet.WriteVerbose(message);
|
|
// Copy the cmdlet info, changing the module association to be the current module...
|
|
CmdletInfo exportedCmdlet = new CmdletInfo(cmdletToImport.Name, cmdletToImport.ImplementingType,
|
|
cmdletToImport.HelpFile, null, cmdletToImport.Context)
|
|
{ Module = sessionState.Module };
|
|
Dbg.Assert(sessionState.Module != null, "sessionState.Module should not be null by the time we're exporting cmdlets");
|
|
sessionState.Module.CompiledExports.Add(exportedCmdlet);
|
|
}
|
|
}
|
|
}
|
|
|
|
SortAndRemoveDuplicates(sessionState.Module.CompiledExports, delegate (CmdletInfo ci) { return ci.Name; });
|
|
}
|
|
|
|
if (variablePatterns != null)
|
|
{
|
|
IDictionary<string, PSVariable> vt = sessionState.ModuleScope.Variables;
|
|
|
|
foreach (KeyValuePair<string, PSVariable> entry in vt)
|
|
{
|
|
// The magic variables are always private as are all-scope variables...
|
|
if (entry.Value.IsAllScope || Array.IndexOf(PSModuleInfo._builtinVariables, entry.Key) != -1)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (SessionStateUtilities.MatchesAnyWildcardPattern(entry.Key, variablePatterns, false))
|
|
{
|
|
string message = StringUtil.Format(Modules.ExportingVariable, entry.Key);
|
|
cmdlet.WriteVerbose(message);
|
|
sessionState.ExportedVariables.Add(entry.Value);
|
|
}
|
|
}
|
|
SortAndRemoveDuplicates(sessionState.ExportedVariables, delegate (PSVariable v) { return v.Name; });
|
|
}
|
|
|
|
if (aliasPatterns != null)
|
|
{
|
|
IEnumerable<AliasInfo> mai = sessionState.ModuleScope.AliasTable;
|
|
|
|
// Subset the existing alias exports if there are any. This will be the case
|
|
// if we're using ModuleToProcess to import a binary module which has nested modules.
|
|
if (sessionState.Module.CompiledAliasExports.Count > 0)
|
|
{
|
|
AliasInfo[] copy = sessionState.Module.CompiledAliasExports.ToArray();
|
|
|
|
foreach (var element in copy)
|
|
{
|
|
if (SessionStateUtilities.MatchesAnyWildcardPattern(element.Name, aliasPatterns, false))
|
|
{
|
|
string message = StringUtil.Format(Modules.ExportingAlias, element.Name);
|
|
cmdlet.WriteVerbose(message);
|
|
sessionState.ExportedAliases.Add(NewAliasInfo(element, sessionState));
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (AliasInfo entry in mai)
|
|
{
|
|
// Skip allscope items...
|
|
if ((entry.Options & ScopedItemOptions.AllScope) != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (SessionStateUtilities.MatchesAnyWildcardPattern(entry.Name, aliasPatterns, false))
|
|
{
|
|
string message = StringUtil.Format(Modules.ExportingAlias, entry.Name);
|
|
cmdlet.WriteVerbose(message);
|
|
sessionState.ExportedAliases.Add(NewAliasInfo(entry, sessionState));
|
|
}
|
|
}
|
|
|
|
SortAndRemoveDuplicates(sessionState.ExportedAliases, delegate (AliasInfo ci) { return ci.Name; });
|
|
}
|
|
}
|
|
|
|
private static AliasInfo NewAliasInfo(AliasInfo alias, SessionStateInternal sessionState)
|
|
{
|
|
Dbg.Assert(alias != null, "alias should not be null");
|
|
Dbg.Assert(sessionState != null, "sessionState should not be null");
|
|
Dbg.Assert(sessionState.Module != null, "sessionState.Module should not be null by the time we're exporting aliases");
|
|
|
|
// Copy the alias info, changing the module association to be the current module...
|
|
var aliasCopy = new AliasInfo(alias.Name, alias.Definition, alias.Context, alias.Options)
|
|
{
|
|
Module = sessionState.Module
|
|
};
|
|
return aliasCopy;
|
|
}
|
|
} // ModuleIntrinsics
|
|
|
|
/// <summary>
|
|
/// Used by Modules/Snapins to provide a hook to the engine for startup initialization
|
|
/// w.r.t compiled assembly loading.
|
|
/// </summary>
|
|
public interface IModuleAssemblyInitializer
|
|
{
|
|
/// <summary>
|
|
/// Gets called when assembly is loaded.
|
|
/// </summary>
|
|
void OnImport();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used by modules to provide a hook to the engine for cleanup on removal
|
|
/// w.r.t. compiled assembly being removed.
|
|
/// </summary>
|
|
public interface IModuleAssemblyCleanup
|
|
{
|
|
/// <summary>
|
|
/// Gets called when the binary module is unloaded.
|
|
/// </summary>
|
|
void OnRemove(PSModuleInfo psModuleInfo);
|
|
}
|
|
} // System.Management.Automation
|