From eed9842534e1d31bcc9b9b61230424d52ca56e97 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Sat, 2 Jun 2018 14:13:54 -0700 Subject: [PATCH 01/17] First batch: Basic experimental feature flag and attributes enforced --- .../engine/Attributes.cs | 51 +++ .../CommandCompletion/CompletionAnalysis.cs | 6 +- .../CommandCompletion/CompletionCompleters.cs | 6 +- .../engine/CommandProcessor.cs | 13 +- .../engine/CommandProcessorBase.cs | 21 +- .../engine/CompiledCommandParameter.cs | 25 +- .../ExperimentalFeature.cs | 309 ++++++++++++++++++ .../engine/InitialSessionState.cs | 241 ++++---------- .../engine/Modules/ModuleCmdletBase.cs | 2 +- .../engine/PSConfiguration.cs | 8 + .../engine/PseudoParameters.cs | 31 +- .../engine/ScriptCommandProcessor.cs | 3 +- .../engine/TypeMetadata.cs | 30 +- .../engine/parser/Compiler.cs | 43 ++- .../engine/parser/TypeResolver.cs | 1 + .../engine/parser/ast.cs | 71 ++++ .../engine/runtime/CompiledScriptBlock.cs | 25 ++ .../engine/runtime/Operations/MiscOps.cs | 8 +- .../resources/DiscoveryExceptions.resx | 6 + 19 files changed, 667 insertions(+), 233 deletions(-) create mode 100644 src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs diff --git a/src/System.Management.Automation/engine/Attributes.cs b/src/System.Management.Automation/engine/Attributes.cs index efa3d5c76f28..92b392314379 100644 --- a/src/System.Management.Automation/engine/Attributes.cs +++ b/src/System.Management.Automation/engine/Attributes.cs @@ -620,12 +620,63 @@ public ParameterAttribute() { } + /// + /// Initializes a new instance that is associated with an experimental feature. + /// + public ParameterAttribute(string experimentName, ExperimentAction experimentAction) + { + if (string.IsNullOrEmpty(experimentName)) + { + throw PSTraceSource.NewArgumentException(nameof(experimentName)); + } + + if (experimentAction == ExperimentAction.None) + { + throw PSTraceSource.NewArgumentException(nameof(experimentAction)); + } + + ExperimentName = experimentName; + ExperimentAction = experimentAction; + } + private string _parameterSetName = ParameterAttribute.AllParameterSets; private string _helpMessage; private string _helpMessageBaseName; private string _helpMessageResourceId; + #region Experimental Feature Related Properties + + /// + /// Name of the experimental feature this attribute is associated with. + /// + public string ExperimentName { get; } + + /// + /// Action for engine to take when the experimental feature is enabled. + /// + public ExperimentAction ExperimentAction { get; } + + internal bool ToHide => EffectiveAction == ExperimentAction.Hide; + internal bool ToShow => EffectiveAction == ExperimentAction.Show; + + /// + /// Effective action to take at run time. + /// + private ExperimentAction EffectiveAction + { + get { + if (_effectiveAction == ExperimentAction.None) + { + _effectiveAction = ExperimentalFeature.GetActionToTake(ExperimentName, ExperimentAction); + } + return _effectiveAction; + } + } + private ExperimentAction _effectiveAction = default(ExperimentAction); + + #endregion + /// /// Gets and sets the parameter position. If not set, the parameter is named. /// diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 4f8debce7c07..bec70e8b70fc 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -1758,8 +1758,10 @@ private List GetResultForAttributeArgument(CompletionContext c List result = new List(); foreach (PropertyInfo pro in propertyInfos) { - //Ignore TypeId (all attributes inherit it) - if (pro.Name != "TypeId" && (pro.Name.StartsWith(argName, StringComparison.OrdinalIgnoreCase))) + // Ignore getter-only properties, including 'TypeId' (all attributes inherit it). + if (!pro.CanWrite) { continue; } + + if (pro.Name.StartsWith(argName, StringComparison.OrdinalIgnoreCase)) { result.Add(new CompletionResult(pro.Name, pro.Name, CompletionResultType.Property, pro.PropertyType.ToString() + " " + pro.Name)); diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index cf88b76bca72..5bac5a9ae862 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -1698,12 +1698,10 @@ internal static string ConcatenateStringPathArguments(CommandElementAst stringAs foreach (ValidateArgumentsAttribute att in parameter.Parameter.ValidationAttributes) { - if (att is ValidateSetAttribute) + if (att is ValidateSetAttribute setAtt) { RemoveLastNullCompletionResult(result); - var setAtt = (ValidateSetAttribute)att; - string wordToComplete = context.WordToComplete; string quote = HandleDoubleAndSingleQuote(ref wordToComplete); @@ -1712,6 +1710,8 @@ internal static string ConcatenateStringPathArguments(CommandElementAst stringAs foreach (string value in setAtt.ValidValues) { + if (value == string.Empty) { continue; } + if (wordToComplete.Equals(value, StringComparison.OrdinalIgnoreCase)) { string completionText = quote == string.Empty ? value : quote + value + quote; diff --git a/src/System.Management.Automation/engine/CommandProcessor.cs b/src/System.Management.Automation/engine/CommandProcessor.cs index 72f7e10c9e52..fdfeee976415 100644 --- a/src/System.Management.Automation/engine/CommandProcessor.cs +++ b/src/System.Management.Automation/engine/CommandProcessor.cs @@ -18,7 +18,7 @@ namespace System.Management.Automation /// internal class CommandProcessor : CommandProcessorBase { -#region ctor + #region ctor static CommandProcessor() { @@ -87,9 +87,10 @@ internal CommandProcessor(IScriptCommandInfo scriptCommandInfo, ExecutionContext // CommandProcessor -#endregion ctor + #endregion ctor + + #region internal members -#region internal members /// /// Returns a CmdletParameterBinderController for the specified command /// @@ -393,9 +394,9 @@ internal override void ProcessRecord() } } -#endregion public_methods + #endregion public_methods -#region helper_methods + #region helper_methods /// /// Tells whether it is the first call to Read @@ -849,7 +850,7 @@ internal override bool IsHelpRequested(out string helpTarget, out HelpCategory h return base.IsHelpRequested(out helpTarget, out helpCategory); } -#endregion helper_methods + #endregion helper_methods } } diff --git a/src/System.Management.Automation/engine/CommandProcessorBase.cs b/src/System.Management.Automation/engine/CommandProcessorBase.cs index 562f6a9572e7..475718f61926 100644 --- a/src/System.Management.Automation/engine/CommandProcessorBase.cs +++ b/src/System.Management.Automation/engine/CommandProcessorBase.cs @@ -34,14 +34,31 @@ internal CommandProcessorBase() /// The metadata about the command to run. /// /// - internal CommandProcessorBase( - CommandInfo commandInfo) + internal CommandProcessorBase(CommandInfo commandInfo) { if (commandInfo == null) { throw PSTraceSource.NewArgumentNullException("commandInfo"); } + if (commandInfo is IScriptCommandInfo scriptCommand) + { + var expAttribute = scriptCommand.ScriptBlock.ExperimentalAttribute; + if (expAttribute != null && expAttribute.ToHide) + { + string errorTemplate = expAttribute.ExperimentAction == ExperimentAction.Hide + ? DiscoveryExceptions.ScriptDisabledWhenFeatureOn + : DiscoveryExceptions.ScriptDisabledWhenFeatureOff; + string errorMsg = StringUtil.Format(errorTemplate, expAttribute.ExperimentName); + ErrorRecord errorRecord = new ErrorRecord( + new InvalidOperationException(errorMsg), + "ScriptCommandDisabled", + ErrorCategory.InvalidOperation, + commandInfo); + throw new CmdletInvocationException(errorRecord); + } + } + CommandInfo = commandInfo; } diff --git a/src/System.Management.Automation/engine/CompiledCommandParameter.cs b/src/System.Management.Automation/engine/CompiledCommandParameter.cs index 97422f46d4a4..070abdf42f6b 100644 --- a/src/System.Management.Automation/engine/CompiledCommandParameter.cs +++ b/src/System.Management.Automation/engine/CompiledCommandParameter.cs @@ -64,6 +64,18 @@ internal CompiledCommandParameter(RuntimeDefinedParameter runtimeDefinedParamete // First, process attributes that aren't type conversions foreach (Attribute attribute in runtimeDefinedParameter.Attributes) { + if (processingDynamicParameters) + { + // When processing dynamic parameters, the attribute list may contain experimental attributes + // and disabled parameter attributes. We should ignore those attributes. + // When processing non-dynamic parameters, the experimental attributes and disabled parameter + // attributes have already been filtered out when constructing the RuntimeDefinedParameter. + if (attribute is ExperimentalAttribute || attribute is ParameterAttribute param && param.ToHide) + { + continue; + } + } + if (!(attribute is ArgumentTypeConverterAttribute)) { ProcessAttribute(runtimeDefinedParameter.Name, attribute, ref validationAttributes, ref argTransformationAttributes, ref aliases); @@ -78,7 +90,7 @@ internal CompiledCommandParameter(RuntimeDefinedParameter runtimeDefinedParamete } // Now process type converters - foreach (ArgumentTypeConverterAttribute attribute in runtimeDefinedParameter.Attributes.OfType()) + foreach (var attribute in runtimeDefinedParameter.Attributes.OfType()) { ProcessAttribute(runtimeDefinedParameter.Name, attribute, ref validationAttributes, ref argTransformationAttributes, ref aliases); } @@ -169,7 +181,15 @@ internal CompiledCommandParameter(MemberInfo member, bool processingDynamicParam foreach (Attribute attr in memberAttributes) { - ProcessAttribute(member.Name, attr, ref validationAttributes, ref argTransformationAttributes, ref aliases); + switch (attr) + { + case ExperimentalAttribute _: + case ParameterAttribute param when param.ToHide: + break; + default: + ProcessAttribute(member.Name, attr, ref validationAttributes, ref argTransformationAttributes, ref aliases); + break; + } } this.ValidationAttributes = validationAttributes == null @@ -436,7 +456,6 @@ internal IEnumerable GetMatchingParameterSetData(u ref Collection argTransformationAttributes, ref string[] aliases) { - // NTRAID#Windows Out Of Band Releases-926374-2005/12/22-JonN if (attribute == null) return; diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs new file mode 100644 index 000000000000..3c6b2060fbaf --- /dev/null +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Immutable; +using System.Linq; +using System.Management.Automation.Configuration; +using System.Management.Automation.Internal; +using System.Runtime.CompilerServices; + +namespace System.Management.Automation +{ + /// + /// Support experimental features in PowerShell + /// + public class ExperimentalFeature + { + #region Const Members + + internal const string EngineSource = "PSEngine"; + + #endregion + + #region Instance Members + + /// + /// Name of an experimental feature. + /// + public string Name { get; } + + /// + /// Description of an experimental feature. + /// + public string Description { get; } + + /// + /// Source of an experimental feature. + /// + public string Source { get; } + + /// + /// Indicate whether the feature is enabled. + /// + public bool IsEnabled { get; private set; } + + /// + /// Constructor for ExperimentalFeature. + /// + internal ExperimentalFeature(string name, string description, string source, bool isEnabled) + { + Name = name; + Description = description; + Source = source; + IsEnabled = isEnabled; + } + + #endregion + + #region Static Members + + /// + /// All available engine experimental features. + /// + internal static readonly ReadOnlyCollection EngineExperimentalFeatures; + + /// + /// A dictionary of all available engine experimental features. Feature name is the key. + /// + internal static readonly ReadOnlyDictionary EngineExperimentalFeatureMap; + + /// + /// Experimental feature names that are enabled in the config file. + /// + internal static readonly ImmutableHashSet EnabledExperimentalFeatureNames; + + /// + /// Type initializer. Initialize the engine experimental feature list. + /// + static ExperimentalFeature() + { + // Initialize the readonly collection 'EngineExperimentalFeatures'. + var engineFeatures = new ExperimentalFeature[] { + /* Register engine experimental features here. Follow the same pattern as the example: + new ExperimentalFeature(name: "PSFileSystemProviderV2", + description: "Replace the old FileSystemProvider with cleaner design and faster code", + source: EngineSource, + isEnabled: false)) + */ + }; + EngineExperimentalFeatures = new ReadOnlyCollection(engineFeatures); + + // Initialize the readonly dictionary 'EngineExperimentalFeatureMap'. + var engineExpFeatureMap = engineFeatures.ToDictionary(f => f.Name, StringComparer.OrdinalIgnoreCase); + EngineExperimentalFeatureMap = new ReadOnlyDictionary(engineExpFeatureMap); + + // Initialize the immutable hashset 'EnabledExperimentalFeatureNames'. + // The initialization of 'EnabledExperimentalFeatureNames' is deliberately made in the type initializer so that: + // 1. 'EnabledExperimentalFeatureNames' can be declared as readonly; + // 2. No need to deal with initialization from multiple threads; + // 3. We don't need to decide where/when to read the config file for the enabled experimental features, + // instead, it will be done when the type is used for the first time, which is always earlier than + // any experimental features take effect. + string[] enabledFeatures = Utils.EmptyArray(); + try { enabledFeatures = PowerShellConfig.Instance.GetExperimentalFeatures(); } + catch (Exception ex) when (LogException(ex, "Placeholder")) { } + + EnabledExperimentalFeatureNames = ProcessEnabledFeatures(enabledFeatures); + } + + /// + /// Check if the specified experimental feature has been enabled. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasEnabled(string featureName) + { + return EnabledExperimentalFeatureNames.Contains(featureName); + } + + /// + /// Determine the action to take for the specified experiment name and action. + /// + internal static ExperimentAction GetActionToTake(string experimentName, ExperimentAction experimentAction) + { + if (experimentName == null || experimentAction == ExperimentAction.None) + { + // If either the experiment name or action is not defined, then return 'Show' by default. + // This could happen to 'ParameterAttribute' when no experimental related field is declared. + return ExperimentAction.Show; + } + + ExperimentAction action = experimentAction; + if (!HasEnabled(experimentName)) + { + action = (action == ExperimentAction.Hide) ? ExperimentAction.Show : ExperimentAction.Hide; + } + return action; + } + + /// + /// Check if the name follows the engine experimental feature name convention. + /// Convention: prefix 'PS' to the feature name -- PSFeatureName + /// + internal static bool IsEngineFeatureName(string featureName) + { + return featureName.IndexOf('.') == -1 && featureName.Length > 2 && featureName.StartsWith("PS", StringComparison.Ordinal); + } + + /// + /// Check if the name follows the module experimental feature name convention. + /// Convention: ModuleName.FeatureName + /// + /// The feature name to check. + /// When specified, we check if the feature name matches the module name + internal static bool IsModuleFeatureName(string featureName, string moduleName = null) + { + int firstDotIndex = featureName.IndexOf('.'); + int lastDotIndex = featureName.LastIndexOf('.'); + + bool legit = firstDotIndex > 0 && lastDotIndex < featureName.Length - 1; + if (legit && moduleName != null) + { + var moduleNamePart = featureName.AsSpan(0, lastDotIndex); + return moduleNamePart.Equals(moduleName.AsSpan(), StringComparison.OrdinalIgnoreCase); + } + return legit; + } + + /// + /// Log the exception without rewinding the stack. + /// + private static bool LogException(Exception ex, string errorTemplate) + { + // TODO: Logging + return false; + } + + /// + /// Log an error message. + /// + private static void LogError(string message) + { + // TODO: Logging + } + + /// + /// Process the array of enabled feature names retrieved from configuration. + /// Ignore invalid feature names and unavailable engine feature names, and + /// return an ImmutableHashSet of the valid enabled feature names. + /// + private static ImmutableHashSet ProcessEnabledFeatures(string[] enabledFeatures) + { + if (enabledFeatures.Length == 0) { return ImmutableHashSet.Create(); } + + var list = new List(enabledFeatures.Length); + foreach (string name in enabledFeatures) + { + if (IsModuleFeatureName(name)) + { + list.Add(name); + } + else if (IsEngineFeatureName(name)) + { + if (EngineExperimentalFeatureMap.TryGetValue(name, out ExperimentalFeature feature)) + { + feature.IsEnabled = true; + list.Add(name); + } + else + { + LogError("No such engine feature registered. Ignore it."); + } + } + else + { + LogError("Invalid experimental feature name. Ignore it"); + } + } + return ImmutableHashSet.CreateRange(StringComparer.OrdinalIgnoreCase, list); + } + + #endregion + } + + /// + /// Indicates the action to take on the cmdlet/parameter that has the attribute declared. + /// + public enum ExperimentAction + { + /// + /// Represent an undefined action, used as the default value. + /// + None = 0, + + /// + /// Hide the cmdlet/parameter when the corresponding experimental feature is enabled. + /// + Hide = 1, + + /// + /// Show the cmdlet/parameter when the corresponding experimental feature is enabled. + /// + Show = 2 + } + + /// + /// The attribute that applies to cmdlet/function/parameter to define what the engine should do with it. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class ExperimentalAttribute : ParsingBaseAttribute + { + /// + /// Specify the experimental feature this attribute is associated with. + /// + public string ExperimentName { get; } + + /// + /// Specify the action for engine to take when the experimental feature is enabled. + /// + public ExperimentAction ExperimentAction { get; } + + /// + /// Initializes a new instance of the ExperimentalAttribute class. + /// + public ExperimentalAttribute(string experimentName, ExperimentAction experimentAction) + { + if (string.IsNullOrEmpty(experimentName)) + { + throw PSTraceSource.NewArgumentException(nameof(experimentName)); + } + + if (experimentAction == ExperimentAction.None) + { + throw PSTraceSource.NewArgumentException(nameof(experimentAction)); + } + + ExperimentName = experimentName; + ExperimentAction = experimentAction; + } + + /// + /// Initialize an instance that represents the none-value. + /// + private ExperimentalAttribute() {} + + /// + /// An instance that represents the none-value. + /// + internal readonly static ExperimentalAttribute None = new ExperimentalAttribute(); + + internal bool ToHide => EffectiveAction == ExperimentAction.Hide; + internal bool ToShow => EffectiveAction == ExperimentAction.Show; + + /// + /// Effective action to take at run time. + /// + private ExperimentAction EffectiveAction + { + get { + if (_effectiveAction == ExperimentAction.None) + { + _effectiveAction = ExperimentalFeature.GetActionToTake(ExperimentName, ExperimentAction); + } + return _effectiveAction; + } + } + private ExperimentAction _effectiveAction = default(ExperimentAction); + } +} \ No newline at end of file diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index 320961e16749..c31eba64b57c 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -13,12 +13,13 @@ using System.Management.Automation.Provider; using System.Management.Automation.Language; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using Microsoft.PowerShell.Commands; -using Debug = System.Management.Automation.Diagnostics; using System.Management.Automation.Host; using System.Text; using System.Threading.Tasks; +using Debug = System.Management.Automation.Diagnostics; namespace System.Management.Automation.Runspaces { @@ -3752,7 +3753,7 @@ internal PSSnapInInfo ImportPSSnapIn(PSSnapInInfo psSnapInInfo, out PSSnapInExce { s_PSSnapInTracer.WriteLine("Loading assembly for psSnapIn {0}", psSnapInInfo.Name); - assembly = PSSnapInHelpers.LoadPSSnapInAssembly(psSnapInInfo, out cmdlets, out providers); + assembly = PSSnapInHelpers.LoadPSSnapInAssembly(psSnapInInfo); if (assembly == null) { @@ -4897,13 +4898,9 @@ internal static string GetNestedModuleDllName(string moduleName) internal static class PSSnapInHelpers { [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom")] - internal static Assembly LoadPSSnapInAssembly(PSSnapInInfo psSnapInInfo, - out Dictionary cmdlets, out Dictionary providers) + internal static Assembly LoadPSSnapInAssembly(PSSnapInInfo psSnapInInfo) { Assembly assembly = null; - cmdlets = null; - providers = null; - s_PSSnapInTracer.WriteLine("Loading assembly from GAC. Assembly Name: {0}", psSnapInInfo.AssemblyName); try @@ -4960,13 +4957,11 @@ internal static class PSSnapInHelpers return assembly; } - private static T GetCustomAttribute(Type decoratedType) where T : Attribute + private static bool GetCustomAttribute(Type decoratedType, out T attribute) where T : Attribute { - var attributes = decoratedType.GetCustomAttributes(false); - var customAttrs = attributes.ToArray(); - - Debug.Assert(customAttrs.Length <= 1, "CmdletAttribute and/or CmdletProviderAttribute cannot normally appear more than once"); - return customAttrs.Length == 0 ? null : customAttrs[0]; + var attributes = decoratedType.GetCustomAttributes(inherit: false); + attribute = attributes.FirstOrDefault(); + return attribute != null; } internal static void AnalyzePSSnapInAssembly(Assembly assembly, string name, PSSnapInInfo psSnapInInfo, PSModuleInfo moduleInfo, bool isModuleLoad, @@ -4976,7 +4971,7 @@ internal static class PSSnapInHelpers helpFile = null; if (assembly == null) { - throw new ArgumentNullException("assembly"); + throw new ArgumentNullException(nameof(assembly)); } cmdlets = null; @@ -4988,8 +4983,8 @@ internal static class PSSnapInHelpers Dictionary>> cachedCmdlets; if (s_cmdletCache.Value.TryGetValue(assembly, out cachedCmdlets)) { - cmdlets = new Dictionary(s_cmdletCache.Value.Count, StringComparer.OrdinalIgnoreCase); - aliases = new Dictionary>(StringComparer.OrdinalIgnoreCase); + cmdlets = new Dictionary(cachedCmdlets.Count, StringComparer.OrdinalIgnoreCase); + aliases = new Dictionary>(cachedCmdlets.Count, StringComparer.OrdinalIgnoreCase); foreach (var pair in cachedCmdlets) { @@ -5051,30 +5046,15 @@ internal static class PSSnapInHelpers } string assemblyPath = assembly.Location; - Type[] assemblyTypes; if (cmdlets != null || providers != null) { - if (!s_assembliesWithModuleInitializerCache.Value.ContainsKey(assembly)) - { - s_PSSnapInTracer.WriteLine("Returning cached cmdlet and provider entries for {0}", assemblyPath); - return; - } - else - { - s_PSSnapInTracer.WriteLine("Executing IModuleAssemblyInitializer.Import for {0}", assemblyPath); - assemblyTypes = GetAssemblyTypes(assembly, name); - ExecuteModuleInitializer(assembly, assemblyTypes, isModuleLoad); - return; - } + s_PSSnapInTracer.WriteLine("Returning cached cmdlet and provider entries for {0}", assemblyPath); + return; } s_PSSnapInTracer.WriteLine("Analyzing assembly {0} for cmdlet and providers", assemblyPath); - helpFile = GetHelpFile(assemblyPath); - Type randomCmdletToCheckLinkDemand = null; - Type randomProviderToCheckLinkDemand = null; - if (psSnapInInfo != null && psSnapInInfo.Name.Equals(InitialSessionState.CoreSnapin, StringComparison.OrdinalIgnoreCase)) { InitializeCoreCmdletsAndProviders(psSnapInInfo, out cmdlets, out providers, helpFile); @@ -5086,13 +5066,10 @@ internal static class PSSnapInHelpers Dictionary cmdletsCheck = null; Dictionary providersCheck = null; Dictionary> aliasesCheck = null; - Type unused1 = null; - Type unused2 = null; - AnalyzeModuleAssemblyWithReflection(assembly, name, psSnapInInfo, moduleInfo, isModuleLoad, - ref cmdletsCheck, ref aliasesCheck, ref providersCheck, helpFile, ref unused1, ref unused2); + AnalyzeModuleAssemblyWithReflection(assembly, name, psSnapInInfo, moduleInfo, isModuleLoad, ref cmdletsCheck, ref aliasesCheck, ref providersCheck, helpFile); Diagnostics.Assert(aliasesCheck == null, "InitializeCoreCmdletsAndProviders assumes no aliases are defined in System.Management.Automation.dll"); - Diagnostics.Assert(providersCheck.Keys.Count == providers.Keys.Count, "new Provider added to System.Management.Automation.dll - update InitializeCoreCmdletsAndProviders"); + Diagnostics.Assert(providersCheck.Count == providers.Count, "new Provider added to System.Management.Automation.dll - update InitializeCoreCmdletsAndProviders"); foreach (var pair in providersCheck) { SessionStateProviderEntry other; @@ -5110,7 +5087,7 @@ internal static class PSSnapInHelpers Diagnostics.Assert(false, "Missing provider: " + pair.Key); } } - Diagnostics.Assert(cmdletsCheck.Keys.Count == cmdlets.Keys.Count, "new Cmdlet added to System.Management.Automation.dll - update InitializeCoreCmdletsAndProviders"); + Diagnostics.Assert(cmdletsCheck.Count == cmdlets.Count, "new Cmdlet added to System.Management.Automation.dll - update InitializeCoreCmdletsAndProviders"); foreach (var pair in cmdletsCheck) { @@ -5133,39 +5110,7 @@ internal static class PSSnapInHelpers } else { - AnalyzeModuleAssemblyWithReflection(assembly, name, psSnapInInfo, moduleInfo, isModuleLoad, - ref cmdlets, ref aliases, ref providers, helpFile, ref randomCmdletToCheckLinkDemand, ref randomProviderToCheckLinkDemand); - } - - // force a LinkDemand check to get an explicit exception if - // Cmdlet[Provider]Attributes are silently swallowed by Type.GetCustomAttributes - // bug Win7:705573 - if ((providers == null || providers.Count == 0) && (cmdlets == null || cmdlets.Count == 0)) - { - try - { - if (randomCmdletToCheckLinkDemand != null) - { - ConstructorInfo constructor = randomCmdletToCheckLinkDemand.GetConstructor(PSTypeExtensions.EmptyTypes); - if (constructor != null) - { - constructor.Invoke(null); // this is how we artificially force a LinkDemand check - } - } - - if (randomProviderToCheckLinkDemand != null) - { - ConstructorInfo constructor = randomProviderToCheckLinkDemand.GetConstructor(PSTypeExtensions.EmptyTypes); - if (constructor != null) - { - constructor.Invoke(null); // this is how we artificially force a LinkDemand check - } - } - } - catch (TargetInvocationException e) - { - throw e.InnerException; - } + AnalyzeModuleAssemblyWithReflection(assembly, name, psSnapInInfo, moduleInfo, isModuleLoad, ref cmdlets, ref aliases, ref providers, helpFile); } // Cache the cmdlet and provider info for this assembly... @@ -5212,118 +5157,83 @@ internal static class PSSnapInHelpers ref Dictionary cmdlets, ref Dictionary> aliases, ref Dictionary providers, - string helpFile, - ref Type randomCmdletToCheckLinkDemand, - ref Type randomProviderToCheckLinkDemand) + string helpFile) { var assemblyTypes = GetAssemblyTypes(assembly, name); - ExecuteModuleInitializer(assembly, assemblyTypes, isModuleLoad); foreach (Type type in assemblyTypes) { - if (!(type.IsPublic || type.IsNestedPublic) || type.IsAbstract) - continue; + if (!HasDefaultConstructor(type)) { continue; } // Check for cmdlets - if (IsCmdletClass(type) && HasDefaultConstructor(type)) + if (IsCmdletClass(type) && GetCustomAttribute(type, out CmdletAttribute cmdletAttribute)) { - randomCmdletToCheckLinkDemand = type; - - CmdletAttribute cmdletAttribute = GetCustomAttribute(type); - if (cmdletAttribute == null) - { - continue; - } - string cmdletName = GetCmdletName(cmdletAttribute); - if (string.IsNullOrEmpty(cmdletName)) + if (GetCustomAttribute(type, out ExperimentalAttribute expAttribute) && expAttribute.ToHide) { + // If 'ExperimentalAttribute' is specified on the cmdlet type and the + // effective action at run time is 'Hide', then we ignore the type. continue; } + string cmdletName = cmdletAttribute.VerbName + "-" + cmdletAttribute.NounName; if (cmdlets != null && cmdlets.ContainsKey(cmdletName)) { string message = StringUtil.Format(ConsoleInfoErrorStrings.PSSnapInDuplicateCmdlets, cmdletName, name); - s_PSSnapInTracer.TraceError(message); - throw new PSSnapInException(name, message); } SessionStateCmdletEntry cmdlet = new SessionStateCmdletEntry(cmdletName, type, helpFile); - cmdlet.SetPSSnapIn(psSnapInInfo); - if (cmdlets == null) - { - cmdlets = new Dictionary(StringComparer.OrdinalIgnoreCase); - } + if (psSnapInInfo != null) { cmdlet.SetPSSnapIn(psSnapInInfo); } + if (moduleInfo != null) { cmdlet.SetModule(moduleInfo); } + + cmdlets = cmdlets ?? new Dictionary(StringComparer.OrdinalIgnoreCase); cmdlets.Add(cmdletName, cmdlet); - var aliasAttribute = GetCustomAttribute(type); - if (aliasAttribute != null) + if (GetCustomAttribute(type, out AliasAttribute aliasAttribute)) { - if (aliases == null) - { - aliases = new Dictionary>(StringComparer.OrdinalIgnoreCase); - } + aliases = aliases ?? new Dictionary>(StringComparer.OrdinalIgnoreCase); var aliasList = new List(); foreach (var alias in aliasAttribute.AliasNames) { - // Alias declared by AliasAttribute is set with the option 'ScopedItemOptions.None', - // because we believe a user of the cmdlet, instead of the author of it, - // should be the one to decide the option - // ('ScopedItemOptions.ReadOnly' and/or 'ScopedItemOptions.AllScopes') of the alias usage." - var aliasEntry = new SessionStateAliasEntry(alias, cmdletName, string.Empty, ScopedItemOptions.None); - if (psSnapInInfo != null) - { - aliasEntry.SetPSSnapIn(psSnapInInfo); - } + // Alias declared by 'AliasAttribute' is set with the option 'ScopedItemOptions.None', because we believe + // the users of the cmdlet, instead of the author, should have control of what options applied to an alias + // ('ScopedItemOptions.ReadOnly' and/or 'ScopedItemOptions.AllScopes'). + var aliasEntry = new SessionStateAliasEntry(alias, cmdletName, description: string.Empty, ScopedItemOptions.None); + if (psSnapInInfo != null) { aliasEntry.SetPSSnapIn(psSnapInInfo); } + if (moduleInfo != null) { aliasEntry.SetModule(moduleInfo); } aliasList.Add(aliasEntry); } aliases.Add(cmdletName, aliasList); } s_PSSnapInTracer.WriteLine("{0} from type {1} is added as a cmdlet. ", cmdletName, type.FullName); - continue; } - // Check for providers - if (IsProviderClass(type) && HasDefaultConstructor(type)) + else if (IsProviderClass(type) && GetCustomAttribute(type, out CmdletProviderAttribute providerAttribute)) { - randomProviderToCheckLinkDemand = type; - - CmdletProviderAttribute providerAttribute = GetCustomAttribute(type); - if (providerAttribute == null) - { - continue; - } - string providerName = GetProviderName(providerAttribute); - if (string.IsNullOrEmpty(providerName)) + if (GetCustomAttribute(type, out ExperimentalAttribute expAttribute) && expAttribute.ToHide) { + // If 'ExperimentalAttribute' is specified on the provider type and + // the effective action at run time is 'Hide', then we ignore the type. continue; } + string providerName = providerAttribute.ProviderName; if (providers != null && providers.ContainsKey(providerName)) { string message = StringUtil.Format(ConsoleInfoErrorStrings.PSSnapInDuplicateProviders, providerName, psSnapInInfo.Name); - s_PSSnapInTracer.TraceError(message); - throw new PSSnapInException(psSnapInInfo.Name, message); } SessionStateProviderEntry provider = new SessionStateProviderEntry(providerName, type, helpFile); - provider.SetPSSnapIn(psSnapInInfo); + if (psSnapInInfo != null) { provider.SetPSSnapIn(psSnapInInfo); } + if (moduleInfo != null) { provider.SetModule(moduleInfo); } - // After converting core snapins to load as modules, the providers will have Module property populated - if (moduleInfo != null) - { - provider.SetModule(moduleInfo); - } - if (providers == null) - { - providers = new Dictionary(StringComparer.OrdinalIgnoreCase); - } + providers = providers ?? new Dictionary(StringComparer.OrdinalIgnoreCase); providers.Add(providerName, provider); s_PSSnapInTracer.WriteLine("{0} from type {1} is added as a provider. ", providerName, type.FullName); @@ -5431,39 +5341,28 @@ internal static class PSSnapInHelpers } } - private static void ExecuteModuleInitializer(Assembly assembly, Type[] assemblyTypes, bool isModuleLoad) + private static void ExecuteModuleInitializer(Assembly assembly, IEnumerable assemblyTypes, bool isModuleLoad) { - for (int i = 0; i < assemblyTypes.Length; i++) + foreach (Type type in assemblyTypes) { - Type type = assemblyTypes[i]; - if (!(type.IsPublic || type.IsNestedPublic) || type.IsAbstract) { continue; } - - if (isModuleLoad && typeof(IModuleAssemblyInitializer).IsAssignableFrom(type) && type != typeof(IModuleAssemblyInitializer)) + if (isModuleLoad && typeof(IModuleAssemblyInitializer).IsAssignableFrom(type)) { - s_assembliesWithModuleInitializerCache.Value[assembly] = true; IModuleAssemblyInitializer moduleInitializer = (IModuleAssemblyInitializer)Activator.CreateInstance(type, true); moduleInitializer.OnImport(); } } } - internal static Type[] GetAssemblyTypes(Assembly assembly, string name) + internal static IEnumerable GetAssemblyTypes(Assembly assembly, string name) { - Type[] assemblyTypes = null; - try { - var exportedTypes = assembly.ExportedTypes; - assemblyTypes = exportedTypes as Type[] ?? exportedTypes.ToArray(); + // Return types that are public, non-abstract, non-interface and non-valueType. + return assembly.ExportedTypes.Where(t => !t.IsAbstract && !t.IsInterface && !t.IsValueType); } catch (ReflectionTypeLoadException e) { - string message; - - message = e.Message; - - message += "\nLoader Exceptions: \n"; - + string message = e.Message + "\nLoader Exceptions: \n"; if (e.LoaderExceptions != null) { foreach (Exception exception in e.LoaderExceptions) @@ -5473,10 +5372,8 @@ internal static Type[] GetAssemblyTypes(Assembly assembly, string name) } s_PSSnapInTracer.TraceError(message); - throw new PSSnapInException(name, message); } - return assemblyTypes; } // cmdletCache holds the list of cmdlets along with its aliases per each assembly. @@ -5484,54 +5381,26 @@ internal static Type[] GetAssemblyTypes(Assembly assembly, string name) new Lazy>>>>(); private static Lazy>> s_providerCache = new Lazy>>(); - // Using a ConcurrentDictionary for this so that we can avoid having a private lock variable. We use only the keys for checking. - private static Lazy> s_assembliesWithModuleInitializerCache = new Lazy>(); - - private static string GetCmdletName(CmdletAttribute cmdletAttribute) - { - string verb = cmdletAttribute.VerbName; - - string noun = cmdletAttribute.NounName; - - return verb + "-" + noun; - } - - private static string GetProviderName(CmdletProviderAttribute providerAttribute) - { - return providerAttribute.ProviderName; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsCmdletClass(Type type) { - if (type == null) - return false; - return type.IsSubclassOf(typeof(System.Management.Automation.Cmdlet)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsProviderClass(Type type) { - if (type == null) - return false; - return type.IsSubclassOf(typeof(System.Management.Automation.Provider.CmdletProvider)); } - internal static bool IsModuleAssemblyInitializerClass(Type type) - { - if (type == null) - { - return false; - } - - return type.IsSubclassOf(typeof(System.Management.Automation.IModuleAssemblyInitializer)); - } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool HasDefaultConstructor(Type type) { return !(type.GetConstructor(PSTypeExtensions.EmptyTypes) == null); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string GetHelpFile(string assemblyPath) { // Help files exist only for original module assemblies, not for generated Ngen binaries diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 9d019f95b0d5..11107ceac1ad 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -4672,7 +4672,7 @@ internal void RemoveModule(PSModuleInfo module, string moduleNameInRemoveModuleC var exportedTypes = PSSnapInHelpers.GetAssemblyTypes(module.ImplementingAssembly, module.Name); foreach (var type in exportedTypes) { - if (typeof(IModuleAssemblyCleanup).IsAssignableFrom(type) && type != typeof(IModuleAssemblyCleanup)) + if (typeof(IModuleAssemblyCleanup).IsAssignableFrom(type)) { var moduleCleanup = (IModuleAssemblyCleanup)Activator.CreateInstance(type, true); moduleCleanup.OnRemove(module); diff --git a/src/System.Management.Automation/engine/PSConfiguration.cs b/src/System.Management.Automation/engine/PSConfiguration.cs index 18d7814a2ff5..7f3597f84053 100644 --- a/src/System.Management.Automation/engine/PSConfiguration.cs +++ b/src/System.Management.Automation/engine/PSConfiguration.cs @@ -193,6 +193,14 @@ internal void SetDisablePromptToUpdateHelp(bool prompt) WriteValueToFile(ConfigScope.SystemWide, "DisablePromptToUpdateHelp", prompt); } + /// + /// Get the names of experimental features enabled in the config file. + /// + internal string[] GetExperimentalFeatures() + { + return ReadValueFromFile(ConfigScope.SystemWide, "ExperimentalFeatures", Utils.EmptyArray()); + } + /// /// Corresponding settings of the original Group Policies /// diff --git a/src/System.Management.Automation/engine/PseudoParameters.cs b/src/System.Management.Automation/engine/PseudoParameters.cs index 50b8ff4ef2e0..e73ea5fb79fd 100644 --- a/src/System.Management.Automation/engine/PseudoParameters.cs +++ b/src/System.Management.Automation/engine/PseudoParameters.cs @@ -164,7 +164,36 @@ public object Value /// This can be any attribute that can be applied to a normal parameter. /// public Collection Attributes { get; } = new Collection(); - } // class RuntimeDefinedParameter + + /// + /// Check if the parameter is disabled due to the associated experimental feature. + /// + internal bool IsDisabled() + { + bool hasParameterAttribute = false; + bool hasEnabledParamAttribute = false; + bool hasSeenExpAttribute = false; + + foreach (var attr in Attributes) + { + if (!hasSeenExpAttribute && attr is ExperimentalAttribute expAttribute) + { + if (expAttribute.ToHide) { return true; } + hasSeenExpAttribute = true; + } + else if (attr is ParameterAttribute paramAttribute) + { + hasParameterAttribute = true; + if (paramAttribute.ToHide) { continue; } + hasEnabledParamAttribute = true; + } + } + + // If one or more parameter attributes are declared but none is enabled, + // then we consider the parameter is disabled. + return hasParameterAttribute && !hasEnabledParamAttribute; + } + } /// /// Represents a collection of runtime-defined parameters that are keyed based on the name diff --git a/src/System.Management.Automation/engine/ScriptCommandProcessor.cs b/src/System.Management.Automation/engine/ScriptCommandProcessor.cs index e14f0616f7a9..2ba86c672542 100644 --- a/src/System.Management.Automation/engine/ScriptCommandProcessor.cs +++ b/src/System.Management.Automation/engine/ScriptCommandProcessor.cs @@ -17,10 +17,9 @@ namespace System.Management.Automation internal abstract class ScriptCommandProcessorBase : CommandProcessorBase { protected ScriptCommandProcessorBase(ScriptBlock scriptBlock, ExecutionContext context, bool useLocalScope, CommandOrigin origin, SessionStateInternal sessionState) + : base(new ScriptInfo(String.Empty, scriptBlock, context)) { this._dontUseScopeCommandOrigin = false; - this.CommandInfo = new ScriptInfo(String.Empty, scriptBlock, context); - this._fromScriptFile = false; CommonInitialization(scriptBlock, context, useLocalScope, origin, sessionState); diff --git a/src/System.Management.Automation/engine/TypeMetadata.cs b/src/System.Management.Automation/engine/TypeMetadata.cs index 4cf45a6dd050..074a306a5f0c 100644 --- a/src/System.Management.Automation/engine/TypeMetadata.cs +++ b/src/System.Management.Automation/engine/TypeMetadata.cs @@ -1308,10 +1308,12 @@ internal InternalParameterMetadata(Type type, bool processingDynamicParameters) foreach (RuntimeDefinedParameter parameterDefinition in runtimeDefinedParameters.Values) { // Create the compiled parameter and add it to the bindable parameters collection - - // NTRAID#Windows Out Of Band Releases-926374-2005/12/22-JonN - if (parameterDefinition == null) - continue; + if (processingDynamicParameters) + { + // When processing dynamic parameters, parameter definitions come from the user, + // Invalid data could be passed in, or the parameter could be actually disabled. + if (parameterDefinition == null || parameterDefinition.IsDisabled()) { continue; } + } CompiledCommandParameter parameter = new CompiledCommandParameter(parameterDefinition, processingDynamicParameters); AddParameter(parameter, checkNames); @@ -1379,7 +1381,6 @@ private void CheckForReservedParameter(string name) } } - // NTRAID#Windows Out Of Band Releases-906345-2005/06/30-JeffJon // This call verifies that the parameter is unique or // can be deemed unique. If not, an exception is thrown. // If it is unique (or deemed unique), then it is added @@ -1458,7 +1459,6 @@ private void AddParameter(CompiledCommandParameter parameter, bool checkNames) foreach (string alias in parameter.Aliases) { - // NTRAID#Windows Out Of Band Releases-917356-JonN if (AliasedParameters.ContainsKey(alias)) { throw new MetadataException( @@ -1501,16 +1501,20 @@ private void RemoveParameter(CompiledCommandParameter parameter) /// private static bool IsMemberAParameter(MemberInfo member) { - bool result = false; - try { - // MemberInfo.GetCustomAttributes returns IEnumerable in CoreCLR - var attributes = member.GetCustomAttributes(typeof(ParameterAttribute), false); - if (attributes.Any()) + var expAttribute = member.GetCustomAttributes(false).FirstOrDefault(); + if (expAttribute != null && expAttribute.ToHide) { return false; } + + var hasAnyVisibleParamAttributes = false; + var paramAttributes = member.GetCustomAttributes(false); + foreach (var paramAttribute in paramAttributes) { - result = true; + if (paramAttribute.ToHide) { continue; } + hasAnyVisibleParamAttributes = true; + break; } + return hasAnyVisibleParamAttributes; } catch (MetadataException metadataException) { @@ -1530,8 +1534,6 @@ private static bool IsMemberAParameter(MemberInfo member) member.Name, argumentException.Message); } - - return result; } // IsMemberAParameter #endregion helper methods diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index 3426d326a9f7..ee910e59ea21 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -1001,15 +1001,17 @@ internal static Expression ConvertValue(Expression expr, List internal static RuntimeDefinedParameterDictionary GetParameterMetaData(ReadOnlyCollection parameters, bool automaticPositions, ref bool usesCmdletBinding) { var md = new RuntimeDefinedParameterDictionary(); - var listMd = new List(); + var listMd = new List(parameters.Count); var customParameterSet = false; for (int index = 0; index < parameters.Count; index++) { var param = parameters[index]; var rdp = GetRuntimeDefinedParameter(param, ref customParameterSet, ref usesCmdletBinding); - - listMd.Add(rdp); - md.Add(param.Name.VariablePath.UserPath, rdp); + if (rdp != null) + { + listMd.Add(rdp); + md.Add(param.Name.VariablePath.UserPath, rdp); + } } int pos = 0; @@ -1447,28 +1449,47 @@ internal static Attribute GetAttribute(TypeConstraintAst typeConstraintAst) private static RuntimeDefinedParameter GetRuntimeDefinedParameter(ParameterAst parameterAst, ref bool customParameterSet, ref bool usesCmdletBinding) { - List attributes = new List(); + List attributes = new List(parameterAst.Attributes.Count); bool hasParameterAttribute = false; + bool hasEnabledParamAttribute = false; + bool hasSeenExpAttribute = false; + for (int index = 0; index < parameterAst.Attributes.Count; index++) { var attributeAst = parameterAst.Attributes[index]; var attribute = attributeAst.GetAttribute(); - attributes.Add(attribute); - var parameterAttribute = attribute as ParameterAttribute; - if (parameterAttribute != null) + if (attribute is ExperimentalAttribute expAttribute) + { + // Only honor the first seen experimental attribute, ignore the others. + if (!hasSeenExpAttribute && expAttribute.ToHide) { return null; } + + // Do not add experimental attributes to the attribute list. + hasSeenExpAttribute = true; + continue; + } + else if (attribute is ParameterAttribute paramAttribute) { hasParameterAttribute = true; + if (paramAttribute.ToHide) { continue; } + + hasEnabledParamAttribute = true; usesCmdletBinding = true; - if (parameterAttribute.Position != int.MinValue || - !parameterAttribute.ParameterSetName.Equals(ParameterAttribute.AllParameterSets, + if (paramAttribute.Position != int.MinValue || + !paramAttribute.ParameterSetName.Equals(ParameterAttribute.AllParameterSets, StringComparison.OrdinalIgnoreCase)) { customParameterSet = true; } } + + attributes.Add(attribute); } + // If all 'ParameterAttribute' declared for the parameter are hidden due to + // an experimental feature, then the parameter should be ignored. + if (hasParameterAttribute && !hasEnabledParamAttribute) { return null; } + attributes.Reverse(); if (!hasParameterAttribute) { @@ -1476,7 +1497,7 @@ private static RuntimeDefinedParameter GetRuntimeDefinedParameter(ParameterAst p } var result = new RuntimeDefinedParameter(parameterAst.Name.VariablePath.UserPath, parameterAst.StaticType, - new Collection(attributes.ToArray())); + new Collection(attributes)); if (parameterAst.DefaultValue != null) { diff --git a/src/System.Management.Automation/engine/parser/TypeResolver.cs b/src/System.Management.Automation/engine/parser/TypeResolver.cs index e63a53d2eefc..d347457358aa 100644 --- a/src/System.Management.Automation/engine/parser/TypeResolver.cs +++ b/src/System.Management.Automation/engine/parser/TypeResolver.cs @@ -732,6 +732,7 @@ internal static class CoreTypes { typeof(decimal), new[] { "decimal" } }, { typeof(double), new[] { "double" } }, { typeof(DscResourceAttribute), new[] { "DscResource"} }, + { typeof(ExperimentalAttribute), new[] { "Experimental" } }, { typeof(float), new[] { "float", "single" } }, { typeof(Guid), new[] { "guid" } }, { typeof(Hashtable), new[] { "hashtable" } }, diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index 46cf2d9da31b..2a9757b54bd8 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -55,6 +55,7 @@ internal interface IParameterMetadataProvider bool HasAnyScriptBlockAttributes(); RuntimeDefinedParameterDictionary GetParameterMetadata(bool automaticPositions, ref bool usesCmdletBinding); IEnumerable GetScriptBlockAttributes(); + IEnumerable GetExperimentalAttributes(); bool UsesCmdletBinding(); ReadOnlyCollection Parameters { get; } @@ -1370,6 +1371,61 @@ IEnumerable IParameterMetadataProvider.GetScriptBlockAttributes() } } + IEnumerable IParameterMetadataProvider.GetExperimentalAttributes() + { + for (int index = 0; index < Attributes.Count; index++) + { + var attributeAst = Attributes[index]; + var expAttr = GetExpAttributeHelper(attributeAst); + if (expAttr != null) { yield return expAttr; } + } + + if (ParamBlock != null) + { + for (int index = 0; index < ParamBlock.Attributes.Count; index++) + { + var attributeAst = ParamBlock.Attributes[index]; + var expAttr = GetExpAttributeHelper(attributeAst); + if (expAttr != null) { yield return expAttr; } + } + } + + ExperimentalAttribute GetExpAttributeHelper(AttributeAst attributeAst) + { + AttributeAst potentialExpAttr = null; + string expAttrTypeName = typeof(ExperimentalAttribute).FullName; + string attrAstTypeName = attributeAst.TypeName.Name; + + if (TypeAccelerators.Get.TryGetValue(attrAstTypeName, out Type attrType) && attrType == typeof(ExperimentalAttribute)) + { + potentialExpAttr = attributeAst; + } + else if (expAttrTypeName.EndsWith(attrAstTypeName, StringComparison.OrdinalIgnoreCase)) + { + // Handle two cases: + // 1. declare the attribute using full type name; + // 2. declare the attribute using partial type name due to 'using namespace'. + int expAttrLength = expAttrTypeName.Length; + int attrAstLength = attrAstTypeName.Length; + if (expAttrLength == attrAstLength || expAttrTypeName[expAttrLength - attrAstLength - 1] == '.') + { + potentialExpAttr = attributeAst; + } + } + + if (potentialExpAttr != null) + { + try + { + return Compiler.GetAttribute(potentialExpAttr) as ExperimentalAttribute; + } + catch (Exception) { /* catch all and assume it's not a declaration of ExperimentalAttribute */ } + } + + return null; + } + } + ReadOnlyCollection IParameterMetadataProvider.Parameters { get { return (ParamBlock != null) ? this.ParamBlock.Parameters : null; } @@ -3299,6 +3355,11 @@ IEnumerable IParameterMetadataProvider.GetScriptBlockAttributes() return ((IParameterMetadataProvider)_functionDefinitionAst).GetScriptBlockAttributes(); } + IEnumerable IParameterMetadataProvider.GetExperimentalAttributes() + { + return ((IParameterMetadataProvider)_functionDefinitionAst).GetExperimentalAttributes(); + } + bool IParameterMetadataProvider.UsesCmdletBinding() { return ((IParameterMetadataProvider)_functionDefinitionAst).UsesCmdletBinding(); @@ -3412,6 +3473,11 @@ public IEnumerable GetScriptBlockAttributes() return ((IParameterMetadataProvider)Body).GetScriptBlockAttributes(); } + public IEnumerable GetExperimentalAttributes() + { + return ((IParameterMetadataProvider)Body).GetExperimentalAttributes(); + } + public bool UsesCmdletBinding() { return false; @@ -3693,6 +3759,11 @@ IEnumerable IParameterMetadataProvider.GetScriptBlockAttributes() return ((IParameterMetadataProvider)Body).GetScriptBlockAttributes(); } + IEnumerable IParameterMetadataProvider.GetExperimentalAttributes() + { + return ((IParameterMetadataProvider)Body).GetExperimentalAttributes(); + } + ReadOnlyCollection IParameterMetadataProvider.Parameters { get { return Parameters ?? (Body.ParamBlock != null ? Body.ParamBlock.Parameters : null); } diff --git a/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs b/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs index 47e7c3ff2fd8..72e223a873a4 100644 --- a/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs +++ b/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs @@ -348,6 +348,26 @@ internal ObsoleteAttribute ObsoleteAttribute } } + internal ExperimentalAttribute ExperimentalAttribute + { + get + { + if (_expAttribute == ExperimentalAttribute.None) + { + lock (this) + { + if (_expAttribute == ExperimentalAttribute.None) + { + _expAttribute = Ast.GetExperimentalAttributes().FirstOrDefault(); + } + } + } + + return _expAttribute; + } + } + private ExperimentalAttribute _expAttribute = ExperimentalAttribute.None; + public MergedCommandParameterMetadata GetParameterMetadata(ScriptBlock scriptBlock) { if (_parameterMetadata == null) @@ -1228,6 +1248,11 @@ internal ObsoleteAttribute ObsoleteAttribute get { return _scriptBlockData.ObsoleteAttribute; } } + internal ExperimentalAttribute ExperimentalAttribute + { + get { return _scriptBlockData.ExperimentalAttribute; } + } + internal bool Compile(bool optimized) { return _scriptBlockData.Compile(optimized); diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs index 74b0210e761e..ddd148c72df5 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs @@ -1212,8 +1212,12 @@ internal static class FunctionOps ScriptBlock scriptBlock = scriptBlockExpressionWrapper.GetScriptBlock( context, functionDefinitionAst.IsFilter); - context.EngineSessionState.SetFunctionRaw(functionDefinitionAst.Name, - scriptBlock, context.EngineSessionState.CurrentScope.ScopeOrigin); + var expAttribute = scriptBlock.ExperimentalAttribute; + if (expAttribute == null || expAttribute.ToShow) + { + context.EngineSessionState.SetFunctionRaw(functionDefinitionAst.Name, + scriptBlock, context.EngineSessionState.CurrentScope.ScopeOrigin); + } } catch (Exception exception) { diff --git a/src/System.Management.Automation/resources/DiscoveryExceptions.resx b/src/System.Management.Automation/resources/DiscoveryExceptions.resx index 7e1a3bcf5787..795a23a6327d 100644 --- a/src/System.Management.Automation/resources/DiscoveryExceptions.resx +++ b/src/System.Management.Automation/resources/DiscoveryExceptions.resx @@ -218,4 +218,10 @@ The #requires statement must be in one of the following formats: The ShowCommandInfo and Syntax parameters cannot be specified together. + + This script command is disabled when the experimental feature '{0}' has been turned on. + + + This script command is disabled when the experimental feature '{0}' has been turned off. + From 930350e2ee8736225a04f894d4f3f8d20af2b8ce Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 14 Jun 2018 23:25:30 -0700 Subject: [PATCH 02/17] Update code that creates attributes to support the new attribute changes --- .../engine/parser/Compiler.cs | 74 +++++++++++++------ 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index ee910e59ea21..d160ea614988 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -733,6 +733,7 @@ static Compiler() } s_builtinAttributeGenerator.Add(typeof(CmdletBindingAttribute), NewCmdletBindingAttribute); + s_builtinAttributeGenerator.Add(typeof(ExperimentalAttribute), NewExperimentalAttribute); s_builtinAttributeGenerator.Add(typeof(ParameterAttribute), NewParameterAttribute); s_builtinAttributeGenerator.Add(typeof(OutputTypeAttribute), NewOutputTypeAttribute); s_builtinAttributeGenerator.Add(typeof(AliasAttribute), NewAliasAttribute); @@ -1069,6 +1070,9 @@ private static Delegate GetAttributeGenerator(CallInfo callInfo) CallSite>.Create(PSConvertBinder.Get(typeof(ConfirmImpact))); private static readonly CallSite> s_attrArgToRemotingCapabilityConverter = CallSite>.Create(PSConvertBinder.Get(typeof(RemotingCapability))); + private static readonly CallSite> s_attrArgToExperimentActionConverter = + CallSite>.Create(PSConvertBinder.Get(typeof(ExperimentAction))); + private static readonly ConstantValueVisitor s_cvv = new ConstantValueVisitor { AttributeArgument = true }; private static void CheckNoPositionalArgs(AttributeAst ast) { @@ -1093,17 +1097,25 @@ private static void CheckNoNamedArgs(AttributeAst ast) } } + private static (string name, ExperimentAction action) GetFeatureNameAndAction(AttributeAst ast) + { + var argValue0 = ast.PositionalArguments[0].Accept(s_cvv); + var argValue1 = ast.PositionalArguments[1].Accept(s_cvv); + + var featureName = _attrArgToStringConverter.Target(_attrArgToStringConverter, argValue0); + var action = s_attrArgToExperimentActionConverter.Target(s_attrArgToExperimentActionConverter, argValue1); + return (featureName, action); + } + private static Attribute NewCmdletBindingAttribute(AttributeAst ast) { CheckNoPositionalArgs(ast); - var cvv = new ConstantValueVisitor { AttributeArgument = true }; - var result = new CmdletBindingAttribute(); foreach (var namedArg in ast.NamedArguments) { - var argValue = namedArg.Argument.Accept(cvv); + var argValue = namedArg.Argument.Accept(s_cvv); var argumentName = namedArg.ArgumentName; if (argumentName.Equals("DefaultParameterSetName", StringComparison.OrdinalIgnoreCase)) { @@ -1148,17 +1160,42 @@ private static Attribute NewCmdletBindingAttribute(AttributeAst ast) return result; } - private static Attribute NewParameterAttribute(AttributeAst ast) + private static Attribute NewExperimentalAttribute(AttributeAst ast) { - CheckNoPositionalArgs(ast); + int positionalArgCount = ast.PositionalArguments.Count; + if (positionalArgCount != 2) + { + throw InterpreterError.NewInterpreterException(null, typeof(MethodException), ast.Extent, + "MethodCountCouldNotFindBest", ExtendedTypeSystem.MethodArgumentCountException, ".ctor", + positionalArgCount); + } - var cvv = new ConstantValueVisitor { AttributeArgument = true }; + var (name, action) = GetFeatureNameAndAction(ast); + return new ExperimentalAttribute(name, action); + } - var result = new ParameterAttribute(); + private static Attribute NewParameterAttribute(AttributeAst ast) + { + ParameterAttribute result; + int positionalArgCount = ast.PositionalArguments.Count; + switch (positionalArgCount) + { + case 0: + result = new ParameterAttribute(); + break; + case 2: + var (name, action) = GetFeatureNameAndAction(ast); + result = new ParameterAttribute(name, action); + break; + default: + throw InterpreterError.NewInterpreterException(null, typeof(MethodException), ast.Extent, + "MethodCountCouldNotFindBest", ExtendedTypeSystem.MethodArgumentCountException, ".ctor", + positionalArgCount); + } foreach (var namedArg in ast.NamedArguments) { - var argValue = namedArg.Argument.Accept(cvv); + var argValue = namedArg.Argument.Accept(s_cvv); var argumentName = namedArg.ArgumentName; if (argumentName.Equals("Position", StringComparison.OrdinalIgnoreCase)) @@ -1214,8 +1251,6 @@ private static Attribute NewParameterAttribute(AttributeAst ast) private static Attribute NewOutputTypeAttribute(AttributeAst ast) { - var cvv = new ConstantValueVisitor { AttributeArgument = true }; - OutputTypeAttribute result; if (ast.PositionalArguments.Count == 0) { @@ -1231,7 +1266,7 @@ private static Attribute NewOutputTypeAttribute(AttributeAst ast) } else { - var argValue = ast.PositionalArguments[0].Accept(cvv); + var argValue = ast.PositionalArguments[0].Accept(s_cvv); result = new OutputTypeAttribute(_attrArgToStringConverter.Target(_attrArgToStringConverter, argValue)); } } @@ -1244,7 +1279,7 @@ private static Attribute NewOutputTypeAttribute(AttributeAst ast) var typeArg = positionalArgument as TypeExpressionAst; args[i] = typeArg != null ? TypeOps.ResolveTypeName(typeArg.TypeName, typeArg.Extent) - : positionalArgument.Accept(cvv); + : positionalArgument.Accept(s_cvv); } if (args[0] is Type) @@ -1259,7 +1294,7 @@ private static Attribute NewOutputTypeAttribute(AttributeAst ast) foreach (var namedArg in ast.NamedArguments) { - var argValue = namedArg.Argument.Accept(cvv); + var argValue = namedArg.Argument.Accept(s_cvv); var argumentName = namedArg.ArgumentName; if (argumentName.Equals("ParameterSetName", StringComparison.OrdinalIgnoreCase)) @@ -1285,12 +1320,11 @@ private static Attribute NewAliasAttribute(AttributeAst ast) { CheckNoNamedArgs(ast); - var cvv = new ConstantValueVisitor { AttributeArgument = true }; var args = new string[ast.PositionalArguments.Count]; for (int i = 0; i < ast.PositionalArguments.Count; i++) { args[i] = _attrArgToStringConverter.Target(_attrArgToStringConverter, - ast.PositionalArguments[i].Accept(cvv)); + ast.PositionalArguments[i].Accept(s_cvv)); } return new AliasAttribute(args); } @@ -1298,7 +1332,6 @@ private static Attribute NewAliasAttribute(AttributeAst ast) private static Attribute NewValidateSetAttribute(AttributeAst ast) { ValidateSetAttribute result; - var cvv = new ConstantValueVisitor { AttributeArgument = true }; // 'ValidateSet([CustomGeneratorType], IgnoreCase=$false)' is supported in scripts. if (ast.PositionalArguments.Count == 1 && ast.PositionalArguments[0] is TypeExpressionAst generatorTypeAst) @@ -1322,7 +1355,7 @@ private static Attribute NewValidateSetAttribute(AttributeAst ast) for (int i = 0; i < ast.PositionalArguments.Count; i++) { args[i] = _attrArgToStringConverter.Target(_attrArgToStringConverter, - ast.PositionalArguments[i].Accept(cvv)); + ast.PositionalArguments[i].Accept(s_cvv)); } result = new ValidateSetAttribute(args); @@ -1330,7 +1363,7 @@ private static Attribute NewValidateSetAttribute(AttributeAst ast) foreach (var namedArg in ast.NamedArguments) { - var argValue = namedArg.Argument.Accept(cvv); + var argValue = namedArg.Argument.Accept(s_cvv); var argumentName = namedArg.ArgumentName; if (argumentName.Equals("IgnoreCase", StringComparison.OrdinalIgnoreCase)) { @@ -1395,17 +1428,16 @@ internal static Attribute GetAttribute(AttributeAst attributeAst) var delegateArgs = new object[totalArgCount + 1]; delegateArgs[0] = attributeType; - var cvv = new ConstantValueVisitor { AttributeArgument = true }; int i = 1; for (int index = 0; index < attributeAst.PositionalArguments.Count; index++) { var posArg = attributeAst.PositionalArguments[index]; - delegateArgs[i++] = posArg.Accept(cvv); + delegateArgs[i++] = posArg.Accept(s_cvv); } for (int index = 0; index < attributeAst.NamedArguments.Count; index++) { var namedArg = attributeAst.NamedArguments[index]; - delegateArgs[i++] = namedArg.Argument.Accept(cvv); + delegateArgs[i++] = namedArg.Argument.Accept(s_cvv); } try From 8a5ddbb231b1009c09134a80860b4d278deb5c40 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 18 Jun 2018 18:12:03 -0700 Subject: [PATCH 03/17] Support experimental feature in modules --- .../engine/Modules/ModuleCmdletBase.cs | 80 +++++++++++++++++-- .../engine/Modules/PSModuleInfo.cs | 63 +++++++-------- .../resources/Modules.resx | 6 ++ 3 files changed, 109 insertions(+), 40 deletions(-) diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 11107ceac1ad..67aaf6309e0f 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -2027,12 +2027,75 @@ internal bool LoadModuleManifestData(ExternalScriptInfo scriptInfo, ManifestProc Array.Clear(tmpNestedModules, 0, tmpNestedModules.Length); } - // Set the private data member for the module if the manifest contains - // this member - object privateData = null; - if (data.Contains("PrivateData")) + // Set the private data member for the module if the manifest contains this member + object privateData = data["PrivateData"]; + + // Validate the 'ExperimentalFeatures' member of the manifest + List expFeatureList = null; + if (privateData is Hashtable hashData && hashData["PSData"] is Hashtable psData) { - privateData = data["PrivateData"]; + if (!GetScalarFromData(psData, moduleManifestPath, "ExperimentalFeatures", manifestProcessingFlags, out Hashtable[] features)) + { + containedErrors = true; + if (bailOnFirstError) return null; + } + + if (features != null && features.Length > 0) + { + bool nameMissingOrEmpty = false; + var invalidNames = new List(); + string moduleName = ModuleIntrinsics.GetModuleName(moduleManifestPath); + expFeatureList = new List(features.Length); + + foreach (var feature in features) + { + string featureName = feature["Name"] as string; + if (String.IsNullOrEmpty(featureName)) + { + nameMissingOrEmpty = true; + continue; + } + + if (ExperimentalFeature.IsModuleFeatureName(featureName, moduleName)) + { + string featureDescription = feature["Description"] as string; + expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, moduleName, + ExperimentalFeature.HasEnabled(featureName))); + } + else + { + invalidNames.Add(featureName); + } + } + + if (nameMissingOrEmpty) + { + if (writingErrors) + { + WriteError(new ErrorRecord(new ArgumentException(Modules.ExperimentalFeatureNameMissingOrEmpty), + "Modules_ExperimentalFeatureNameMissingOrEmpty", + ErrorCategory.InvalidData, null)); + } + + containedErrors = true; + if (bailOnFirstError) { return null; } + } + + if (invalidNames.Count > 0) + { + if (writingErrors) + { + string invalidNameStr = String.Join(',', invalidNames); + string errorMsg = StringUtil.Format(Modules.InvalidExperimentalFeatureName, invalidNameStr); + WriteError(new ErrorRecord(new ArgumentException(errorMsg), + "Modules_InvalidExperimentalFeatureName", + ErrorCategory.InvalidData, null)); + } + + containedErrors = true; + if (bailOnFirstError) { return null; } + } + } } // Process all of the exports... @@ -2413,6 +2476,12 @@ internal bool LoadModuleManifestData(ExternalScriptInfo scriptInfo, ManifestProc manifestInfo.PowerShellVersion = powerShellVersion; manifestInfo.ProcessorArchitecture = requiredProcessorArchitecture; manifestInfo.Prefix = resolvedCommandPrefix; + + if (expFeatureList != null) + { + manifestInfo.ExperimentalFeatures = new ReadOnlyCollection(expFeatureList); + } + if (assemblyList != null) { foreach (var a in assemblyList) @@ -3009,6 +3078,7 @@ internal bool LoadModuleManifestData(ExternalScriptInfo scriptInfo, ManifestProc newManifestInfo.LicenseUri = manifestInfo.LicenseUri; newManifestInfo.IconUri = manifestInfo.IconUri; newManifestInfo.RepositorySourceLocation = manifestInfo.RepositorySourceLocation; + newManifestInfo.ExperimentalFeatures = manifestInfo.ExperimentalFeatures; // If we are in module discovery, then fix the path. if (ss == null) diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs index d0e13d9a6ad3..3bf5c0d9a07c 100644 --- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs +++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs @@ -301,49 +301,37 @@ private void SetPSDataPropertiesFromPrivateData() ProjectUri = null; IconUri = null; - var privateDataHashTable = _privateData as Hashtable; - if (privateDataHashTable != null) + if (_privateData is Hashtable hashData && hashData["PSData"] is Hashtable psData) { - var psData = privateDataHashTable["PSData"] as Hashtable; - if (psData != null) + var tagsValue = psData["Tags"]; + if (tagsValue is object[] tags && tags.Length > 0) { - object tagsValue = psData["Tags"]; - if (tagsValue != null) + foreach (var tagString in tags.OfType()) { - var tags = tagsValue as object[]; - if (tags != null && tags.Any()) - { - foreach (var tagString in tags.OfType()) - { - AddToTags(tagString); - } - } - else - { - AddToTags(tagsValue.ToString()); - } - } - - var licenseUri = psData["LicenseUri"] as string; - if (licenseUri != null) - { - LicenseUri = GetUriFromString(licenseUri); + AddToTags(tagString); } + } + else if (tagsValue is string tag) + { + AddToTags(tag); + } - var projectUri = psData["ProjectUri"] as string; - if (projectUri != null) - { - ProjectUri = GetUriFromString(projectUri); - } + if (psData["LicenseUri"] is string licenseUri) + { + LicenseUri = GetUriFromString(licenseUri); + } - var iconUri = psData["IconUri"] as string; - if (iconUri != null) - { - IconUri = GetUriFromString(iconUri); - } + if (psData["ProjectUri"] is string projectUri) + { + ProjectUri = GetUriFromString(projectUri); + } - ReleaseNotes = psData["ReleaseNotes"] as string; + if (psData["IconUri"] is string iconUri) + { + IconUri = GetUriFromString(iconUri); } + + ReleaseNotes = psData["ReleaseNotes"] as string; } } @@ -360,6 +348,11 @@ private static Uri GetUriFromString(string uriString) return uri; } + /// + /// Get the experimental features declared in this module. + /// + public IEnumerable ExperimentalFeatures { get; internal set; } = Utils.EmptyReadOnlyCollection(); + /// /// Tags of this module. /// diff --git a/src/System.Management.Automation/resources/Modules.resx b/src/System.Management.Automation/resources/Modules.resx index af889e155613..11bf9e477694 100644 --- a/src/System.Management.Automation/resources/Modules.resx +++ b/src/System.Management.Automation/resources/Modules.resx @@ -606,4 +606,10 @@ This prerequisite is valid for the PowerShell Desktop edition only. + + A non-empty string value should be specified for an experimental feature declared in the module manifest. + + + One or more invalid experimental feature names found: {0}. A module experimental feature name should follow this convention: 'ModuleName.FeatureName'. + From 7770aa547e3921b93aa00cccd41aa13512beecac Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 26 Jun 2018 12:37:43 -0700 Subject: [PATCH 04/17] Add 'Get-ExperimentalFeature' cmdlet --- .../ExperimentalFeature.cs | 2 +- .../GetExperimentalFeatureCommand.cs | 180 ++++++++++++++++++ .../engine/InitialSessionState.cs | 1 + .../engine/Modules/ModuleCmdletBase.cs | 4 +- .../engine/Modules/ModuleIntrinsics.cs | 97 ++++++---- .../engine/SessionState.cs | 11 +- .../utils/PsUtils.cs | 1 + 7 files changed, 249 insertions(+), 47 deletions(-) create mode 100644 src/System.Management.Automation/engine/ExperimentalFeature/GetExperimentalFeatureCommand.cs diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index 3c6b2060fbaf..7a76b0075be9 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -286,7 +286,7 @@ public ExperimentalAttribute(string experimentName, ExperimentAction experimentA /// /// An instance that represents the none-value. /// - internal readonly static ExperimentalAttribute None = new ExperimentalAttribute(); + internal static readonly ExperimentalAttribute None = new ExperimentalAttribute(); internal bool ToHide => EffectiveAction == ExperimentAction.Hide; internal bool ToShow => EffectiveAction == ExperimentAction.Show; diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/GetExperimentalFeatureCommand.cs b/src/System.Management.Automation/engine/ExperimentalFeature/GetExperimentalFeatureCommand.cs new file mode 100644 index 000000000000..2ab20492d116 --- /dev/null +++ b/src/System.Management.Automation/engine/ExperimentalFeature/GetExperimentalFeatureCommand.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Internal; + +namespace Microsoft.PowerShell.Commands +{ + /// + /// Implements Get-ExperimentalFeature cmdlet. + /// + [Cmdlet(VerbsCommon.Get, "ExperimentalFeature", HelpUri = "")] + public class GetExperimentalFeatureCommand : PSCmdlet + { + /// + /// Specify the feature names. + /// + [Parameter(ValueFromPipeline = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string[] Name { get; set; } + + /// + /// Search module paths to find all available experimental features. + /// + [Parameter()] + public SwitchParameter ListAvailable { get; set; } + + /// + /// ProcessRecord method of this cmdlet. + /// + protected override void ProcessRecord() + { + const WildcardOptions wildcardOptions = WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant; + IEnumerable namePatterns = SessionStateUtilities.CreateWildcardsFromStrings(Name, wildcardOptions); + + if (ListAvailable) + { + foreach (ExperimentalFeature feature in GetAvailableExperimentalFeatures(namePatterns).OrderBy(GetSortingString)) + { + WriteObject(feature); + } + } + else if (ExperimentalFeature.EnabledExperimentalFeatureNames.Count > 0) + { + foreach (ExperimentalFeature feature in GetEnabledExperimentalFeatures(namePatterns).OrderBy(GetSortingString)) + { + WriteObject(feature); + } + } + } + + /// + /// Construct the string for sorting experimental feature records. + /// + /// + /// Engine features come before module features. + /// Within engine features and more features, features are ordered by name. + /// + private static string GetSortingString(ExperimentalFeature feature) + { + if (ExperimentalFeature.EngineSource.Equals(feature.Source)) + { + return "0:" + feature.Name; + } + else + { + return "1:" + feature.Name; + } + } + + /// + /// Get enabled experimental features based on the specified name patterns. + /// + private IEnumerable GetEnabledExperimentalFeatures(IEnumerable namePatterns) + { + var moduleFeatures = new List(); + var moduleNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (string featureName in ExperimentalFeature.EnabledExperimentalFeatureNames) + { + // Only process the feature names that matches any name patterns. + if (SessionStateUtilities.MatchesAnyWildcardPattern(featureName, namePatterns, defaultValue: true)) + { + if (ExperimentalFeature.EngineExperimentalFeatureMap.TryGetValue(featureName, out ExperimentalFeature feature)) + { + yield return feature; + } + else + { + moduleFeatures.Add(featureName); + int lastDotIndex = featureName.LastIndexOf('.'); + moduleNames.Add(featureName.Substring(0, lastDotIndex)); + } + } + } + + if (moduleFeatures.Count > 0) + { + var featuresFromGivenModules = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string moduleFile in GetValidModuleFiles(moduleNames)) + { + ExperimentalFeature[] features = ModuleIntrinsics.GetExperimentalFeature(moduleFile); + foreach (var feature in features) + { + featuresFromGivenModules.TryAdd(feature.Name, feature); + } + } + + foreach (string featureName in moduleFeatures) + { + if (featuresFromGivenModules.TryGetValue(featureName, out ExperimentalFeature feature)) + { + yield return feature; + } + else + { + yield return new ExperimentalFeature(featureName, description: null, source: null, isEnabled: true); + } + } + } + } + + /// + /// Get available experimental features based on the specified name patterns. + /// + private IEnumerable GetAvailableExperimentalFeatures(IEnumerable namePatterns) + { + foreach (ExperimentalFeature feature in ExperimentalFeature.EngineExperimentalFeatures) + { + if (SessionStateUtilities.MatchesAnyWildcardPattern(feature.Name, namePatterns, defaultValue: true)) + { + yield return feature; + } + } + + foreach (string moduleFile in GetValidModuleFiles(moduleNamesToFind: null)) + { + ExperimentalFeature[] features = ModuleIntrinsics.GetExperimentalFeature(moduleFile); + foreach (var feature in features) + { + if (SessionStateUtilities.MatchesAnyWildcardPattern(feature.Name, namePatterns, defaultValue: true)) + { + yield return feature; + } + } + } + } + + /// + /// Get valid module files from module paths. + /// + private IEnumerable GetValidModuleFiles(HashSet moduleNamesToFind) + { + var modulePaths = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string path in ModuleIntrinsics.GetModulePath(includeSystemModulePath: false, Context)) + { + string uniquePath = path.TrimEnd(Utils.Separators.Directory); + if (!modulePaths.Add(uniquePath)) { continue; } + + foreach (string moduleFile in ModuleUtils.GetDefaultAvailableModuleFiles(uniquePath)) + { + // We only care about module manifest files because that's where experimental features are declared. + if (!moduleFile.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase)) { continue; } + + if (moduleNamesToFind != null) + { + string currentModuleName = ModuleIntrinsics.GetModuleName(moduleFile); + if (!moduleNamesToFind.Contains(currentModuleName)) { continue; } + } + + yield return moduleFile; + } + } + } + } +} \ No newline at end of file diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index c31eba64b57c..90bec19566d4 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -5277,6 +5277,7 @@ internal static Assembly LoadPSSnapInAssembly(PSSnapInInfo psSnapInInfo) {"Export-ModuleMember", new SessionStateCmdletEntry("Export-ModuleMember", typeof(ExportModuleMemberCommand), helpFile) }, {"ForEach-Object", new SessionStateCmdletEntry("ForEach-Object", typeof(ForEachObjectCommand), helpFile) }, {"Get-Command", new SessionStateCmdletEntry("Get-Command", typeof(GetCommandCommand), helpFile) }, + {"Get-ExperimentalFeature", new SessionStateCmdletEntry("Get-ExperimentalFeature", typeof(GetExperimentalFeatureCommand), helpFile) }, {"Get-Help", new SessionStateCmdletEntry("Get-Help", typeof(GetHelpCommand), helpFile) }, {"Get-History", new SessionStateCmdletEntry("Get-History", typeof(GetHistoryCommand), helpFile) }, {"Get-Job", new SessionStateCmdletEntry("Get-Job", typeof(GetJobCommand), helpFile) }, diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 67aaf6309e0f..79ec351b28d2 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -2059,8 +2059,8 @@ internal bool LoadModuleManifestData(ExternalScriptInfo scriptInfo, ManifestProc if (ExperimentalFeature.IsModuleFeatureName(featureName, moduleName)) { string featureDescription = feature["Description"] as string; - expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, moduleName, - ExperimentalFeature.HasEnabled(featureName))); + expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, moduleManifestPath, + ExperimentalFeature.HasEnabled(featureName))); } else { diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index 8451daa80f17..99fcdb89eb02 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections; using System.Collections.Generic; using System.IO; using System.Management.Automation.Configuration; @@ -414,62 +415,90 @@ internal static bool IsModuleMatchingModuleSpec(PSModuleInfo moduleInfo, ModuleS internal static Version GetManifestModuleVersion(string manifestPath) { - if (manifestPath != null && - manifestPath.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase)) + try { - try - { - var dataFileSetting = - PsUtils.GetModuleManifestProperties( - manifestPath, - PsUtils.ManifestModuleVersionPropertyName); + Hashtable dataFileSetting = + PsUtils.GetModuleManifestProperties( + manifestPath, + PsUtils.ManifestModuleVersionPropertyName); - var versionValue = dataFileSetting["ModuleVersion"]; - if (versionValue != null) + object versionValue = dataFileSetting["ModuleVersion"]; + if (versionValue != null) + { + Version moduleVersion; + if (LanguagePrimitives.TryConvertTo(versionValue, out moduleVersion)) { - Version moduleVersion; - if (LanguagePrimitives.TryConvertTo(versionValue, out moduleVersion)) - { - return moduleVersion; - } + return moduleVersion; } } - catch (PSInvalidOperationException) - { - } } + catch (PSInvalidOperationException) { } return new Version(0, 0); } internal static Guid GetManifestGuid(string manifestPath) { - if (manifestPath != null && - manifestPath.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase)) + try { - try + Hashtable dataFileSetting = + PsUtils.GetModuleManifestProperties( + manifestPath, + PsUtils.ManifestGuidPropertyName); + + object guidValue = dataFileSetting["GUID"]; + if (guidValue != null) { - var dataFileSetting = - PsUtils.GetModuleManifestProperties( - manifestPath, - PsUtils.ManifestGuidPropertyName); + Guid guidID; + if (LanguagePrimitives.TryConvertTo(guidValue, out guidID)) + { + return guidID; + } + } + } + catch (PSInvalidOperationException) { } - var guidValue = dataFileSetting["GUID"]; - if (guidValue != null) + return new Guid(); + } + + internal static ExperimentalFeature[] GetExperimentalFeature(string manifestPath) + { + try + { + Hashtable dataFileSetting = + PsUtils.GetModuleManifestProperties( + manifestPath, + PsUtils.ManifestPrivateDataPropertyName); + + object privateData = dataFileSetting["PrivateData"]; + if (privateData is Hashtable hashData && hashData["PSData"] is Hashtable psData) + { + object expFeatureValue = psData["ExperimentalFeatures"]; + if (expFeatureValue != null && + LanguagePrimitives.TryConvertTo(expFeatureValue, out Hashtable[] features) && + features.Length > 0) { - Guid guidID; - if (LanguagePrimitives.TryConvertTo(guidValue, out guidID)) + string moduleName = ModuleIntrinsics.GetModuleName(manifestPath); + var expFeatureList = new List(); + foreach (Hashtable feature in features) { - return guidID; + string featureName = feature["Name"] as string; + if (String.IsNullOrEmpty(featureName)) { continue; } + + if (ExperimentalFeature.IsModuleFeatureName(featureName, moduleName)) + { + string featureDescription = feature["Description"] as string; + expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, manifestPath, + ExperimentalFeature.HasEnabled(featureName))); + } } + return expFeatureList.ToArray(); } } - catch (PSInvalidOperationException) - { - } } + catch (PSInvalidOperationException) { } - return new Guid(); + return Utils.EmptyArray(); } // The extensions of all of the files that can be processed with Import-Module, put the ni.dll in front of .dll to have higher priority to be loaded. diff --git a/src/System.Management.Automation/engine/SessionState.cs b/src/System.Management.Automation/engine/SessionState.cs index 516828c743dc..4f66c4211050 100644 --- a/src/System.Management.Automation/engine/SessionState.cs +++ b/src/System.Management.Automation/engine/SessionState.cs @@ -357,7 +357,6 @@ internal void InitializeFixedVariables() // $ShellId - if there is no runspace config, use the default string string shellId = ExecutionContext.ShellID; - v = new PSVariable(SpecialVariables.ShellId, shellId, ScopedItemOptions.Constant | ScopedItemOptions.AllScope, RunspaceInit.MshShellIdDescription); @@ -366,18 +365,10 @@ internal void InitializeFixedVariables() // $PSHOME // This depends on the shellId. If we cannot read the application base // registry key, set the variable to empty string - string applicationBase = string.Empty; - try - { - applicationBase = Utils.GetApplicationBase(shellId); - } - catch (SecurityException) - { - } + string applicationBase = Utils.DefaultPowerShellAppBase; v = new PSVariable(SpecialVariables.PSHome, applicationBase, ScopedItemOptions.Constant | ScopedItemOptions.AllScope, RunspaceInit.PSHOMEDescription); - this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true); } diff --git a/src/System.Management.Automation/utils/PsUtils.cs b/src/System.Management.Automation/utils/PsUtils.cs index 3fe10bacba27..57340056327c 100644 --- a/src/System.Management.Automation/utils/PsUtils.cs +++ b/src/System.Management.Automation/utils/PsUtils.cs @@ -477,6 +477,7 @@ internal static string GetUsingExpressionKey(Language.UsingExpressionAst usingAs internal static readonly string[] ManifestModuleVersionPropertyName = new[] { "ModuleVersion" }; internal static readonly string[] ManifestGuidPropertyName = new[] { "GUID" }; internal static readonly string[] FastModuleManifestAnalysisPropertyNames = new[] { "AliasesToExport", "CmdletsToExport", "FunctionsToExport", "NestedModules", "RootModule", "ModuleToProcess", "ModuleVersion" }; + internal static readonly string[] ManifestPrivateDataPropertyName = new[] { "PrivateData" }; internal static Hashtable GetModuleManifestProperties(string psDataFilePath, string[] keys) { From dc2d741791ac0f94d6eb2d322da1a9c7b87a1e95 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 27 Jun 2018 17:16:21 -0700 Subject: [PATCH 05/17] Add logging, formatting, and '$EnabledExperimentalFeatures' --- .../PowerShell.Core.Instrumentation.man | 65 +++++++++ .../PowerShellCore_format_ps1xml.cs | 31 ++++ .../ExperimentalFeature.cs | 132 +++++++++--------- .../engine/Modules/ModuleCmdletBase.cs | 2 +- .../engine/SessionState.cs | 9 +- .../engine/SpecialVariables.cs | 16 +-- .../engine/parser/TypeResolver.cs | 4 +- .../engine/remoting/common/PSETWTracer.cs | 5 + .../resources/Logging.resx | 9 ++ .../resources/RunspaceInit.resx | 3 + 10 files changed, 193 insertions(+), 83 deletions(-) diff --git a/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man b/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man index 33c0a5f519a5..491e4a874612 100644 --- a/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man +++ b/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man @@ -98,6 +98,29 @@ value="0xD003" version="1" /> + + + + + +