MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections.Generic;5using System.Diagnostics.CodeAnalysis;6using System.Linq;7using System.Management.Automation.Language;8using System.Management.Automation.Runspaces;9using System.Reflection;10using System.Reflection.Emit;11using System.Runtime.CompilerServices;12using System.Threading;13 14// These APIs are not part of the public contract.15// They are implementation details and intended to be called from generated assemblies for PS classes.16//17// Because they are called from other assemblies, we have to make them public.18// We put them in Internal namespace to emphasise that despite the fact that they are public, it's not part of API contract.19 20namespace System.Management.Automation.Internal21{22 /// <summary>23 /// Every Runspace in one process contains SessionStateInternal per module (module SessionState).24 /// Every RuntimeType is associated to only one SessionState in the Runspace, which creates it:25 /// it's either global state or a module state.26 /// In the former case, module can be imported from the different runspaces in the same process.27 /// And so runspaces will share RuntimeType. But in every runspace, Type is associated with just one SessionState.28 /// We want type methods to be able access $script: variables and module-specific methods.29 /// To achieve it, we preserve reference to SessionState that creates type in the private field 'SessionStateFieldName'.30 /// Later, we use it to call scriptBlocks captured in ScriptBlockMemberMethodWrapper with the right sessionState.31 /// </summary>32 public class SessionStateKeeper33 {34 // We use ConditionalWeakTable, because if GC already collect Runspace, then there35 // is no way to call a ctor or a static method on the type in this Runspace.36 private readonly ConditionalWeakTable<Runspace, SessionStateInternal> _stateMap;37 38 internal SessionStateKeeper()39 {40 _stateMap = new ConditionalWeakTable<Runspace, SessionStateInternal>();41 }42 43 internal void RegisterRunspace()44 {45 SessionStateInternal sessionStateInMap = null;46 Runspace runspaceToUse = Runspace.DefaultRunspace;47 SessionStateInternal sessionStateToUse = runspaceToUse.ExecutionContext.EngineSessionState;48 49 // Different threads will operate on different key/value pairs (default-runspace/session-state pairs),50 // and a ConditionalWeakTable itself is thread safe, so there won't be race condition here.51 if (!_stateMap.TryGetValue(runspaceToUse, out sessionStateInMap))52 {53 // If the key doesn't exist yet, add it54 _stateMap.Add(runspaceToUse, sessionStateToUse);55 }56 else if (sessionStateInMap != sessionStateToUse)57 {58 // If the key exists but the corresponding value is not what we should use, then remove the key/value pair and add the new pair.59 // This could happen when a powershell class is defined in a module and the module gets reloaded. In such case, the same TypeDefinitionAst60 // instance will get reused, but should be associated with the SessionState from the new module, instead of the one from the old module.61 _stateMap.AddOrUpdate(runspaceToUse, sessionStateToUse);62 }63 // If the key exists and the corresponding value is the one we should use, then do nothing.64 }65 66 /// <summary>67 /// This method should be called only from68 /// - generated ctors for PowerShell classes, AND69 /// - ScriptBlockMemberMethodWrapper when invoking static methods of PowerShell classes.70 /// It's not intended to be a public API, but because we generate type in a different assembly it has to be public.71 /// Return type should be SessionStateInternal, but it violates accessibility consistency, so we use object.72 /// </summary>73 /// <remarks>74 /// By default, PowerShell class instantiation usually happens in the same Runspace where the class is defined. In75 /// that case, the created instance will be bound to the session state used to define that class in the Runspace.76 /// However, if the instantiation happens in a different Runspace where the class is not defined, or it happens on77 /// a thread without a default Runspace, then the created instance won't be bound to any session state.78 /// </remarks>79 /// <returns>SessionStateInternal.</returns>80 public object GetSessionState()81 {82 SessionStateInternal ss = null;83 84 // DefaultRunspace could be null when we reach here. For example, create instance of85 // a PowerShell class by using reflection on a thread without DefaultRunspace.86 // Make sure we call 'TryGetValue' with a non-null key, otherwise ArgumentNullException will be thrown.87 Runspace defaultRunspace = Runspace.DefaultRunspace;88 if (defaultRunspace != null)89 {90 _stateMap.TryGetValue(defaultRunspace, out ss);91 }92 93 return ss;94 }95 }96 97 /// <summary/>98 public class ScriptBlockMemberMethodWrapper99 {100 /// <summary>Used in codegen</summary>101 public static readonly object[] _emptyArgumentArray = Array.Empty<object>(); // See TypeDefiner.DefineTypeHelper.DefineMethodBody102 103 /// <summary>104 /// Indicate the wrapper is for a static member method.105 /// </summary>106 private readonly bool _isStatic;107 108 /// <summary>109 /// The SessionStateKeeper associated with the helper type generated from PowerShell class.110 /// We query it for the SessionState to run static method in.111 /// </summary>112 private readonly SessionStateKeeper _sessionStateKeeper;113 114 /// <summary>115 /// We use WeakReference object to point to the default SessionState because if GC already collect the SessionState,116 /// or the Runspace it chains to is closed and disposed, then we cannot run the static method there anyways.117 /// </summary>118 /// <remarks>119 /// The default SessionState is used only if a static method is called from a Runspace where the PowerShell class is120 /// never defined, or is called on a thread without a default Runspace. Usage like those should be rare.121 /// </remarks>122 private readonly WeakReference<SessionStateInternal> _defaultSessionStateToUse;123 124 /// <summary>125 /// The body AST of the member method.126 /// </summary>127 private readonly IParameterMetadataProvider _ast;128 129 /// <summary>130 /// We use _scriptBlock instance to provide the shared CompiledScriptBlockData.131 /// </summary>132 private readonly Lazy<ScriptBlock> _scriptBlock;133 134 /// <summary>135 /// We use ThreadLocal boundScriptBlock to allow multi-thread execution of member methods.136 /// </summary>137 private readonly ThreadLocal<ScriptBlock> _boundScriptBlock;138 139 /// <summary>140 /// Constructor to be called when the wrapper is for a static member method.141 /// </summary>142 internal ScriptBlockMemberMethodWrapper(IParameterMetadataProvider ast, SessionStateKeeper sessionStateKeeper)143 : this(ast)144 {145 _isStatic = true;146 _sessionStateKeeper = sessionStateKeeper;147 _defaultSessionStateToUse = new WeakReference<SessionStateInternal>(null);148 }149 150 /// <summary>151 /// Constructor to be called when the wrapper is for an instance member method.152 /// </summary>153 internal ScriptBlockMemberMethodWrapper(IParameterMetadataProvider ast)154 {155 _ast = ast;156 // This 'Lazy<T>' constructor ensures that only a single thread can initialize the instance in a thread-safe manner.157 _scriptBlock = new Lazy<ScriptBlock>(() => new ScriptBlock(_ast, isFilter: false));158 _boundScriptBlock = new ThreadLocal<ScriptBlock>(() => _scriptBlock.Value.Clone());159 }160 161 /// <summary>162 /// Initialization happens when the script that defines PowerShell class is executed.163 /// This initialization is required only if this wrapper is for a static method.164 /// </summary>165 /// <remarks>166 /// When the same script file gets executed multiple times, the .NET type generated from the PowerShell class167 /// defined in the file will be shared in those executions, and thus this method will be called multiple times168 /// possibly in the contexts of different Runspace/SessionState.169 ///170 /// We always use the SessionState from the most recent execution as the default SessionState, so be noted that171 /// the default SessionState may change over time.172 ///173 /// This should be OK because the common usage is to run the static method in the same Runspace where the class174 /// is declared, and thus we can always get the correct SessionState to use by querying the 'SessionStateKeeper'.175 /// The default SessionState is used only if a static method is called from a Runspace where the class is never176 /// defined, or is called on a thread without a default Runspace.177 /// </remarks>178 internal void InitAtRuntime()179 {180 if (_isStatic)181 {182 // WeakReference<T>'s instance methods are not thread-safe, so we need the lock to guarantee183 // 'SetTarget' and 'TryGetTarget' are not called by multiple threads at the same time.184 lock (_defaultSessionStateToUse)185 {186 var context = Runspace.DefaultRunspace.ExecutionContext;187 _defaultSessionStateToUse.SetTarget(context.EngineSessionState);188 }189 }190 }191 192 /// <summary>193 /// Set the SessionState of the script block appropriately.194 /// </summary>195 private void PrepareScriptBlockToInvoke(object instance, object sessionStateInternal)196 {197 SessionStateInternal sessionStateToUse = null;198 if (instance != null)199 {200 // Use the SessionState passed in, which is the one associated with the instance.201 sessionStateToUse = (SessionStateInternal)sessionStateInternal;202 }203 else204 {205 // For static method, it's a little complex.206 // - Check if the current default runspace is registered with the SessionStateKeeper. If so, use the registered SessionState.207 // - Otherwise, check if default SessionState is still alive. If so, use the default SessionState.208 // - Otherwise, the 'SessionStateInternal' property will be set to null, and thus the default runspace of the current thread will be used.209 // If the current thread doesn't have a default Runspace, then an InvalidOperationException will be thrown when invoking the210 // script block, which is expected.211 sessionStateToUse = (SessionStateInternal)_sessionStateKeeper.GetSessionState();212 if (sessionStateToUse == null)213 {214 lock (_defaultSessionStateToUse)215 {216 _defaultSessionStateToUse.TryGetTarget(out sessionStateToUse);217 }218 }219 }220 221 _boundScriptBlock.Value.SessionStateInternal = sessionStateToUse;222 }223 224 /// <summary>225 /// </summary>226 /// <param name="instance">Target object or null for static call.</param>227 /// <param name="sessionStateInternal">SessionStateInternal from private field of instance or null for static call.</param>228 /// <param name="args"></param>229 public void InvokeHelper(object instance, object sessionStateInternal, object[] args)230 {231 try232 {233 PrepareScriptBlockToInvoke(instance, sessionStateInternal);234 _boundScriptBlock.Value.InvokeAsMemberFunction(instance, args);235 }236 finally237 {238 // '_boundScriptBlock.Value' for a thread will live until239 // - the thread is gone, OR240 // - the dyanmic assembly holding this wrapper instance is GC collected.241 // We don't hold on the SessionState object, so that GC can collect it as appropriate.242 _boundScriptBlock.Value.SessionStateInternal = null;243 }244 }245 246 /// <summary>247 /// </summary>248 /// <typeparam name="T"></typeparam>249 /// <param name="instance">Target object or null for static call.</param>250 /// <param name="sessionStateInternal">SessionStateInternal from private field of instance or null for static call.</param>251 /// <param name="args"></param>252 /// <returns></returns>253 public T InvokeHelperT<T>(object instance, object sessionStateInternal, object[] args)254 {255 try256 {257 PrepareScriptBlockToInvoke(instance, sessionStateInternal);258 return _boundScriptBlock.Value.InvokeAsMemberFunctionT<T>(instance, args);259 }260 finally261 {262 // '_boundScriptBlock.Value' for a thread will live until263 // - the thread is gone, OR264 // - the dyanmic assembly holding this wrapper instance is GC collected.265 // We don't hold on the SessionState object, so that GC can collect it as appropriate.266 _boundScriptBlock.Value.SessionStateInternal = null;267 }268 }269 }270 271 /// <summary>272 /// Support methods for PowerShell classes.273 /// </summary>274 public static class ClassOps275 {276 /// <summary>277 /// This method calls all Validate attributes for the property to validate value.278 /// Called from class property setters with ValidateArgumentsAttribute attributes.279 /// </summary>280 /// <param name="type"></param>281 /// <param name="propertyName"></param>282 /// <param name="value"></param>283 public static void ValidateSetProperty(Type type, string propertyName, object value)284 {285 var validateAttributes = type.GetProperty(propertyName).GetCustomAttributes<ValidateArgumentsAttribute>();286 var executionContext = LocalPipeline.GetExecutionContextFromTLS();287 var engineIntrinsics = executionContext?.EngineIntrinsics;288 foreach (var validateAttribute in validateAttributes)289 {290 validateAttribute.InternalValidate(value, engineIntrinsics);291 }292 }293 294 /// <summary>295 /// Performs base ctor call as a method call.296 /// </summary>297 /// <param name="target">Object for invocation.</param>298 /// <param name="ci">Ctor info for invocation.</param>299 /// <param name="args">Arguments for invocation.</param>300 [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]301 public static void CallBaseCtor(object target, ConstructorInfo ci, object[] args)302 {303 ci.Invoke(target, args);304 }305 306 /// <summary>307 /// Performs non-virtual method call with return value. Main usage: base class method call inside subclass method.308 /// </summary>309 /// <param name="target">Object for invocation.</param>310 /// <param name="mi">Method info for invocation.</param>311 /// <param name="args">Arguments for invocation.</param>312 public static object CallMethodNonVirtually(object target, MethodInfo mi, object[] args)313 {314 return CallMethodNonVirtuallyImpl(target, mi, args);315 }316 317 /// <summary>318 /// Performs non-virtual void method call. Main usage: base class method call inside subclass method.319 /// </summary>320 /// <param name="target">Object for invocation.</param>321 /// <param name="mi">Method info for invocation.</param>322 /// <param name="args">Arguments for invocation.</param>323 public static void CallVoidMethodNonVirtually(object target, MethodInfo mi, object[] args)324 {325 CallMethodNonVirtuallyImpl(target, mi, args);326 }327 328 /// <summary>329 /// A cache for the DynamicMethod objects that call to base method non-virtually.330 /// The cache can clean up the outdated WeakReference entries by itself.331 /// </summary>332 private static readonly ConditionalWeakTable<MethodInfo, DynamicMethod> s_nonVirtualCallCache =333 new ConditionalWeakTable<MethodInfo, DynamicMethod>();334 335 /// <summary>336 /// Implementation of non-virtual method call.337 /// </summary>338 /// <param name="target">Object for invocation.</param>339 /// <param name="mi">Method info for invocation.</param>340 /// <param name="args">Arguments for invocation.</param>341 private static object CallMethodNonVirtuallyImpl(object target, MethodInfo mi, object[] args)342 {343 DynamicMethod dm = s_nonVirtualCallCache.GetValue(mi, CreateDynamicMethod);344 345 // The target object will be passed to the hidden parameter 'this' of the instance method346 var newArgs = new List<object>(args.Length + 1) { target };347 newArgs.AddRange(args);348 349 return dm.Invoke(null, newArgs.ToArray());350 }351 352 /// <summary>353 /// Help method to create the DynamicMethod for calling base method non-virtually.354 /// </summary>355 private static DynamicMethod CreateDynamicMethod(MethodInfo mi)356 {357 // Pass in the declaring type because instance method has a hidden parameter 'this' as the first parameter.358 var paramTypes = new List<Type> { mi.DeclaringType };359 paramTypes.AddRange(mi.GetParameters().Select(static x => x.ParameterType));360 361 var dm = new DynamicMethod("PSNonVirtualCall_" + mi.Name, mi.ReturnType, paramTypes.ToArray(), mi.DeclaringType);362 ILGenerator il = dm.GetILGenerator();363 for (int i = 0; i < paramTypes.Count; i++)364 {365 il.Emit(OpCodes.Ldarg, i);366 }367 368 il.Emit(OpCodes.Tailcall);369 il.EmitCall(OpCodes.Call, mi, null);370 il.Emit(OpCodes.Ret);371 372 return dm;373 }374 }375}376 