Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/System.Management.Automation/engine/Modules/AnalysisCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ private static ConcurrentDictionary<string, CommandTypes> AnalyzeManifestModule(
var moduleManifestProperties = PsUtils.GetModuleManifestProperties(modulePath, PsUtils.FastModuleManifestAnalysisPropertyNames);
if (moduleManifestProperties != null)
{
if (!Configuration.PowerShellConfig.Instance.IsImplicitWinCompatEnabled() && ModuleIsEditionIncompatible(modulePath, moduleManifestProperties))
{
ModuleIntrinsics.Tracer.WriteLine($"Module lies on the Windows System32 legacy module path and is incompatible with current PowerShell edition, skipping module: {modulePath}");
return null;
}

Version version;
if (ModuleUtils.IsModuleInVersionSubdirectory(modulePath, out version))
{
Expand Down Expand Up @@ -485,6 +491,14 @@ internal static void CacheModuleExports(PSModuleInfo module, ExecutionContext co
{
ModuleIntrinsics.Tracer.WriteLine("Requested caching for {0}", module.Name);

// Don't cache incompatible modules on the system32 module path even if loaded with
// -SkipEditionCheck, since it will break subsequent sessions
if (!Configuration.PowerShellConfig.Instance.IsImplicitWinCompatEnabled() && !module.IsConsideredEditionCompatible)
{
ModuleIntrinsics.Tracer.WriteLine($"Module '{module.Name}' not edition compatible and not cached.");
return;
}

DateTime lastWriteTime;
ModuleCacheEntry moduleCacheEntry;
GetModuleEntryFromCache(module.Path, out lastWriteTime, out moduleCacheEntry);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2363,18 +2363,58 @@ internal PSModuleInfo LoadModuleManifest(
bool isConsideredCompatible = ModuleUtils.IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions);
if (!BaseSkipEditionCheck && !isConsideredCompatible)
{
if (importingModule)
if (PowerShellConfig.Instance.IsImplicitWinCompatEnabled())
{
IList<PSModuleInfo> moduleProxies = ImportModulesUsingWinCompat(new string [] {moduleManifestPath}, null, new ImportModuleOptions());
if (importingModule)
{
IList<PSModuleInfo> moduleProxies = ImportModulesUsingWinCompat(new string [] {moduleManifestPath}, null, new ImportModuleOptions());

// we are loading by a single ManifestPath so expect max of 1
if(moduleProxies.Count > 0)
// we are loading by a single ManifestPath so expect max of 1
if(moduleProxies.Count > 0)
{
return moduleProxies[0];
}
else
{
return null;
}
}
}
else
{
containedErrors = true;
if (writingErrors)
{
return moduleProxies[0];
message = StringUtil.Format(
Modules.ImplicitWinCompatDisabled,
moduleManifestPath,
string.Join(',', inferredCompatiblePSEditions));

ErrorRecord er = new ErrorRecord(
new InvalidOperationException(message),
nameof(Modules) + "_" + nameof(Modules.ImplicitWinCompatDisabled),
ErrorCategory.ResourceUnavailable,
moduleManifestPath);

WriteError(er);
}
else

if (bailOnFirstError)
{
return null;
// If we're trying to load the module, return null so that caches
// are not polluted
if (importingModule)
{
return null;
}

// If we return null with Get-Module, a fake module info will be created. Since
// we want to suppress output of the module, we need to do that here.
return new PSModuleInfo(moduleManifestPath, context: null, sessionState: null)
{
HadErrorsLoading = true,
IsConsideredEditionCompatible = false,
};
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/System.Management.Automation/engine/PSConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ internal sealed class PowerShellConfig
{
private const string ConfigFileName = "powershell.config.json";
private const string ExecutionPolicyDefaultShellKey = "Microsoft.PowerShell:ExecutionPolicy";
private const string DisableImplicitWinCompatKey = "DisableImplicitWinCompat";

// Provide a singleton
internal static readonly PowerShellConfig Instance = new PowerShellConfig();
Expand Down Expand Up @@ -214,6 +215,18 @@ internal void SetExperimentalFeatures(ConfigScope scope, string featureName, boo
}
}

internal bool IsImplicitWinCompatEnabled()
{
bool? settingValue = ReadValueFromFile<bool?>(ConfigScope.CurrentUser, DisableImplicitWinCompatKey);
if (!settingValue.HasValue)
{
// if the setting is not mentioned in configuration files, then the default DisableImplicitWinCompat value is False
settingValue = ReadValueFromFile<bool?>(ConfigScope.AllUsers, DisableImplicitWinCompatKey, defaultValue: false);
}

return !settingValue.Value;
}

/// <summary>
/// Corresponding settings of the original Group Policies.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/System.Management.Automation/resources/Modules.resx
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,9 @@
<data name="PSEditionNotSupported" xml:space="preserve">
<value>Module '{0}' does not support current PowerShell edition '{1}'. Its supported editions are '{2}'. Use 'Import-Module -SkipEditionCheck' to ignore the compatibility of this module.</value>
</data>
<data name="ImplicitWinCompatDisabled" xml:space="preserve">
<value>Module '{0}' supports PowerShell edition '{1}' and cannot be loaded implicitly using the Windows Compatibility feature because it is disabled in the settings file. Use 'Import-Module -UseWindowsPowerShell' to load this module with Windows PowerShell or 'Import-Module -SkipEditionCheck' to try to load the module with the current PowerShell.</value>
</data>
<data name="ExperimentalFeatureNameMissingOrEmpty" xml:space="preserve">
<value>A non-empty string value should be specified for an experimental feature declared in the module manifest.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,13 +390,26 @@ Describe "Import-Module from CompatiblePSEditions-checked paths" -Tag "CI" {
}
}

Describe "Additional tests for Import-Module with WinCompat" -Tag "CI" {
Describe "Additional tests for Import-Module with WinCompat" -Tag "Feature" {

BeforeAll {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
if ( ! $IsWindows ) {
$PSDefaultParameterValues["it:skip"] = $true
}

$ModuleName = "DesktopModule"
$ModuleName2 = "DesktopModule2"
$basePath = Join-Path $TestDrive "WinCompatModules"
Remove-Item -Path $basePath -Recurse -ErrorAction SilentlyContinue
# create an incompatible module that generates an error on import
New-EditionCompatibleModule -ModuleName $ModuleName -CompatiblePSEditions "Desktop" -Dir $basePath -ErrorGenerationCode '1/0;'
# create an incompatible module
New-EditionCompatibleModule -ModuleName $ModuleName2 -CompatiblePSEditions "Desktop" -Dir $basePath
}

AfterAll {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}

Context "Tests that ErrorAction/WarningAction have effect when Import-Module with WinCompat is used" {
Expand All @@ -408,34 +421,68 @@ Describe "Additional tests for Import-Module with WinCompat" -Tag "CI" {
Restore-ModulePath
}

It "Verify that Error is generated with default ErrorAction" -Skip:(-not $IsWindows) {
It "Verify that Error is generated with default ErrorAction" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
pwsh -NoProfile -NonInteractive -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName" *> $LogPath
$LogPath | Should -FileContentMatch 'divide by zero'
}

It "Verify that Warning is generated with default WarningAction" -Skip:(-not $IsWindows) {
It "Verify that Warning is generated with default WarningAction" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
pwsh -NoProfile -NonInteractive -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName" *> $LogPath
$LogPath | Should -FileContentMatch 'loaded in Windows PowerShell'
}

It "Verify that Error is Not generated with -ErrorAction Ignore" -Skip:(-not $IsWindows) {
It "Verify that Error is Not generated with -ErrorAction Ignore" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
pwsh -NoProfile -NonInteractive -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName -ErrorAction Ignore" *> $LogPath
$LogPath | Should -Not -FileContentMatch 'divide by zero'
}

It "Verify that Warning is Not generated with -WarningAction Ignore" -Skip:(-not $IsWindows) {
It "Verify that Warning is Not generated with -WarningAction Ignore" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
pwsh -NoProfile -NonInteractive -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName -WarningAction Ignore" *> $LogPath
$LogPath | Should -Not -FileContentMatch 'loaded in Windows PowerShell'
}

It "Fails to import incompatible module if implicit WinCompat is disabled in config" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
$ConfigPath = Join-Path $TestDrive 'powershell.config.json'
'{"DisableImplicitWinCompat" : "True"}' | Out-File -Force $ConfigPath
pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`');Import-Module $ModuleName2" *> $LogPath
$LogPath | Should -FileContentMatch 'cannot be loaded implicitly using the Windows Compatibility'
}

It "Fails to auto-import incompatible module during CommandDiscovery\ModuleAutoload if implicit WinCompat is Disabled in config" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
$ConfigPath = Join-Path $TestDrive 'powershell.config.json'
'{"DisableImplicitWinCompat" : "True"}' | Out-File -Force $ConfigPath
pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`'); Test-$ModuleName2" *> $LogPath
$LogPath | Should -FileContentMatch 'not recognized as the name of a cmdlet'
}

It "Successfully auto-imports incompatible module during CommandDiscovery\ModuleAutoload if implicit WinCompat is Enabled in config" {
$LogPath = Join-Path $TestDrive (New-Guid).ToString()
$ConfigPath = Join-Path $TestDrive 'powershell.config.json'
'{"DisableImplicitWinCompat" : "False"}' | Out-File -Force $ConfigPath
pwsh -NoProfile -NonInteractive -settingsFile $ConfigPath -c "[System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('TestWindowsPowerShellPSHomeLocation', `'$basePath`'); Test-$ModuleName2" *> $LogPath
$LogPath | Should -FileContentMatch 'True'
}
}
}

Describe "PSModulePath changes interacting with other PowerShell processes" -Tag "Feature" {
$PSDefaultParameterValues = @{ 'It:Skip' = (-not $IsWindows) }

BeforeAll {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
if ( ! $IsWindows ) {
$PSDefaultParameterValues["it:skip"] = $true
}
}

AfterAll {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}

Context "System32 module path prepended to PSModulePath" {
BeforeAll {
Expand Down