Skip to content
180 changes: 52 additions & 128 deletions src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Threading;

using Microsoft.PowerShell.Commands;
using Microsoft.Win32;

using Dbg = System.Management.Automation.Diagnostics;

Expand Down Expand Up @@ -1162,92 +1163,6 @@ private static string AddToPath(string basePath, string pathToAdd, int insertPos
return result.ToString();
}

/// <summary>
/// Check if the current powershell is likely running in following scenarios:
/// - PSCore started on windows [machine-wide env:PSModulePath will influence]
/// - PSCore started from full ps
/// - PSCore started from inbox nano/iot 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)
{
#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";

// 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;
#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 = "pwsh";
#else
const string powershellExeName = "pwsh.exe";
#endif
const string powershellDepsName = "pwsh.deps.json";

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, powershellDepsName);
if ((File.Exists(psExePath) && File.Exists(psDepsPath)))
{
// Path is a PSHome module path from a different PowerShell 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
Expand All @@ -1259,16 +1174,6 @@ public static string GetModulePath(string currentProcessModulePath, string hklmM
string sharedModulePath = GetSharedModulePath(); // aka <Program Files> location
string psHomeModulePath = GetPSHomeModulePath(); // $PSHome\Modules location

if (!string.IsNullOrEmpty(currentProcessModulePath) &&
NeedToClearProcessModulePath(currentProcessModulePath, personalModulePath, sharedModulePath))
{
// Clear the current process module path in the following cases
// - start PSCore on windows [machine-wide env:PSModulePath will influence]
// - start PSCore from full ps
// - start PSCore from inbox nano/iot 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
{
Expand All @@ -1291,39 +1196,30 @@ public static string GetModulePath(string currentProcessModulePath, string hklmM
{
currentProcessModulePath += hklmMachineModulePath; // += EVT.Machine
}

#if !UNIX
// Add Windows Modules path
currentProcessModulePath = $"{currentProcessModulePath}{Path.PathSeparator}{s_windowsPowerShellPSHomeModulePath}";
#endif
}
// EVT.Process exists
// Now handle the case where the environment variable is already set.
else
{
// 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;

// Maintain order of the paths, but ahead of any existing paths:
// personalModulePath
// sharedModulePath
// systemModulePath
currentProcessModulePath = AddToPath(currentProcessModulePath, personalModulePathToUse, 0);

int insertIndex = -1;
#if !UNIX
string windowsPowerShellModulePath = GetWindowsPowerShellPSHomeModulePath();
// If the Windows PowerShell Module path is already present, insert the system module path
// ($PSHOME/Modules) before it.
insertIndex = PathContainsSubstring(currentProcessModulePath, windowsPowerShellModulePath);
#endif
int insertIndex = PathContainsSubstring(currentProcessModulePath, personalModulePathToUse) + personalModulePathToUse.Length + 1;
currentProcessModulePath = AddToPath(currentProcessModulePath, sharedModulePath, insertIndex);
insertIndex = PathContainsSubstring(currentProcessModulePath, sharedModulePath) + sharedModulePath.Length + 1;
currentProcessModulePath = AddToPath(currentProcessModulePath, systemModulePathToUse, insertIndex);
}

// if we reached this point - always add <Program Files> location to EVT.Process
// everything below is the same behaviour as WMF 4 code

// index of $PSHome\Modules in currentProcessModulePath
int indexOfPSHomeModulePath = PathContainsSubstring(currentProcessModulePath, psHomeModulePath);

// 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;
}

Expand All @@ -1338,6 +1234,45 @@ internal static string GetModulePath()
return currentModulePath;
}

#if !UNIX
/// <summary>
/// Returns a PSModulePath suiteable for Windows PowerShell by removing this PowerShell's specific
/// paths from current PSModulePath.
/// </summary>
/// <returns>
/// Returns appropriate PSModulePath for Windows PowerShell.
/// </returns>
internal static string GetWindowsPowerShellModulePath()
{
string currentModulePath = GetModulePath();

if (currentModulePath == null)
{
return null;
}

// PowerShell specific paths including if set in powershell.config.json file we want to exclude
var excludeModulePaths = new HashSet<string> {
GetPersonalModulePath(),
GetSharedModulePath(),
GetPSHomeModulePath(),
PowerShellConfig.Instance.GetModulePath(ConfigScope.AllUsers),
PowerShellConfig.Instance.GetModulePath(ConfigScope.CurrentUser)
};

var modulePathList = new List<string>();
foreach (var path in currentModulePath.Split(';'))
{
if (!excludeModulePaths.Contains(path))
{
modulePathList.Add(path);
}
}

return string.Join(Path.PathSeparator, modulePathList);
}
#endif

/// <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
Expand All @@ -1353,17 +1288,6 @@ private static string SetModulePath()

if (!string.IsNullOrEmpty(newModulePathString))
{
#if !UNIX
// If on Windows, we want to add the System32 Windows PowerShell module directory
// so that Windows modules are discoverable
string windowsPowerShellModulePath = GetWindowsPowerShellPSHomeModulePath();
if (!newModulePathString.Contains(windowsPowerShellModulePath, StringComparison.OrdinalIgnoreCase))
{
newModulePathString += Path.PathSeparator + windowsPowerShellModulePath;
}
#endif

// Set the environment variable...
Environment.SetEnvironmentVariable(Constants.PSModulePathEnvVar, newModulePathString);
}

Expand Down
13 changes: 13 additions & 0 deletions src/System.Management.Automation/engine/NativeCommandProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,19 @@ private void InitNativeProcess()
// Get the start info for the process.
ProcessStartInfo startInfo = GetProcessStartInfo(redirectOutput, redirectError, redirectInput, soloCommand);

#if !UNIX
string commandPath = this.Path.ToLowerInvariant();
if (commandPath.EndsWith("powershell.exe") || commandPath.EndsWith("powershell_ise.exe"))
{
// if starting Windows PowerShell, need to remove PowerShell specific segments of PSModulePath
string psmodulepath = ModuleIntrinsics.GetWindowsPowerShellModulePath();
startInfo.Environment["PSModulePath"] = psmodulepath;

// must set UseShellExecute to false if we modify the environment block
startInfo.UseShellExecute = false;
}
#endif

if (this.Command.Context.CurrentPipelineStopping)
{
throw new PipelineStoppedException();
Expand Down
5 changes: 3 additions & 2 deletions test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ Describe "TabCompletion" -Tags CI {
}

It 'Should complete abbreviated function' {
$res = (TabExpansion2 -inputScript 'pschrl' -cursorColumn 'pschr'.Length).CompletionMatches.CompletionText
function Test-AbbreviatedFunctionExpansion {}
$res = (TabExpansion2 -inputScript 't-afe' -cursorColumn 't-afe'.Length).CompletionMatches.CompletionText
$res.Count | Should -BeGreaterOrEqual 1
$res | Should -BeExactly 'PSConsoleHostReadLine'
$res | Should -BeExactly 'Test-AbbreviatedFunctionExpansion'
}

It 'Should complete native exe' -Skip:(!$IsWindows) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Describe "Import-Module" -Tags "CI" {
}

It "should be able to load a module with a trailing directory separator: <modulePath>" -TestCases @(
@{ modulePath = (Get-Module -ListAvailable $moduleName).ModuleBase + [System.IO.Path]::DirectorySeparatorChar; expectedName = $moduleName },
@{ modulePath = (Get-Module -ListAvailable $moduleName)[0].ModuleBase + [System.IO.Path]::DirectorySeparatorChar; expectedName = $moduleName },
@{ modulePath = Join-Path -Path $TestDrive -ChildPath "\Modules\TestModule\"; expectedName = "TestModule" }
) {
param( $modulePath, $expectedName )
Expand All @@ -41,7 +41,7 @@ Describe "Import-Module" -Tags "CI" {
}

It "should be able to add a module with using ModuleInfo switch" {
$a = Get-Module -ListAvailable $moduleName
$a = (Get-Module -ListAvailable $moduleName)[0]
{ Import-Module -ModuleInfo $a } | Should -Not -Throw
(Get-Module -Name $moduleName).Name | Should -BeExactly $moduleName
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,22 +146,15 @@ function Install-TestCertificates
# PKI module is not available for PowerShell, so we need to use Windows PowerShell to import the cert
$fullPowerShell = Join-Path "$env:SystemRoot" "System32\WindowsPowerShell\v1.0\powershell.exe"

try {
$modulePathCopy = $env:PSModulePath
$env:PSModulePath = $null

$command = @"
$command = @"
Import-PfxCertificate $script:certLocation -CertStoreLocation cert:\CurrentUser\My | ForEach-Object PSPath
Import-Certificate $script:badCertLocation -CertStoreLocation Cert:\CurrentUser\My | ForEach-Object PSPath
"@
$certPaths = & $fullPowerShell -NoProfile -NonInteractive -Command $command
$certPaths.Count | Should -Be 2 | Out-Null
$certPaths = & $fullPowerShell -NoProfile -NonInteractive -Command $command
$certPaths.Count | Should -Be 2 | Out-Null

$script:importedCert = Get-ChildItem $certPaths[0]
$script:testBadCert = Get-ChildItem $certPaths[1]
} finally {
$env:PSModulePath = $modulePathCopy
}
$script:importedCert = Get-ChildItem $certPaths[0]
$script:testBadCert = Get-ChildItem $certPaths[1]
}
elseif($IsWindows)
{
Expand Down
Loading