402 lines
17 KiB
C#
402 lines
17 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Management.Automation.Configuration;
|
|
using System.Management.Automation.Internal;
|
|
using System.Management.Automation.Tracing;
|
|
using System.Runtime.CompilerServices;
|
|
using Microsoft.PowerShell.Telemetry;
|
|
|
|
namespace System.Management.Automation
|
|
{
|
|
/// <summary>
|
|
/// Support experimental features in PowerShell.
|
|
/// </summary>
|
|
public class ExperimentalFeature
|
|
{
|
|
#region Const Members
|
|
|
|
internal const string EngineSource = "PSEngine";
|
|
internal const string PSAnsiProgressFeatureName = "PSAnsiProgress";
|
|
internal const string PSNativeCommandArgumentPassingFeatureName = "PSNativeCommandArgumentPassing";
|
|
|
|
#endregion
|
|
|
|
#region Instance Members
|
|
|
|
/// <summary>
|
|
/// Name of an experimental feature.
|
|
/// </summary>
|
|
public string Name { get; }
|
|
|
|
/// <summary>
|
|
/// Description of an experimental feature.
|
|
/// </summary>
|
|
public string Description { get; }
|
|
|
|
/// <summary>
|
|
/// Source of an experimental feature.
|
|
/// </summary>
|
|
public string Source { get; }
|
|
|
|
/// <summary>
|
|
/// Indicate whether the feature is enabled.
|
|
/// </summary>
|
|
public bool Enabled { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ExperimentalFeature"/> class.
|
|
/// </summary>
|
|
/// <param name="name">The name of the experimental feature.</param>
|
|
/// <param name="description">A description of the experimental feature.</param>
|
|
/// <param name="source">The source where the experimental feature is defined.</param>
|
|
/// <param name="isEnabled">Indicate whether the experimental feature is enabled.</param>
|
|
internal ExperimentalFeature(string name, string description, string source, bool isEnabled)
|
|
{
|
|
Name = name;
|
|
Description = description;
|
|
Source = source;
|
|
Enabled = isEnabled;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ExperimentalFeature"/> class.
|
|
/// This is a private constructor only for declaring new experimental features within this type.
|
|
/// </summary>
|
|
/// <param name="name">The name of the experimental feature.</param>
|
|
/// <param name="description">A description of the experimental feature.</param>
|
|
private ExperimentalFeature(string name, string description)
|
|
: this(name, description, source: EngineSource, isEnabled: false)
|
|
{
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Static Members
|
|
|
|
/// <summary>
|
|
/// All available engine experimental features.
|
|
/// </summary>
|
|
internal static readonly ReadOnlyCollection<ExperimentalFeature> EngineExperimentalFeatures;
|
|
|
|
/// <summary>
|
|
/// A dictionary of all available engine experimental features. Feature name is the key.
|
|
/// </summary>
|
|
internal static readonly ReadOnlyDictionary<string, ExperimentalFeature> EngineExperimentalFeatureMap;
|
|
|
|
/// <summary>
|
|
/// Experimental feature names that are enabled in the config file.
|
|
/// </summary>
|
|
internal static readonly ReadOnlyBag<string> EnabledExperimentalFeatureNames;
|
|
|
|
/// <summary>
|
|
/// Type initializer. Initialize the engine experimental feature list.
|
|
/// </summary>
|
|
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"),
|
|
*/
|
|
new ExperimentalFeature(
|
|
name: "PSImplicitRemotingBatching",
|
|
description: "Batch implicit remoting proxy commands to improve performance"),
|
|
new ExperimentalFeature(
|
|
name: "PSCommandNotFoundSuggestion",
|
|
description: "Recommend potential commands based on fuzzy search on a CommandNotFoundException"),
|
|
#if UNIX
|
|
new ExperimentalFeature(
|
|
name: "PSUnixFileStat",
|
|
description: "Provide unix permission information for files and directories"),
|
|
#endif
|
|
new ExperimentalFeature(
|
|
name: "PSCultureInvariantReplaceOperator",
|
|
description: "Use culture invariant to-string convertor for lval in replace operator"),
|
|
new ExperimentalFeature(
|
|
name: "PSNativePSPathResolution",
|
|
description: "Convert PSPath to filesystem path, if possible, for native commands"),
|
|
new ExperimentalFeature(
|
|
name: "PSNotApplyErrorActionToStderr",
|
|
description: "Don't have $ErrorActionPreference affect stderr output"),
|
|
new ExperimentalFeature(
|
|
name: "PS7DscSupport",
|
|
description: "Support the cross-platform class-based DSC"),
|
|
new ExperimentalFeature(
|
|
name: "PSSubsystemPluginModel",
|
|
description: "A plugin model for registering and un-registering PowerShell subsystems"),
|
|
new ExperimentalFeature(
|
|
name: "PSAnsiRendering",
|
|
description: "Enable $PSStyle variable to control ANSI rendering of strings"),
|
|
new ExperimentalFeature(
|
|
name: PSAnsiProgressFeatureName,
|
|
description: "Enable lightweight progress bar that leverages ANSI codes for rendering"),
|
|
new ExperimentalFeature(
|
|
name: PSNativeCommandArgumentPassingFeatureName,
|
|
description: "Use ArgumentList when invoking a native command"),
|
|
new ExperimentalFeature(
|
|
name: "PSLoadAssemblyFromNativeCode",
|
|
description: "Expose an API to allow assembly loading from native code"),
|
|
};
|
|
|
|
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);
|
|
|
|
// Initialize the readonly dictionary 'EngineExperimentalFeatureMap'.
|
|
var engineExpFeatureMap = engineFeatures.ToDictionary(static f => f.Name, StringComparer.OrdinalIgnoreCase);
|
|
EngineExperimentalFeatureMap = new ReadOnlyDictionary<string, ExperimentalFeature>(engineExpFeatureMap);
|
|
|
|
// Initialize the readonly 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 = Array.Empty<string>();
|
|
try
|
|
{
|
|
enabledFeatures = PowerShellConfig.Instance.GetExperimentalFeatures();
|
|
}
|
|
catch (Exception e) when (LogException(e)) { }
|
|
|
|
EnabledExperimentalFeatureNames = ProcessEnabledFeatures(enabledFeatures);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process the array of enabled feature names retrieved from configuration.
|
|
/// Ignore invalid feature names and unavailable engine feature names, and
|
|
/// return an ReadOnlyBag of the valid enabled feature names.
|
|
/// </summary>
|
|
private static ReadOnlyBag<string> ProcessEnabledFeatures(string[] enabledFeatures)
|
|
{
|
|
if (enabledFeatures.Length == 0) { return ReadOnlyBag<string>.Empty; }
|
|
|
|
var list = new List<string>(enabledFeatures.Length);
|
|
foreach (string name in enabledFeatures)
|
|
{
|
|
if (IsModuleFeatureName(name))
|
|
{
|
|
list.Add(name);
|
|
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ExperimentalModuleFeatureActivation, name);
|
|
}
|
|
else if (IsEngineFeatureName(name))
|
|
{
|
|
if (EngineExperimentalFeatureMap.TryGetValue(name, out ExperimentalFeature feature))
|
|
{
|
|
feature.Enabled = true;
|
|
list.Add(name);
|
|
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ExperimentalEngineFeatureActivation, name);
|
|
}
|
|
else
|
|
{
|
|
string message = StringUtil.Format(Logging.EngineExperimentalFeatureNotFound, name);
|
|
LogError(PSEventId.ExperimentalFeature_InvalidName, name, message);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string message = StringUtil.Format(Logging.InvalidExperimentalFeatureName, name);
|
|
LogError(PSEventId.ExperimentalFeature_InvalidName, name, message);
|
|
}
|
|
}
|
|
|
|
return new ReadOnlyBag<string>(new HashSet<string>(list, StringComparer.OrdinalIgnoreCase));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Log the exception without rewinding the stack.
|
|
/// </summary>
|
|
private static bool LogException(Exception e)
|
|
{
|
|
LogError(PSEventId.ExperimentalFeature_ReadConfig_Error, e.GetType().FullName, e.Message, e.StackTrace);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Log an error message.
|
|
/// </summary>
|
|
private static void LogError(PSEventId eventId, params object[] args)
|
|
{
|
|
PSEtwLog.LogOperationalError(eventId, PSOpcode.Constructor, PSTask.ExperimentalFeature, PSKeyword.UseAlwaysOperational, args);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the name follows the engine experimental feature name convention.
|
|
/// Convention: prefix 'PS' to the feature name -- 'PSFeatureName'.
|
|
/// </summary>
|
|
internal static bool IsEngineFeatureName(string featureName)
|
|
{
|
|
return featureName.Length > 2 && !featureName.Contains('.') && featureName.StartsWith("PS", StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the name follows the module experimental feature name convention.
|
|
/// Convention: prefix the module name to the feature name -- 'ModuleName.FeatureName'.
|
|
/// </summary>
|
|
/// <param name="featureName">The feature name to check.</param>
|
|
/// <param name="moduleName">When specified, we check if the feature name matches the module name.</param>
|
|
internal static bool IsModuleFeatureName(string featureName, string moduleName = null)
|
|
{
|
|
// Feature names cannot start with a dot
|
|
if (featureName.StartsWith('.'))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Feature names must contain a dot, but not at the end
|
|
int lastDotIndex = featureName.LastIndexOf('.');
|
|
if (lastDotIndex == -1 || lastDotIndex == featureName.Length - 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (moduleName == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// If the module name is given, it must match the prefix of the feature name (up to the last dot).
|
|
var moduleNamePart = featureName.AsSpan(0, lastDotIndex);
|
|
return moduleNamePart.Equals(moduleName.AsSpan(), StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determine the action to take for the specified experiment name and action.
|
|
/// </summary>
|
|
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 (!IsEnabled(experimentName))
|
|
{
|
|
action = (action == ExperimentAction.Hide) ? ExperimentAction.Show : ExperimentAction.Hide;
|
|
}
|
|
|
|
return action;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the specified experimental feature has been enabled.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool IsEnabled(string featureName)
|
|
{
|
|
return EnabledExperimentalFeatureNames.Contains(featureName);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates the action to take on the cmdlet/parameter that has the attribute declared.
|
|
/// </summary>
|
|
public enum ExperimentAction
|
|
{
|
|
/// <summary>
|
|
/// Represent an undefined action, used as the default value.
|
|
/// </summary>
|
|
None = 0,
|
|
|
|
/// <summary>
|
|
/// Hide the cmdlet/parameter when the corresponding experimental feature is enabled.
|
|
/// </summary>
|
|
Hide = 1,
|
|
|
|
/// <summary>
|
|
/// Show the cmdlet/parameter when the corresponding experimental feature is enabled.
|
|
/// </summary>
|
|
Show = 2
|
|
}
|
|
|
|
/// <summary>
|
|
/// The attribute that applies to cmdlet/function/parameter to define what the engine should do with it.
|
|
/// </summary>
|
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property)]
|
|
public sealed class ExperimentalAttribute : ParsingBaseAttribute
|
|
{
|
|
/// <summary>
|
|
/// Get name of the experimental feature this attribute is associated with.
|
|
/// </summary>
|
|
public string ExperimentName { get; }
|
|
|
|
/// <summary>
|
|
/// Get action for engine to take when the experimental feature is enabled.
|
|
/// </summary>
|
|
public ExperimentAction ExperimentAction { get; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the ExperimentalAttribute class.
|
|
/// </summary>
|
|
public ExperimentalAttribute(string experimentName, ExperimentAction experimentAction)
|
|
{
|
|
ValidateArguments(experimentName, experimentAction);
|
|
ExperimentName = experimentName;
|
|
ExperimentAction = experimentAction;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize an instance that represents the none-value.
|
|
/// </summary>
|
|
private ExperimentalAttribute() { }
|
|
|
|
/// <summary>
|
|
/// An instance that represents the none-value.
|
|
/// </summary>
|
|
internal static readonly ExperimentalAttribute None = new ExperimentalAttribute();
|
|
|
|
/// <summary>
|
|
/// Validate arguments for the constructor.
|
|
/// </summary>
|
|
internal static void ValidateArguments(string experimentName, ExperimentAction experimentAction)
|
|
{
|
|
if (string.IsNullOrEmpty(experimentName))
|
|
{
|
|
const string paramName = nameof(experimentName);
|
|
throw PSTraceSource.NewArgumentNullException(paramName, Metadata.ArgumentNullOrEmpty, paramName);
|
|
}
|
|
|
|
if (experimentAction == ExperimentAction.None)
|
|
{
|
|
const string paramName = nameof(experimentAction);
|
|
const string invalidMember = nameof(ExperimentAction.None);
|
|
string validMembers = StringUtil.Format("{0}, {1}", ExperimentAction.Hide, ExperimentAction.Show);
|
|
throw PSTraceSource.NewArgumentException(paramName, Metadata.InvalidEnumArgument, invalidMember, paramName, validMembers);
|
|
}
|
|
}
|
|
|
|
internal bool ToHide => EffectiveAction == ExperimentAction.Hide;
|
|
|
|
internal bool ToShow => EffectiveAction == ExperimentAction.Show;
|
|
|
|
/// <summary>
|
|
/// Get effective action to take at run time.
|
|
/// </summary>
|
|
private ExperimentAction EffectiveAction
|
|
{
|
|
get
|
|
{
|
|
if (_effectiveAction == ExperimentAction.None)
|
|
{
|
|
_effectiveAction = ExperimentalFeature.GetActionToTake(ExperimentName, ExperimentAction);
|
|
}
|
|
|
|
return _effectiveAction;
|
|
}
|
|
}
|
|
|
|
private ExperimentAction _effectiveAction = ExperimentAction.None;
|
|
}
|
|
}
|