From 79b8140199ad5b5439a4e7ce339f4e4ba3a7058d Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 22 Sep 2022 10:49:08 -0700 Subject: [PATCH] Make PowerShell class not affiliate with `Runspace` when declaring the `NoRunspaceAffinity` attribute (#18138) --- .../engine/parser/PSType.cs | 14 +++++ .../engine/parser/TypeResolver.cs | 1 + .../engine/runtime/Operations/ClassOps.cs | 2 +- .../engine/runtime/Operations/MiscOps.cs | 9 +++- ...pting.Classes.NoRunspaceAffinity.Tests.ps1 | 52 +++++++++++++++++++ .../Language/Parser/TypeAccelerator.Tests.ps1 | 8 ++- 6 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 test/powershell/Language/Classes/Scripting.Classes.NoRunspaceAffinity.Tests.ps1 diff --git a/src/System.Management.Automation/engine/parser/PSType.cs b/src/System.Management.Automation/engine/parser/PSType.cs index 1f961af8d1..7f35927af2 100644 --- a/src/System.Management.Automation/engine/parser/PSType.cs +++ b/src/System.Management.Automation/engine/parser/PSType.cs @@ -1472,4 +1472,18 @@ namespace System.Management.Automation.Language } } } + + /// + /// The attribute for a PowerShell class to not affiliate with a particular Runspace\SessionState. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class NoRunspaceAffinityAttribute : ParsingBaseAttribute + { + /// + /// Initializes a new instance of the attribute. + /// + public NoRunspaceAffinityAttribute() + { + } + } } diff --git a/src/System.Management.Automation/engine/parser/TypeResolver.cs b/src/System.Management.Automation/engine/parser/TypeResolver.cs index 8a12aefe15..1b5736df0a 100644 --- a/src/System.Management.Automation/engine/parser/TypeResolver.cs +++ b/src/System.Management.Automation/engine/parser/TypeResolver.cs @@ -750,6 +750,7 @@ namespace System.Management.Automation { typeof(CimConverter), new[] { "cimconverter" } }, { typeof(ModuleSpecification), null }, { typeof(IPEndPoint), new[] { "IPEndpoint" } }, + { typeof(NoRunspaceAffinityAttribute), new[] { "NoRunspaceAffinity" } }, { typeof(NullString), new[] { "NullString" } }, { typeof(OutputTypeAttribute), new[] { "OutputType" } }, { typeof(object[]), null }, diff --git a/src/System.Management.Automation/engine/runtime/Operations/ClassOps.cs b/src/System.Management.Automation/engine/runtime/Operations/ClassOps.cs index d065171988..fdbb91ec40 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/ClassOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/ClassOps.cs @@ -22,7 +22,7 @@ namespace System.Management.Automation.Internal /// /// Every Runspace in one process contains SessionStateInternal per module (module SessionState). /// Every RuntimeType is associated to only one SessionState in the Runspace, which creates it: - /// it's ever global state or a module state. + /// it's either global state or a module state. /// In the former case, module can be imported from the different runspaces in the same process. /// And so runspaces will share RuntimeType. But in every runspace, Type is associated with just one SessionState. /// We want type methods to be able access $script: variables and module-specific methods. diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs index 1c15c6510b..bd1eb62f19 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs @@ -2300,8 +2300,13 @@ namespace System.Management.Automation Diagnostics.Assert(t.Type != null, "TypeDefinitionAst.Type cannot be null"); if (t.IsClass) { - var helperType = - t.Type.Assembly.GetType(t.Type.FullName + "_"); + if (t.Type.IsDefined(typeof(NoRunspaceAffinityAttribute), inherit: true)) + { + // Skip the initialization for session state affinity. + continue; + } + + var helperType = t.Type.Assembly.GetType(t.Type.FullName + "_"); Diagnostics.Assert(helperType != null, "no corresponding " + t.Type.FullName + "_ type found"); foreach (var p in helperType.GetFields(BindingFlags.Static | BindingFlags.NonPublic)) { diff --git a/test/powershell/Language/Classes/Scripting.Classes.NoRunspaceAffinity.Tests.ps1 b/test/powershell/Language/Classes/Scripting.Classes.NoRunspaceAffinity.Tests.ps1 new file mode 100644 index 0000000000..bc08db2a06 --- /dev/null +++ b/test/powershell/Language/Classes/Scripting.Classes.NoRunspaceAffinity.Tests.ps1 @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe "Class can be defined without Runspace affinity" -Tags "CI" { + + It "Applying the 'NoRunspaceAffinity' attribute make the class not affiliate with a particular Runspace/SessionState" { + [NoRunspaceAffinity()] + class NoAffinity { + [string] $Name; + [int] $RunspaceId; + + NoAffinity() { + $this.RunspaceId = [runspace]::DefaultRunspace.Id + } + + static [int] Echo() { + return [runspace]::DefaultRunspace.Id + } + + [int] SetAndEcho([string] $value) { + $this.Name = $value + return [runspace]::DefaultRunspace.Id + } + } + + $t = [NoAffinity] + $o = [NoAffinity]::new() + + ## Running directly should use the current Runspace/SessionState. + $t::Echo() | Should -Be $Host.Runspace.Id + $o.RunspaceId | Should -Be $Host.Runspace.Id + $o.SetAndEcho('Blue') | Should -Be $Host.Runspace.Id + $o.Name | Should -Be 'Blue' + + ## Running in a new Runspace should use that Runspace and its current SessionState. + try { + $ps = [powershell]::Create() + $ps.AddScript('function CallEcho($type) { $type::Echo() }').Invoke() > $null; $ps.Commands.Clear() + $ps.AddScript('function CallSetAndEcho($obj) { $obj.SetAndEcho(''Hello world'') }').Invoke() > $null; $ps.Commands.Clear() + $ps.AddScript('function GetName($obj) { $obj.Name }').Invoke() > $null; $ps.Commands.Clear() + $ps.AddScript('function NewObj($type) { $type::new().RunspaceId }').Invoke() > $null; $ps.Commands.Clear() + + $ps.AddCommand('CallEcho').AddArgument($t).Invoke() | Should -Be $ps.Runspace.Id; $ps.Commands.Clear() + $ps.AddCommand('CallSetAndEcho').AddArgument($o).Invoke() | Should -Be $ps.Runspace.Id; $ps.Commands.Clear() + $ps.AddCommand('GetName').AddArgument($o).Invoke() | Should -Be 'Hello world'; $ps.Commands.Clear() + $ps.AddCommand('NewObj').AddArgument($t).Invoke() | Should -Be $ps.Runspace.Id; $ps.Commands.Clear() + } + finally { + $ps.Dispose() + } + } +} diff --git a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 index 018f5af55c..99925600f6 100644 --- a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 +++ b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 @@ -410,15 +410,19 @@ Describe "Type accelerators" -Tags "CI" { Accelerator = 'ordered' Type = [System.Collections.Specialized.OrderedDictionary] } + @{ + Accelerator = 'NoRunspaceAffinity' + Type = [System.Management.Automation.Language.NoRunspaceAffinityAttribute] + } ) if ( !$IsWindows ) { - $totalAccelerators = 101 + $totalAccelerators = 102 } else { - $totalAccelerators = 106 + $totalAccelerators = 107 $extraFullPSAcceleratorTestCases = @( @{