Skip to content

Commit b218e6f

Browse files
Andrewdaxian-dbw
authored andcommitted
Support using non-compatible Windows PowerShell modules in PowerShell Core (#10973)
1 parent 6dda230 commit b218e6f

21 files changed

Lines changed: 466 additions & 75 deletions

File tree

src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,12 @@ static ExperimentalFeature()
134134
#endif
135135
new ExperimentalFeature(
136136
name: "PSNullConditionalOperators",
137-
description: "Support the null conditional member access operators in PowerShell language")
137+
description: "Support the null conditional member access operators in PowerShell language"),
138+
#if !UNIX
139+
new ExperimentalFeature(
140+
name: "PSWindowsPowerShellCompatibility",
141+
description: "Load non-PSCore-compartible modules into Windows PowerShell over PS Remoting")
142+
#endif
138143
};
139144
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);
140145

src/System.Management.Automation/engine/Modules/AnalysisCache.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ private static ConcurrentDictionary<string, CommandTypes> AnalyzeManifestModule(
103103
var moduleManifestProperties = PsUtils.GetModuleManifestProperties(modulePath, PsUtils.FastModuleManifestAnalysisPropertyNames);
104104
if (moduleManifestProperties != null)
105105
{
106-
if (ModuleIsEditionIncompatible(modulePath, moduleManifestProperties))
106+
if (!ExperimentalFeature.IsEnabled("PSWindowsPowerShellCompatibility") && ModuleIsEditionIncompatible(modulePath, moduleManifestProperties))
107107
{
108108
ModuleIntrinsics.Tracer.WriteLine($"Module lies on the Windows System32 legacy module path and is incompatible with current PowerShell edition, skipping module: {modulePath}");
109109
return null;
@@ -493,7 +493,7 @@ internal static void CacheModuleExports(PSModuleInfo module, ExecutionContext co
493493

494494
// Don't cache incompatible modules on the system32 module path even if loaded with
495495
// -SkipEditionCheck, since it will break subsequent sessions.
496-
if (!module.IsConsideredEditionCompatible)
496+
if (!ExperimentalFeature.IsEnabled("PSWindowsPowerShellCompatibility") && !module.IsConsideredEditionCompatible)
497497
{
498498
ModuleIntrinsics.Tracer.WriteLine($"Module '{module.Name}' not edition compatible and not cached.");
499499
return;

src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ public sealed class ImportModuleCommand : ModuleCmdletBase, IDisposable
5151
private const string ParameterSet_ViaPsrpSession = "PSSession";
5252
private const string ParameterSet_ViaCimSession = "CimSession";
5353
private const string ParameterSet_FQName_ViaPsrpSession = "FullyQualifiedNameAndPSSession";
54+
private const string ParameterSet_ViaWinCompat = "WinCompat";
55+
private const string ParameterSet_FQName_ViaWinCompat = "FullyQualifiedNameAndWinCompat";
56+
5457

5558
/// <summary>
5659
/// This parameter specifies whether to import to the current session state
@@ -82,6 +85,7 @@ public string Prefix
8285
[Parameter(ParameterSetName = ParameterSet_Name, Mandatory = true, ValueFromPipeline = true, Position = 0)]
8386
[Parameter(ParameterSetName = ParameterSet_ViaPsrpSession, Mandatory = true, ValueFromPipeline = true, Position = 0)]
8487
[Parameter(ParameterSetName = ParameterSet_ViaCimSession, Mandatory = true, ValueFromPipeline = true, Position = 0)]
88+
[Parameter("PSWindowsPowerShellCompatibility", ExperimentAction.Show, ParameterSetName = ParameterSet_ViaWinCompat, Mandatory = true, ValueFromPipeline = true, Position = 0)]
8589
[ValidateTrustedData]
8690
[SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Cmdlets use arrays for parameters.")]
8791
public string[] Name { set; get; } = Array.Empty<string>();
@@ -91,6 +95,7 @@ public string Prefix
9195
/// </summary>
9296
[Parameter(ParameterSetName = ParameterSet_FQName, Mandatory = true, ValueFromPipeline = true, Position = 0)]
9397
[Parameter(ParameterSetName = ParameterSet_FQName_ViaPsrpSession, Mandatory = true, ValueFromPipeline = true, Position = 0)]
98+
[Parameter("PSWindowsPowerShellCompatibility", ExperimentAction.Show, ParameterSetName = ParameterSet_FQName_ViaWinCompat, Mandatory = true, ValueFromPipeline = true, Position = 0)]
9499
[ValidateTrustedData]
95100
[SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Cmdlets use arrays for parameters.")]
96101
public ModuleSpecification[] FullyQualifiedName { get; set; }
@@ -226,8 +231,15 @@ public SwitchParameter Force
226231

227232
/// <summary>
228233
/// Skips the check on CompatiblePSEditions for modules loaded from the System32 module path.
234+
/// This is mutually exclusive with UseWindowsPowerShell parameter.
229235
/// </summary>
230-
[Parameter]
236+
[Parameter(ParameterSetName = ParameterSet_Name)]
237+
[Parameter(ParameterSetName = ParameterSet_FQName)]
238+
[Parameter(ParameterSetName = ParameterSet_ModuleInfo)]
239+
[Parameter(ParameterSetName = ParameterSet_Assembly)]
240+
[Parameter(ParameterSetName = ParameterSet_ViaPsrpSession)]
241+
[Parameter(ParameterSetName = ParameterSet_ViaCimSession)]
242+
[Parameter(ParameterSetName = ParameterSet_FQName_ViaPsrpSession)]
231243
public SwitchParameter SkipEditionCheck
232244
{
233245
get { return (SwitchParameter)BaseSkipEditionCheck; }
@@ -263,6 +275,7 @@ public SwitchParameter AsCustomObject
263275
[Parameter(ParameterSetName = ParameterSet_Name)]
264276
[Parameter(ParameterSetName = ParameterSet_ViaPsrpSession)]
265277
[Parameter(ParameterSetName = ParameterSet_ViaCimSession)]
278+
[Parameter("PSWindowsPowerShellCompatibility", ExperimentAction.Show, ParameterSetName = ParameterSet_ViaWinCompat)]
266279
[Alias("Version")]
267280
public Version MinimumVersion
268281
{
@@ -277,6 +290,7 @@ public Version MinimumVersion
277290
[Parameter(ParameterSetName = ParameterSet_Name)]
278291
[Parameter(ParameterSetName = ParameterSet_ViaPsrpSession)]
279292
[Parameter(ParameterSetName = ParameterSet_ViaCimSession)]
293+
[Parameter("PSWindowsPowerShellCompatibility", ExperimentAction.Show, ParameterSetName = ParameterSet_ViaWinCompat)]
280294
public string MaximumVersion
281295
{
282296
get
@@ -306,6 +320,7 @@ public string MaximumVersion
306320
[Parameter(ParameterSetName = ParameterSet_Name)]
307321
[Parameter(ParameterSetName = ParameterSet_ViaPsrpSession)]
308322
[Parameter(ParameterSetName = ParameterSet_ViaCimSession)]
323+
[Parameter("PSWindowsPowerShellCompatibility", ExperimentAction.Show, ParameterSetName = ParameterSet_ViaWinCompat)]
309324
public Version RequiredVersion
310325
{
311326
get { return BaseRequiredVersion; }
@@ -406,6 +421,15 @@ public ImportModuleCommand()
406421
[ValidateNotNullOrEmpty]
407422
public string CimNamespace { get; set; }
408423

424+
/// <summary>
425+
/// This parameter causes a module to be loaded into Windows PowerShell.
426+
/// This is mutually exclusive with SkipEditionCheck parameter.
427+
/// </summary>
428+
[Parameter("PSWindowsPowerShellCompatibility", ExperimentAction.Show, ParameterSetName = ParameterSet_ViaWinCompat, Mandatory = true)]
429+
[Parameter("PSWindowsPowerShellCompatibility", ExperimentAction.Show, ParameterSetName = ParameterSet_FQName_ViaWinCompat, Mandatory = true)]
430+
[Alias("UseWinPS")]
431+
public SwitchParameter UseWindowsPowerShell { get; set; }
432+
409433
#endregion Cmdlet parameters
410434

411435
#region Local import
@@ -805,7 +829,8 @@ private IList<PSModuleInfo> ImportModule_RemotelyViaPsrpSession(
805829
ImportModuleOptions importModuleOptions,
806830
IEnumerable<string> moduleNames,
807831
IEnumerable<ModuleSpecification> fullyQualifiedNames,
808-
PSSession psSession)
832+
PSSession psSession,
833+
bool usingWinCompat = false)
809834
{
810835
var remotelyImportedModules = new List<PSModuleInfo>();
811836
if (moduleNames != null)
@@ -829,7 +854,7 @@ private IList<PSModuleInfo> ImportModule_RemotelyViaPsrpSession(
829854
// Send telemetry on the imported modules
830855
foreach (PSModuleInfo moduleInfo in remotelyImportedModules)
831856
{
832-
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, moduleInfo.Name);
857+
ApplicationInsightsTelemetry.SendTelemetryMetric(usingWinCompat ? TelemetryType.WinCompatModuleLoad : TelemetryType.ModuleLoad, moduleInfo.Name);
833858
}
834859

835860
return remotelyImportedModules;
@@ -1795,11 +1820,15 @@ protected override void ProcessRecord()
17951820
{
17961821
SetModuleBaseForEngineModules(foundModule.Name, this.Context);
17971822

1798-
// Telemetry here - report module load
1799-
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name);
1823+
// report loading of the module in telemetry
1824+
// avoid double reporting for WinCompat modules that go through CommandDiscovery\AutoloadSpecifiedModule
1825+
if (!foundModule.IsWindowsPowerShellCompatModule)
1826+
{
1827+
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name);
18001828
#if LEGACYTELEMETRY
1801-
TelemetryAPI.ReportModuleLoad(foundModule);
1829+
TelemetryAPI.ReportModuleLoad(foundModule);
18021830
#endif
1831+
}
18031832
}
18041833
}
18051834
}
@@ -1836,6 +1865,25 @@ protected override void ProcessRecord()
18361865
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, modulespec.Name);
18371866
}
18381867
}
1868+
else if (this.ParameterSetName.Equals(ParameterSet_ViaWinCompat, StringComparison.OrdinalIgnoreCase)
1869+
|| this.ParameterSetName.Equals(ParameterSet_FQName_ViaWinCompat, StringComparison.OrdinalIgnoreCase))
1870+
{
1871+
if (this.UseWindowsPowerShell)
1872+
{
1873+
var WindowsPowerShellCompatRemotingSession = CreateWindowsPowerShellCompatResources();
1874+
if (WindowsPowerShellCompatRemotingSession != null)
1875+
{
1876+
foreach(PSModuleInfo moduleProxy in ImportModule_RemotelyViaPsrpSession(importModuleOptions, this.Name, this.FullyQualifiedName, WindowsPowerShellCompatRemotingSession, true))
1877+
{
1878+
moduleProxy.IsWindowsPowerShellCompatModule = true;
1879+
System.Threading.Interlocked.Increment(ref s_WindowsPowerShellCompatUsageCounter);
1880+
1881+
string message = StringUtil.Format(Modules.WinCompatModuleWarning, moduleProxy.Name, WindowsPowerShellCompatRemotingSession.Name);
1882+
WriteWarning(message);
1883+
}
1884+
}
1885+
}
1886+
}
18391887
else
18401888
{
18411889
Dbg.Assert(false, "Unrecognized parameter set");

src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs

Lines changed: 132 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System.Reflection;
1919
using System.Text;
2020
using System.Xml;
21+
using System.Diagnostics;
2122

2223
using Microsoft.PowerShell.Cmdletization;
2324

@@ -283,6 +284,21 @@ internal List<WildcardPattern> MatchAll
283284
"Desktop"
284285
};
285286

287+
/// <summary>
288+
/// A counter for modules that are loaded using WindowsPS compat session.
289+
/// </summary>
290+
internal static int s_WindowsPowerShellCompatUsageCounter = 0;
291+
292+
/// <summary>
293+
/// Session name for WindowsPS compat remoting session.
294+
/// </summary>
295+
internal const string WindowsPowerShellCompatRemotingSessionName = "WinPSCompatSession";
296+
297+
/// <summary>
298+
/// Synchronization object for creation/cleanup of WindowsPS compat remoting session.
299+
/// </summary>
300+
internal static object s_WindowsPowerShellCompatSyncObject = new object();
301+
286302
private Dictionary<string, PSModuleInfo> _currentlyProcessingModules = new Dictionary<string, PSModuleInfo>();
287303

288304
internal bool LoadUsingModulePath(bool found, IEnumerable<string> modulePath, string name, SessionState ss,
@@ -2347,43 +2363,70 @@ internal PSModuleInfo LoadModuleManifest(
23472363
bool isConsideredCompatible = ModuleUtils.IsPSEditionCompatible(moduleManifestPath, inferredCompatiblePSEditions);
23482364
if (!BaseSkipEditionCheck && !isConsideredCompatible)
23492365
{
2350-
containedErrors = true;
2351-
2352-
if (writingErrors)
2366+
if (ExperimentalFeature.IsEnabled("PSWindowsPowerShellCompatibility"))
23532367
{
2354-
message = StringUtil.Format(
2355-
Modules.PSEditionNotSupported,
2356-
moduleManifestPath,
2357-
PSVersionInfo.PSEdition,
2358-
string.Join(',', inferredCompatiblePSEditions));
2368+
if (importingModule)
2369+
{
2370+
var commandInfo = new CmdletInfo("Import-Module", typeof(ImportModuleCommand));
2371+
using var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);
2372+
ps.AddCommand(commandInfo);
2373+
ps.AddParameter("PassThru", true);
2374+
ps.AddParameter("Name", moduleManifestPath);
2375+
ps.AddParameter("UseWindowsPowerShell", true);
23592376

2360-
InvalidOperationException ioe = new InvalidOperationException(message);
2377+
var moduleProxies = ps.Invoke<PSModuleInfo>();
23612378

2362-
ErrorRecord er = new ErrorRecord(
2363-
ioe,
2364-
nameof(Modules) + "_" + nameof(Modules.PSEditionNotSupported),
2365-
ErrorCategory.ResourceUnavailable,
2366-
moduleManifestPath);
2367-
2368-
WriteError(er);
2379+
// we are loading by a single ManifestPath so expect max of 1
2380+
if(moduleProxies.Count > 0)
2381+
{
2382+
return moduleProxies[0];
2383+
}
2384+
else
2385+
{
2386+
return null;
2387+
}
2388+
}
23692389
}
2370-
2371-
if (bailOnFirstError)
2390+
else
23722391
{
2373-
// If we're trying to load the module, return null so that caches
2374-
// are not polluted
2375-
if (importingModule)
2392+
containedErrors = true;
2393+
2394+
if (writingErrors)
23762395
{
2377-
return null;
2396+
message = StringUtil.Format(
2397+
Modules.PSEditionNotSupported,
2398+
moduleManifestPath,
2399+
PSVersionInfo.PSEdition,
2400+
string.Join(',', inferredCompatiblePSEditions));
2401+
2402+
InvalidOperationException ioe = new InvalidOperationException(message);
2403+
2404+
ErrorRecord er = new ErrorRecord(
2405+
ioe,
2406+
nameof(Modules) + "_" + nameof(Modules.PSEditionNotSupported),
2407+
ErrorCategory.ResourceUnavailable,
2408+
moduleManifestPath);
2409+
2410+
WriteError(er);
23782411
}
23792412

2380-
// If we return null with Get-Module, a fake module info will be created. Since
2381-
// we want to suppress output of the module, we need to do that here.
2382-
return new PSModuleInfo(moduleManifestPath, context: null, sessionState: null)
2413+
if (bailOnFirstError)
23832414
{
2384-
HadErrorsLoading = true,
2385-
IsConsideredEditionCompatible = false,
2386-
};
2415+
// If we're trying to load the module, return null so that caches
2416+
// are not polluted
2417+
if (importingModule)
2418+
{
2419+
return null;
2420+
}
2421+
2422+
// If we return null with Get-Module, a fake module info will be created. Since
2423+
// we want to suppress output of the module, we need to do that here.
2424+
return new PSModuleInfo(moduleManifestPath, context: null, sessionState: null)
2425+
{
2426+
HadErrorsLoading = true,
2427+
IsConsideredEditionCompatible = false,
2428+
};
2429+
}
23872430
}
23882431
}
23892432

@@ -4768,6 +4811,62 @@ internal static Collection<string> GetResolvedPathCollection(string filePath, Ex
47684811
return filePaths;
47694812
}
47704813

4814+
internal PSSession GetWindowsPowerShellCompatRemotingSession()
4815+
{
4816+
PSSession result = null;
4817+
var commandInfo = new CmdletInfo("Get-PSSession", typeof(GetPSSessionCommand));
4818+
using var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);
4819+
ps.AddCommand(commandInfo);
4820+
ps.AddParameter("Name", WindowsPowerShellCompatRemotingSessionName);
4821+
var results = ps.Invoke<PSSession>();
4822+
if (results.Count > 0)
4823+
{
4824+
result = results[0];
4825+
}
4826+
return result;
4827+
}
4828+
4829+
internal PSSession CreateWindowsPowerShellCompatResources()
4830+
{
4831+
PSSession compatSession = null;
4832+
lock(s_WindowsPowerShellCompatSyncObject)
4833+
{
4834+
compatSession = GetWindowsPowerShellCompatRemotingSession();
4835+
if (compatSession == null)
4836+
{
4837+
var commandInfo = new CmdletInfo("New-PSSession", typeof(NewPSSessionCommand));
4838+
using var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);
4839+
ps.AddCommand(commandInfo);
4840+
ps.AddParameter("UseWindowsPowerShell", true);
4841+
ps.AddParameter("Name", WindowsPowerShellCompatRemotingSessionName);
4842+
var results = ps.Invoke<PSSession>();
4843+
if (results.Count > 0)
4844+
{
4845+
compatSession = results[0];
4846+
System.Threading.Interlocked.Exchange(ref s_WindowsPowerShellCompatUsageCounter, 0);
4847+
}
4848+
}
4849+
}
4850+
4851+
return compatSession;
4852+
}
4853+
4854+
internal void CleanupWindowsPowerShellCompatResources()
4855+
{
4856+
lock(s_WindowsPowerShellCompatSyncObject)
4857+
{
4858+
var compatSession = GetWindowsPowerShellCompatRemotingSession();
4859+
if (compatSession != null)
4860+
{
4861+
var commandInfo = new CmdletInfo("Remove-PSSession", typeof(RemovePSSessionCommand));
4862+
using var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);
4863+
ps.AddCommand(commandInfo);
4864+
ps.AddParameter("Session", compatSession);
4865+
ps.Invoke();
4866+
}
4867+
}
4868+
}
4869+
47714870
private void RemoveTypesAndFormatting(
47724871
IList<string> formatFilesToRemove,
47734872
IList<string> typeFilesToRemove)
@@ -4860,6 +4959,11 @@ internal void RemoveModule(PSModuleInfo module, string moduleNameInRemoveModuleC
48604959
}
48614960
}
48624961

4962+
if (ExperimentalFeature.IsEnabled("PSWindowsPowerShellCompatibility") && module.IsWindowsPowerShellCompatModule && (System.Threading.Interlocked.Decrement(ref s_WindowsPowerShellCompatUsageCounter) == 0))
4963+
{
4964+
CleanupWindowsPowerShellCompatResources();
4965+
}
4966+
48634967
// First remove cmdlets from the session state
48644968
// (can't just go through module.ExportedCmdlets
48654969
// because the names of the cmdlets might have been changed by the -Prefix parameter of Import-Module)

0 commit comments

Comments
 (0)