Allow pwsh to inherit $env:PSModulePath and enable powershell.exe to start correctly (#11057)

This commit is contained in:
Steve Lee
2019-11-15 17:29:04 -08:00
committed by Dongbo Wang
parent fe712f8de2
commit 2e553038c5
6 changed files with 118 additions and 164 deletions
@@ -12,6 +12,7 @@ using System.Text;
using System.Threading;
using Microsoft.PowerShell.Commands;
using Microsoft.Win32;
using Dbg = System.Management.Automation.Diagnostics;
@@ -1162,92 +1163,6 @@ namespace System.Management.Automation
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
@@ -1259,16 +1174,6 @@ namespace System.Management.Automation
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
{
@@ -1291,39 +1196,30 @@ namespace System.Management.Automation
{
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;
}
@@ -1338,6 +1234,45 @@ namespace System.Management.Automation
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
@@ -1353,17 +1288,6 @@ namespace System.Management.Automation
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);
}
@@ -409,6 +409,19 @@ namespace System.Management.Automation
// 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();
@@ -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) {
@@ -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 )
@@ -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
}
@@ -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)
{
@@ -3,7 +3,6 @@
Describe "SxS Module Path Basic Tests" -tags "CI" {
BeforeAll {
if ($IsWindows)
{
$powershell = "$PSHOME\pwsh.exe"
@@ -14,13 +13,22 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
}
$expectedUserPath = Join-Path -Path $HOME -ChildPath "Documents\$ProductName\Modules"
$expectedSharedPath = Join-Path -Path $env:ProgramFiles -ChildPath "$ProductName\Modules"
$userConfigPath = "~/Documents/powershell/powershell.config.json"
}
else
{
$powershell = "$PSHOME/pwsh"
$expectedUserPath = [System.Management.Automation.Platform]::SelectProductNameForDirectory("USER_MODULES")
$expectedSharedPath = [System.Management.Automation.Platform]::SelectProductNameForDirectory("SHARED_MODULES")
$userConfigPath = "~/.config/powershell/powershell.config.json"
}
$userConfigExists = $false
if (Test-Path $userConfigPath) {
$userConfigExists = $true
Copy-Item $userConfigPath "$userConfigPath.backup" -Force -ErrorAction Ignore
}
$expectedSystemPath = Join-Path -Path $PSHOME -ChildPath 'Modules'
# Skip these tests in cases when there is no 'pwsh' executable (e.g. when framework dependent PS package is used)
@@ -41,6 +49,15 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
New-Item -Path $fakePSHomeModuleDir -ItemType Directory > $null
}
AfterAll {
if ($userConfigExists) {
Move-Item "$userConfigPath.backup" $userConfigPath -Force -ErrorAction Ignore
}
else {
Remove-Item "$userConfigPath" -Force -ErrorAction Ignore
}
}
BeforeEach {
$originalModulePath = $env:PSModulePath
}
@@ -74,7 +91,7 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
}
}
It "ignore pshome module path derived from a different PowerShell instance" -Skip:(!$IsCoreCLR -or $skipNoPwsh) {
It "Works with pshome module path derived from a different PowerShell instance" -Skip:(!$IsCoreCLR -or $skipNoPwsh) {
## Create 'powershell' and 'pwsh.deps.json' in the fake PSHome folder,
## so that the module path calculation logic would believe it's real.
@@ -88,21 +105,15 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
$newModulePath = & $powershell -nopro -c '$env:PSModulePath'
$paths = $newModulePath -split [System.IO.Path]::PathSeparator
if ($IsWindows)
{
$paths.Count | Should -Be 4
}
else
{
$paths.Count | Should -Be 3
}
$paths.Count | Should -Be 4
$paths[0] | Should -Be $expectedUserPath
$paths[1] | Should -Be $expectedSharedPath
$paths[2] | Should -Be $expectedSystemPath
$paths[3] | Should -Be $fakePSHomeModuleDir
if ($IsWindows)
{
$paths[3].TrimEnd([System.IO.Path]::DirectorySeparatorChar) | Should -Be $expectedWindowsPowerShellPSHomePath
$expectedWindowsPowerShellPSHomePath | Should -Not -BeIn $paths
}
} finally {
@@ -120,15 +131,7 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
$env:PSModulePath = $fakePSHomeModuleDir, $customeModules -join ([System.IO.Path]::PathSeparator)
$newModulePath = & $powershell -nopro -c '$env:PSModulePath'
$paths = $newModulePath -split [System.IO.Path]::PathSeparator
if ($IsWindows)
{
$paths.Count | Should -Be 6
}
else
{
$paths.Count | Should -Be 5
}
$paths.Count | Should -Be 5
$paths -contains $fakePSHomeModuleDir | Should -BeTrue
$paths -contains $customeModules | Should -BeTrue
}
@@ -162,4 +165,24 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
$wpshIndex | Should -Not -Be -1
$pwshIndex | Should -BeLessThan $wpshIndex
}
It 'Windows PowerShell does not inherit PowerShell paths' -Skip:(!$IsWindows) {
$out = powershell.exe -noprofile -command '$env:PSModulePath'
$out | Should -Not -Contain $expectedUserPath
$out | Should -Not -Contain $expectedSharedPath
$out | Should -Not -Contain $expectedSystemPath
}
It 'Windows PowerShell inherits user added paths' -Skip:(!$IsWindows) {
$env:PSModulePath += ";myPath"
$out = powershell.exe -noprofile -command '$env:PSModulePath'
$out | Should -BeLike '*;myPath'
}
It 'Windows PowerShell does not inherit path defined in powershell.config.json' -Skip:(!$IsWindows) {
$userConfig = '{ "PSModulePath": "myUserPath" }'
Set-Content -Path $userConfigPath -Value $userConfig -Force
$out = pwsh -noprofile -command 'powershell.exe -noprofile -command $env:PSModulePath'
$out | Should -Not -BeLike 'myUserPath;*'
}
}