Initial work of the subsystem plugin model (for minimal powershell) (#13186)

This commit is contained in:
Dongbo Wang 2020-08-20 17:41:44 -07:00 committed by GitHub
parent 10fdfc4ac6
commit fc4c9cbfd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1787 additions and 45 deletions

View file

@ -133,6 +133,10 @@ namespace System.Management.Automation.Runspaces
"System.Management.Automation.PSDriveInfo",
ViewsOf_System_Management_Automation_PSDriveInfo());
yield return new ExtendedTypeDefinition(
"System.Management.Automation.Subsystem.SubsystemInfo",
ViewsOf_System_Management_Automation_Subsystem_SubsystemInfo());
yield return new ExtendedTypeDefinition(
"System.Management.Automation.ShellVariable",
ViewsOf_System_Management_Automation_ShellVariable());
@ -728,6 +732,24 @@ namespace System.Management.Automation.Runspaces
.EndList());
}
private static IEnumerable<FormatViewDefinition> ViewsOf_System_Management_Automation_Subsystem_SubsystemInfo()
{
yield return new FormatViewDefinition(
"System.Management.Automation.Subsystem.SubsystemInfo",
TableControl.Create()
.AddHeader(Alignment.Left, width: 17, label: "Kind")
.AddHeader(Alignment.Left, width: 15, label: "SubsystemType")
.AddHeader(Alignment.Right, width: 12, label: "IsRegistered")
.AddHeader(Alignment.Left, label: "Implementations")
.StartRowDefinition()
.AddPropertyColumn("Kind")
.AddScriptBlockColumn("$_.SubsystemType.Name")
.AddPropertyColumn("IsRegistered")
.AddPropertyColumn("Implementations")
.EndRowDefinition()
.EndTable());
}
private static IEnumerable<FormatViewDefinition> ViewsOf_System_Management_Automation_ShellVariable()
{
yield return new FormatViewDefinition("ShellVariable",

View file

@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
@ -94,6 +95,58 @@ namespace System.Management.Automation
return cursor.Offset < extent.StartOffset || cursor.Offset > extent.EndOffset;
}
internal readonly struct AstAnalysisContext
{
internal AstAnalysisContext(Token tokenAtCursor, Token tokenBeforeCursor, List<Ast> relatedAsts, int replacementIndex)
{
TokenAtCursor = tokenAtCursor;
TokenBeforeCursor = tokenBeforeCursor;
RelatedAsts = relatedAsts;
ReplacementIndex = replacementIndex;
}
internal readonly Token TokenAtCursor;
internal readonly Token TokenBeforeCursor;
internal readonly List<Ast> RelatedAsts;
internal readonly int ReplacementIndex;
}
internal static AstAnalysisContext ExtractAstContext(Ast inputAst, Token[] inputTokens, IScriptPosition cursor)
{
bool adjustLineAndColumn = false;
IScriptPosition positionForAstSearch = cursor;
Token tokenBeforeCursor = null;
Token tokenAtCursor = InterstingTokenAtCursorOrDefault(inputTokens, cursor);
if (tokenAtCursor == null)
{
tokenBeforeCursor = InterstingTokenBeforeCursorOrDefault(inputTokens, cursor);
if (tokenBeforeCursor != null)
{
positionForAstSearch = tokenBeforeCursor.Extent.EndScriptPosition;
adjustLineAndColumn = true;
}
}
else
{
var stringExpandableToken = tokenAtCursor as StringExpandableToken;
if (stringExpandableToken?.NestedTokens != null)
{
tokenAtCursor = InterstingTokenAtCursorOrDefault(stringExpandableToken.NestedTokens, cursor) ?? stringExpandableToken;
}
}
int replacementIndex = adjustLineAndColumn ? cursor.Offset : 0;
List<Ast> relatedAsts = AstSearcher.FindAll(
inputAst,
ast => IsCursorWithinOrJustAfterExtent(positionForAstSearch, ast.Extent),
searchNestedScriptBlocks: true).ToList();
Diagnostics.Assert(tokenAtCursor == null || tokenBeforeCursor == null, "Only one of these tokens can be non-null");
return new AstAnalysisContext(tokenAtCursor, tokenBeforeCursor, relatedAsts, replacementIndex);
}
internal CompletionContext CreateCompletionContext(PowerShell powerShell)
{
var typeInferenceContext = new TypeInferenceContext(powerShell);
@ -107,48 +160,24 @@ namespace System.Management.Automation
private CompletionContext InitializeCompletionContext(TypeInferenceContext typeInferenceContext)
{
Token tokenBeforeCursor = null;
IScriptPosition positionForAstSearch = _cursorPosition;
var adjustLineAndColumn = false;
var tokenAtCursor = InterstingTokenAtCursorOrDefault(_tokens, _cursorPosition);
if (tokenAtCursor == null)
{
tokenBeforeCursor = InterstingTokenBeforeCursorOrDefault(_tokens, _cursorPosition);
if (tokenBeforeCursor != null)
{
positionForAstSearch = tokenBeforeCursor.Extent.EndScriptPosition;
adjustLineAndColumn = true;
}
}
else
{
var stringExpandableToken = tokenAtCursor as StringExpandableToken;
if (stringExpandableToken?.NestedTokens != null)
{
tokenAtCursor = InterstingTokenAtCursorOrDefault(stringExpandableToken.NestedTokens, _cursorPosition) ?? stringExpandableToken;
}
}
var asts = AstSearcher.FindAll(_ast, ast => IsCursorWithinOrJustAfterExtent(positionForAstSearch, ast.Extent), searchNestedScriptBlocks: true).ToList();
Diagnostics.Assert(tokenAtCursor == null || tokenBeforeCursor == null, "Only one of these tokens can be non-null");
var astContext = ExtractAstContext(_ast, _tokens, _cursorPosition);
if (typeInferenceContext.CurrentTypeDefinitionAst == null)
{
typeInferenceContext.CurrentTypeDefinitionAst = Ast.GetAncestorTypeDefinitionAst(asts.Last());
typeInferenceContext.CurrentTypeDefinitionAst = Ast.GetAncestorTypeDefinitionAst(astContext.RelatedAsts.Last());
}
ExecutionContext executionContext = typeInferenceContext.ExecutionContext;
return new CompletionContext
{
TokenAtCursor = tokenAtCursor,
TokenBeforeCursor = tokenBeforeCursor,
CursorPosition = _cursorPosition,
RelatedAsts = asts,
Options = _options,
CursorPosition = _cursorPosition,
TokenAtCursor = astContext.TokenAtCursor,
TokenBeforeCursor = astContext.TokenBeforeCursor,
RelatedAsts = astContext.RelatedAsts,
ReplacementIndex = astContext.ReplacementIndex,
ExecutionContext = executionContext,
ReplacementIndex = adjustLineAndColumn ? _cursorPosition.Offset : 0,
TypeInferenceContext = typeInferenceContext,
Helper = typeInferenceContext.Helper,
CustomArgumentCompleters = executionContext.CustomArgumentCompleters,
@ -156,14 +185,32 @@ namespace System.Management.Automation
};
}
private static Token InterstingTokenAtCursorOrDefault(IEnumerable<Token> tokens, IScriptPosition cursorPosition)
private static Token InterstingTokenAtCursorOrDefault(IReadOnlyList<Token> tokens, IScriptPosition cursorPosition)
{
return tokens.LastOrDefault(token => IsCursorWithinOrJustAfterExtent(cursorPosition, token.Extent) && IsInterestingToken(token));
for (int i = tokens.Count - 1; i >= 0; --i)
{
Token token = tokens[i];
if (IsCursorWithinOrJustAfterExtent(cursorPosition, token.Extent) && IsInterestingToken(token))
{
return token;
}
}
return null;
}
private static Token InterstingTokenBeforeCursorOrDefault(IEnumerable<Token> tokens, IScriptPosition cursorPosition)
private static Token InterstingTokenBeforeCursorOrDefault(IReadOnlyList<Token> tokens, IScriptPosition cursorPosition)
{
return tokens.LastOrDefault(token => IsCursorAfterExtent(cursorPosition, token.Extent) && IsInterestingToken(token));
for (int i = tokens.Count - 1; i >= 0; --i)
{
Token token = tokens[i];
if (IsCursorAfterExtent(cursorPosition, token.Extent) && IsInterestingToken(token))
{
return token;
}
}
return null;
}
private static Ast GetLastAstAtCursor(ScriptBlockAst scriptBlockAst, IScriptPosition cursorPosition)

View file

@ -126,6 +126,9 @@ namespace System.Management.Automation
new ExperimentalFeature(
name: "PSNotApplyErrorActionToStderr",
description: "Don't have $ErrorActionPreference affect stderr output"),
new ExperimentalFeature(
name: "PSSubsystemPluginModel",
description: "A plugin model for registering and un-registering PowerShell subsystems"),
};
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);

View file

@ -5392,6 +5392,12 @@ end {
{ "Out-LineOutput", new SessionStateCmdletEntry("Out-LineOutput", typeof(OutLineOutputCommand), helpFile) },
{ "Format-Default", new SessionStateCmdletEntry("Format-Default", typeof(FormatDefaultCommand), helpFile) },
};
if (ExperimentalFeature.IsEnabled("PSSubsystemPluginModel"))
{
cmdlets.Add("Get-Subsystem", new SessionStateCmdletEntry("Get-Subsystem", typeof(Subsystem.GetSubsystemCommand), helpFile));
}
foreach (var val in cmdlets.Values)
{
val.SetPSSnapIn(psSnapInInfo);
@ -5408,6 +5414,7 @@ end {
{ "Function", new SessionStateProviderEntry("Function", typeof(FunctionProvider), helpFile) },
{ "Variable", new SessionStateProviderEntry("Variable", typeof(VariableProvider), helpFile) },
};
foreach (var val in providers.Values)
{
val.SetPSSnapIn(psSnapInInfo);

View file

@ -0,0 +1,170 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Collections.Generic;
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
using System.Threading;
using System.Threading.Tasks;
namespace System.Management.Automation.Subsystem
{
/// <summary>
/// The class represents the prediction result from a predictor.
/// </summary>
public sealed class PredictionResult
{
/// <summary>
/// Gets the Id of the predictor.
/// </summary>
public Guid Id { get; }
/// <summary>
/// Gets the name of the predictor.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the suggestions.
/// </summary>
public IReadOnlyList<PredictiveSuggestion> Suggestions { get; }
internal PredictionResult(Guid id, string name, List<PredictiveSuggestion> suggestions)
{
Id = id;
Name = name;
Suggestions = suggestions;
}
}
/// <summary>
/// Provides a set of possible predictions for given input.
/// </summary>
public static class CommandPrediction
{
/// <summary>
/// Collect the predictive suggestions from registered predictors using the default timeout.
/// </summary>
/// <param name="ast">The <see cref="Ast"/> object from parsing the current command line input.</param>
/// <param name="astTokens">The <see cref="Token"/> objects from parsing the current command line input.</param>
/// <returns>A list of <see cref="PredictionResult"/> objects.</returns>
public static Task<List<PredictionResult>?> PredictInput(Ast ast, Token[] astTokens)
{
return PredictInput(ast, astTokens, millisecondsTimeout: 20);
}
/// <summary>
/// Collect the predictive suggestions from registered predictors using the specified timeout.
/// </summary>
/// <param name="ast">The <see cref="Ast"/> object from parsing the current command line input.</param>
/// <param name="astTokens">The <see cref="Token"/> objects from parsing the current command line input.</param>
/// <param name="millisecondsTimeout">The milliseconds to timeout.</param>
/// <returns>A list of <see cref="PredictionResult"/> objects.</returns>
public static async Task<List<PredictionResult>?> PredictInput(Ast ast, Token[] astTokens, int millisecondsTimeout)
{
Requires.Condition(millisecondsTimeout > 0, nameof(millisecondsTimeout));
var predictors = SubsystemManager.GetSubsystems<ICommandPredictor>();
if (predictors.Count == 0)
{
return null;
}
var context = new PredictionContext(ast, astTokens);
var tasks = new Task<PredictionResult?>[predictors.Count];
using var cancellationSource = new CancellationTokenSource();
for (int i = 0; i < predictors.Count; i++)
{
ICommandPredictor predictor = predictors[i];
tasks[i] = Task.Factory.StartNew(
state => {
var predictor = (ICommandPredictor)state!;
List<PredictiveSuggestion>? texts = predictor.GetSuggestion(context, cancellationSource.Token);
return texts?.Count > 0 ? new PredictionResult(predictor.Id, predictor.Name, texts) : null;
},
predictor,
cancellationSource.Token,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default);
}
await Task.WhenAny(
Task.WhenAll(tasks),
Task.Delay(millisecondsTimeout, cancellationSource.Token)).ConfigureAwait(false);
cancellationSource.Cancel();
var results = new List<PredictionResult>(predictors.Count);
foreach (Task<PredictionResult?> task in tasks)
{
if (task.IsCompletedSuccessfully)
{
PredictionResult? result = task.Result;
if (result != null)
{
results.Add(result);
}
}
}
return results;
}
/// <summary>
/// Allow registered predictors to do early processing when a command line is accepted.
/// </summary>
/// <param name="history">History command lines provided as references for prediction.</param>
public static void OnCommandLineAccepted(IReadOnlyList<string> history)
{
Requires.NotNull(history, nameof(history));
var predictors = SubsystemManager.GetSubsystems<ICommandPredictor>();
if (predictors.Count == 0)
{
return;
}
foreach (ICommandPredictor predictor in predictors)
{
if (predictor.SupportEarlyProcessing)
{
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
state => state.StartEarlyProcessing(history),
predictor,
preferLocal: false);
}
}
}
/// <summary>
/// Send feedback to predictors about their last suggestions.
/// </summary>
/// <param name="predictorId">The identifier of the predictor whose prediction result was accepted.</param>
/// <param name="suggestionText">The accepted suggestion text.</param>
public static void OnSuggestionAccepted(Guid predictorId, string suggestionText)
{
Requires.NotNullOrEmpty(suggestionText, nameof(suggestionText));
var predictors = SubsystemManager.GetSubsystems<ICommandPredictor>();
if (predictors.Count == 0)
{
return;
}
foreach (ICommandPredictor predictor in predictors)
{
if (predictor.AcceptFeedback && predictor.Id == predictorId)
{
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
state => state.OnSuggestionAccepted(suggestionText),
predictor,
preferLocal: false);
}
}
}
}
}

View file

@ -0,0 +1,163 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Collections.Generic;
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
using System.Threading;
namespace System.Management.Automation.Subsystem
{
/// <summary>
/// Interface for implementing a predictor plugin.
/// </summary>
public interface ICommandPredictor : ISubsystem
{
/// <summary>
/// Default implementation. No function is required for a predictor.
/// </summary>
Dictionary<string, string>? ISubsystem.FunctionsToDefine => null;
/// <summary>
/// Default implementation for `ISubsystem.Kind`.
/// </summary>
SubsystemKind ISubsystem.Kind => SubsystemKind.CommandPredictor;
/// <summary>
/// Gets a value indicating whether the predictor supports early processing.
/// </summary>
bool SupportEarlyProcessing { get; }
/// <summary>
/// Gets a value indicating whether the predictor accepts feedback about the previous suggestion.
/// </summary>
bool AcceptFeedback { get; }
/// <summary>
/// A command line was accepted to execute.
/// The predictor can start processing early as needed with the latest history.
/// </summary>
/// <param name="history">History command lines provided as references for prediction.</param>
void StartEarlyProcessing(IReadOnlyList<string> history);
/// <summary>
/// The suggestion given by the predictor was accepted.
/// </summary>
/// <param name="acceptedSuggestion">The accepted suggestion text.</param>
void OnSuggestionAccepted(string acceptedSuggestion);
/// <summary>
/// Get the predictive suggestions.
/// </summary>
/// <param name="context">The <see cref="PredictionContext"/> object to be used for prediction.</param>
/// <param name="cancellationToken">The cancellation token to cancel the prediction.</param>
/// <returns>A list of predictive suggestions.</returns>
List<PredictiveSuggestion>? GetSuggestion(PredictionContext context, CancellationToken cancellationToken);
}
/// <summary>
/// Context information about the user input.
/// </summary>
public class PredictionContext
{
/// <summary>
/// Gets the abstract syntax tree (AST) generated from parsing the user input.
/// </summary>
public Ast InputAst { get; }
/// <summary>
/// Gets the tokens generated from parsing the user input.
/// </summary>
public IReadOnlyList<Token> InputTokens { get; }
/// <summary>
/// Gets the cursor position, which is assumed always at the end of the input line.
/// </summary>
public IScriptPosition CursorPosition { get; }
/// <summary>
/// Gets the token at the cursor.
/// </summary>
public Token? TokenAtCursor { get; }
/// <summary>
/// Gets all ASTs that are related to the cursor position,
/// which is assumed always at the end of the input line.
/// </summary>
public IReadOnlyList<Ast> RelatedAsts { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PredictionContext"/> class from the AST and tokens that represent the user input.
/// </summary>
/// <param name="inputAst">The <see cref="Ast"/> object from parsing the current command line input.</param>
/// <param name="inputTokens">The <see cref="Token"/> objects from parsing the current command line input.</param>
public PredictionContext(Ast inputAst, Token[] inputTokens)
{
Requires.NotNull(inputAst, nameof(inputAst));
Requires.NotNull(inputTokens, nameof(inputTokens));
var cursor = inputAst.Extent.EndScriptPosition;
var astContext = CompletionAnalysis.ExtractAstContext(inputAst, inputTokens, cursor);
InputAst = inputAst;
InputTokens = inputTokens;
CursorPosition = cursor;
TokenAtCursor = astContext.TokenAtCursor;
RelatedAsts = astContext.RelatedAsts;
}
/// <summary>
/// Creates a context instance from the user input line.
/// </summary>
/// <param name="input">The user input.</param>
/// <returns>A <see cref="PredictionContext"/> object.</returns>
public static PredictionContext Create(string input)
{
Requires.NotNullOrEmpty(input, nameof(input));
Ast ast = Parser.ParseInput(input, out Token[] tokens, out _);
return new PredictionContext(ast, tokens);
}
}
/// <summary>
/// The class represents a predictive suggestion generated by a predictor.
/// </summary>
public sealed class PredictiveSuggestion
{
/// <summary>
/// Gets the suggestion.
/// </summary>
public string SuggestionText { get; }
/// <summary>
/// Gets the tooltip of the suggestion.
/// </summary>
public string? ToolTip { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PredictiveSuggestion"/> class.
/// </summary>
/// <param name="suggestion">The predictive suggestion text.</param>
public PredictiveSuggestion(string suggestion)
: this(suggestion, toolTip: null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PredictiveSuggestion"/> class.
/// </summary>
/// <param name="suggestion">The predictive suggestion text.</param>
/// <param name="toolTip">The tooltip of the suggestion.</param>
public PredictiveSuggestion(string suggestion, string? toolTip)
{
Requires.NotNullOrEmpty(suggestion, nameof(suggestion));
SuggestionText = suggestion;
ToolTip = toolTip;
}
}
}

View file

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
namespace System.Management.Automation.Subsystem
{
/// <summary>
/// Implementation of 'Get-Subsystem' cmdlet.
/// </summary>
[Experimental("PSSubsystemPluginModel", ExperimentAction.Show)]
[Cmdlet(VerbsCommon.Get, "Subsystem", DefaultParameterSetName = AllSet)]
[OutputType(typeof(SubsystemInfo))]
public sealed class GetSubsystemCommand : PSCmdlet
{
private const string AllSet = "GetAllSet";
private const string TypeSet = "GetByTypeSet";
private const string KindSet = "GetByKindSet";
/// <summary>
/// Gets or sets a concrete subsystem kind.
/// </summary>
[Parameter(Mandatory = true, ParameterSetName = KindSet, ValueFromPipeline = true)]
public SubsystemKind Kind { get; set; }
/// <summary>
/// Gets or sets the interface or abstract class type of a concrete subsystem.
/// </summary>
[Parameter(Mandatory = true, ParameterSetName = TypeSet, ValueFromPipeline = true)]
public Type? SubsystemType { get; set; }
/// <summary>
/// ProcessRecord implementation.
/// </summary>
protected override void ProcessRecord()
{
switch (ParameterSetName)
{
case AllSet:
WriteObject(SubsystemManager.GetAllSubsystemInfo());
break;
case KindSet:
WriteObject(SubsystemManager.GetSubsystemInfo(Kind));
break;
case TypeSet:
WriteObject(SubsystemManager.GetSubsystemInfo(SubsystemType!));
break;
default:
throw new InvalidOperationException("New parameter set is added but the switch statement is not updated.");
}
}
}
}

View file

@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Collections.Generic;
namespace System.Management.Automation.Subsystem
{
/// <summary>
/// Define the kinds of subsystems.
/// </summary>
public enum SubsystemKind
{
/// <summary>
/// Component that provides predictive suggestions to commandline input.
/// </summary>
CommandPredictor = 1,
}
/// <summary>
/// Define the base interface to implement a subsystem.
/// The API contracts for specific subsystems are defined within the specific interfaces/abstract classes that implements this interface.
/// </summary>
/// <remarks>
/// There are two purposes to have the internal member `Kind` declared in 'ISubsystem':
/// 1. Make the mapping from an `ISubsystem` implementation to the `SubsystemKind` easy;
/// 2. Make sure a user cannot directly implement 'ISubsystem', but have to derive from one of the concrete subsystem interface or abstract class.
/// <para/>
/// The internal member needs to have a default implementation defined by the specific subsystem interfaces or abstract class,
/// because it should be the same for a specific kind of subsystem.
/// </remarks>
public interface ISubsystem
{
/// <summary>
/// Gets the unique identifier for a subsystem implementation.
/// </summary>
Guid Id { get; }
/// <summary>
/// Gets the name of a subsystem implementation.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the description of a subsystem implementation.
/// </summary>
string Description { get; }
/// <summary>
/// Gets a dictionary that contains the functions to be defined at the global scope of a PowerShell session.
/// Key: function name; Value: function script.
/// </summary>
Dictionary<string, string>? FunctionsToDefine { get; }
/// <summary>
/// Gets the subsystem kind.
/// </summary>
internal SubsystemKind Kind { get; }
}
}

View file

@ -0,0 +1,346 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Management.Automation.Internal;
namespace System.Management.Automation.Subsystem
{
/// <summary>
/// Class used to represent the metadata and state of a subsystem.
/// </summary>
public abstract class SubsystemInfo
{
#region "Metadata of a Subsystem (public)"
/// <summary>
/// The kind of a concrete subsystem.
/// </summary>
public SubsystemKind Kind { get; private set; }
/// <summary>
/// The type of a concrete subsystem.
/// </summary>
public Type SubsystemType { get; private set; }
/// <summary>
/// Indicate whether the subsystem allows to unregister an implementation.
/// </summary>
public bool AllowUnregistration { get; private set; }
/// <summary>
/// Indicate whether the subsystem allows to have multiple implementations registered.
/// </summary>
public bool AllowMultipleRegistration { get; private set; }
/// <summary>
/// Gets the names of the required cmdlets that have to be implemented by the subsystem implementation.
/// </summary>
public ReadOnlyCollection<string> RequiredCmdlets { get; private set; }
/// <summary>
/// Gets the names of the required functions that have to be implemented by the subsystem implementation.
/// </summary>
public ReadOnlyCollection<string> RequiredFunctions { get; private set; }
// /// <summary>
// /// A subsystem may depend on or more other subsystems.
// /// Maybe add a 'DependsOn' member?
// /// This can be validated when registering a subsystem implementation,
// /// to make sure its prerequisites have already been registered.
// /// </summary>
// public ReadOnlyCollection<SubsystemKind> DependsOn { get; private set; }
#endregion
#region "State of a Subsystem (public)"
/// <summary>
/// Indicate whether there is any implementation registered to the subsystem.
/// </summary>
public bool IsRegistered => _cachedImplInfos.Count > 0;
/// <summary>
/// Get the information about the registered implementations.
/// </summary>
public ReadOnlyCollection<ImplementationInfo> Implementations => _cachedImplInfos;
#endregion
#region "private/internal instance members"
private protected readonly object _syncObj;
private protected ReadOnlyCollection<ImplementationInfo> _cachedImplInfos;
private protected SubsystemInfo(SubsystemKind kind, Type subsystemType)
{
_syncObj = new object();
_cachedImplInfos = Utils.EmptyReadOnlyCollection<ImplementationInfo>();
Kind = kind;
SubsystemType = subsystemType;
AllowUnregistration = false;
AllowMultipleRegistration = false;
RequiredCmdlets = Utils.EmptyReadOnlyCollection<string>();
RequiredFunctions = Utils.EmptyReadOnlyCollection<string>();
}
private protected abstract void AddImplementation(ISubsystem rawImpl);
private protected abstract ISubsystem RemoveImplementation(Guid id);
internal void RegisterImplementation(ISubsystem impl)
{
AddImplementation(impl);
}
internal ISubsystem UnregisterImplementation(Guid id)
{
return RemoveImplementation(id);
}
#endregion
#region "Static factory overloads"
internal static SubsystemInfo Create<TConcreteSubsystem>(SubsystemKind kind)
where TConcreteSubsystem : class, ISubsystem
{
return new SubsystemInfoImpl<TConcreteSubsystem>(kind);
}
internal static SubsystemInfo Create<TConcreteSubsystem>(
SubsystemKind kind,
bool allowUnregistration,
bool allowMultipleRegistration) where TConcreteSubsystem : class, ISubsystem
{
return new SubsystemInfoImpl<TConcreteSubsystem>(kind)
{
AllowUnregistration = allowUnregistration,
AllowMultipleRegistration = allowMultipleRegistration,
};
}
internal static SubsystemInfo Create<TConcreteSubsystem>(
SubsystemKind kind,
bool allowUnregistration,
bool allowMultipleRegistration,
ReadOnlyCollection<string> requiredCmdlets,
ReadOnlyCollection<string> requiredFunctions) where TConcreteSubsystem : class, ISubsystem
{
if (allowMultipleRegistration &&
(requiredCmdlets.Count > 0 || requiredFunctions.Count > 0))
{
throw new ArgumentException(
StringUtil.Format(
SubsystemStrings.InvalidSubsystemInfo,
kind.ToString()));
}
return new SubsystemInfoImpl<TConcreteSubsystem>(kind)
{
AllowUnregistration = allowUnregistration,
AllowMultipleRegistration = allowMultipleRegistration,
RequiredCmdlets = requiredCmdlets,
RequiredFunctions = requiredFunctions,
};
}
#endregion
#region "ImplementationInfo"
/// <summary>
/// Information about an implementation of a subsystem.
/// </summary>
public class ImplementationInfo
{
internal ImplementationInfo(ISubsystem implementation)
{
Id = implementation.Id;
Kind = implementation.Kind;
Name = implementation.Name;
Description = implementation.Description;
ImplementationType = implementation.GetType();
}
/// <summary>
/// Gets the unique identifier for a subsystem implementation.
/// </summary>
public Guid Id { get; }
/// <summary>
/// Gets the kind of subsystem.
/// </summary>
public SubsystemKind Kind { get; }
/// <summary>
/// Gets the name of a subsystem implementation.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the description of a subsystem implementation.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets the implementation type.
/// </summary>
public Type ImplementationType { get; }
}
#endregion
}
internal sealed class SubsystemInfoImpl<TConcreteSubsystem> : SubsystemInfo
where TConcreteSubsystem : class, ISubsystem
{
private ReadOnlyCollection<TConcreteSubsystem> _registeredImpls;
internal SubsystemInfoImpl(SubsystemKind kind)
: base(kind, typeof(TConcreteSubsystem))
{
_registeredImpls = Utils.EmptyReadOnlyCollection<TConcreteSubsystem>();
}
/// <summary>
/// The 'add' and 'remove' operations are implemented in a way to optimize the 'reading' operation,
/// so that reading is lock-free and allocation-free, at the cost of O(n) copy in 'add' and 'remove'
/// ('n' is the number of registered implementations).
/// </summary>
/// <remarks>
/// In the subsystem scenario, registration operations will be minimum, and in most cases, the registered
/// implementation will never be unregistered, so optimization for reading is more important.
/// </remarks>
private protected override void AddImplementation(ISubsystem rawImpl)
{
lock (_syncObj)
{
var impl = (TConcreteSubsystem)rawImpl;
if (_registeredImpls.Count == 0)
{
_registeredImpls = new ReadOnlyCollection<TConcreteSubsystem>(new[] { impl });
_cachedImplInfos = new ReadOnlyCollection<ImplementationInfo>(new[] { new ImplementationInfo(impl) });
return;
}
if (!AllowMultipleRegistration)
{
throw new InvalidOperationException(
StringUtil.Format(
SubsystemStrings.MultipleRegistrationNotAllowed,
Kind.ToString()));
}
foreach (TConcreteSubsystem item in _registeredImpls)
{
if (item.Id == impl.Id)
{
throw new InvalidOperationException(
StringUtil.Format(
SubsystemStrings.ImplementationAlreadyRegistered,
impl.Id,
Kind.ToString()));
}
}
var list = new List<TConcreteSubsystem>(_registeredImpls.Count + 1);
list.AddRange(_registeredImpls);
list.Add(impl);
_registeredImpls = new ReadOnlyCollection<TConcreteSubsystem>(list);
_cachedImplInfos = new ReadOnlyCollection<ImplementationInfo>(list.ConvertAll(s => new ImplementationInfo(s)));
}
}
/// <summary>
/// The 'add' and 'remove' operations are implemented in a way to optimize the 'reading' operation,
/// so that reading is lock-free and allocation-free, at the cost of O(n) copy in 'add' and 'remove'
/// ('n' is the number of registered implementations).
/// </summary>
/// <remarks>
/// In the subsystem scenario, registration operations will be minimum, and in most cases, the registered
/// implementation will never be unregistered, so optimization for reading is more important.
/// </remarks>
private protected override ISubsystem RemoveImplementation(Guid id)
{
if (!AllowUnregistration)
{
throw new InvalidOperationException(
StringUtil.Format(
SubsystemStrings.UnregistrationNotAllowed,
Kind.ToString()));
}
lock (_syncObj)
{
if (_registeredImpls.Count == 0)
{
throw new InvalidOperationException(
StringUtil.Format(
SubsystemStrings.NoImplementationRegistered,
Kind.ToString()));
}
int index = -1;
for (int i = 0; i < _registeredImpls.Count; i++)
{
if (_registeredImpls[i].Id == id)
{
index = i;
break;
}
}
if (index == -1)
{
throw new InvalidOperationException(
StringUtil.Format(
SubsystemStrings.ImplementationNotFound,
id.ToString()));
}
ISubsystem target = _registeredImpls[index];
if (_registeredImpls.Count == 1)
{
_registeredImpls = Utils.EmptyReadOnlyCollection<TConcreteSubsystem>();
_cachedImplInfos = Utils.EmptyReadOnlyCollection<ImplementationInfo>();
}
else
{
var list = new List<TConcreteSubsystem>(_registeredImpls.Count - 1);
for (int i = 0; i < _registeredImpls.Count; i++)
{
if (index == i)
{
continue;
}
list.Add(_registeredImpls[i]);
}
_registeredImpls = new ReadOnlyCollection<TConcreteSubsystem>(list);
_cachedImplInfos = new ReadOnlyCollection<ImplementationInfo>(list.ConvertAll(s => new ImplementationInfo(s)));
}
return target;
}
}
internal TConcreteSubsystem? GetImplementation()
{
var localRef = _registeredImpls;
return localRef.Count > 0 ? localRef[localRef.Count - 1] : null;
}
internal ReadOnlyCollection<TConcreteSubsystem> GetAllImplementations()
{
return _registeredImpls;
}
}
}

View file

@ -0,0 +1,282 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation.Internal;
namespace System.Management.Automation.Subsystem
{
/// <summary>
/// Class used to manage subsystems.
/// </summary>
public static class SubsystemManager
{
private static readonly ReadOnlyCollection<SubsystemInfo> s_subsystems;
private static readonly ReadOnlyDictionary<Type, SubsystemInfo> s_subSystemTypeMap;
private static readonly ReadOnlyDictionary<SubsystemKind, SubsystemInfo> s_subSystemKindMap;
static SubsystemManager()
{
var subsystems = new SubsystemInfo[]
{
SubsystemInfo.Create<ICommandPredictor>(
SubsystemKind.CommandPredictor,
allowUnregistration: true,
allowMultipleRegistration: true),
};
var subSystemTypeMap = new Dictionary<Type, SubsystemInfo>(subsystems.Length);
var subSystemKindMap = new Dictionary<SubsystemKind, SubsystemInfo>(subsystems.Length);
foreach (var subsystem in subsystems)
{
subSystemTypeMap.Add(subsystem.SubsystemType, subsystem);
subSystemKindMap.Add(subsystem.Kind, subsystem);
}
s_subsystems = new ReadOnlyCollection<SubsystemInfo>(subsystems);
s_subSystemTypeMap = new ReadOnlyDictionary<Type, SubsystemInfo>(subSystemTypeMap);
s_subSystemKindMap = new ReadOnlyDictionary<SubsystemKind, SubsystemInfo>(subSystemKindMap);
}
#region internal - Retrieve subsystem proxy object
/// <summary>
/// Get the proxy object registered for a specific subsystem.
/// Return null when the given subsystem is not registered.
/// </summary>
/// <remarks>
/// Design point:
/// The implemnentation proxy object is not supposed to expose to users.
/// Users shouldn't depend on a implementation proxy object directly, but instead should depend on PowerShell APIs.
/// <para/>
/// Example: if a user want to use prediction functionality, he/she should use the PowerShell prediction API instead of
/// directly interacting with the implementation proxy object of `IPrediction`.
/// </remarks>
/// <typeparam name="TConcreteSubsystem">The concrete subsystem base type.</typeparam>
/// <returns>The most recently registered implmentation object of the concrete subsystem.</returns>
internal static TConcreteSubsystem? GetSubsystem<TConcreteSubsystem>()
where TConcreteSubsystem : class, ISubsystem
{
if (s_subSystemTypeMap.TryGetValue(typeof(TConcreteSubsystem), out SubsystemInfo? subsystemInfo))
{
var subsystemInfoImpl = (SubsystemInfoImpl<TConcreteSubsystem>)subsystemInfo;
return subsystemInfoImpl.GetImplementation();
}
throw new ArgumentException(
StringUtil.Format(
SubsystemStrings.SubsystemTypeUnknown,
typeof(TConcreteSubsystem).FullName));
}
/// <summary>
/// Get all the proxy objects registered for a specific subsystem.
/// Return an empty collection when the given subsystem is not registered.
/// </summary>
/// <typeparam name="TConcreteSubsystem">The concrete subsystem base type.</typeparam>
/// <returns>A readonly collection of all implmentation objects registered for the concrete subsystem.</returns>
internal static ReadOnlyCollection<TConcreteSubsystem> GetSubsystems<TConcreteSubsystem>()
where TConcreteSubsystem : class, ISubsystem
{
if (s_subSystemTypeMap.TryGetValue(typeof(TConcreteSubsystem), out SubsystemInfo? subsystemInfo))
{
var subsystemInfoImpl = (SubsystemInfoImpl<TConcreteSubsystem>)subsystemInfo;
return subsystemInfoImpl.GetAllImplementations();
}
throw new ArgumentException(
StringUtil.Format(
SubsystemStrings.SubsystemTypeUnknown,
typeof(TConcreteSubsystem).FullName));
}
#endregion
#region public - Subsystem metadata
/// <summary>
/// Get the information about all subsystems.
/// </summary>
/// <returns>A readonly collection of all <see cref="SubsystemInfo"/> objects.</returns>
public static ReadOnlyCollection<SubsystemInfo> GetAllSubsystemInfo()
{
return s_subsystems;
}
/// <summary>
/// Get the information about a subsystem by the subsystem type.
/// </summary>
/// <param name="subsystemType">The base type of a specific concrete subsystem.</param>
/// <returns>The <see cref="SubsystemInfo"/> object that represents the concrete subsystem.</returns>
public static SubsystemInfo GetSubsystemInfo(Type subsystemType)
{
Requires.NotNull(subsystemType, nameof(subsystemType));
if (s_subSystemTypeMap.TryGetValue(subsystemType, out SubsystemInfo? subsystemInfo))
{
return subsystemInfo;
}
throw new ArgumentException(
StringUtil.Format(
SubsystemStrings.SubsystemTypeUnknown,
subsystemType.FullName));
}
/// <summary>
/// Get the information about a subsystem by the subsystem kind.
/// </summary>
/// <param name="kind">A specific <see cref="SubsystemKind"/>.</param>
/// <returns>The <see cref="SubsystemInfo"/> object that represents the concrete subsystem.</returns>
public static SubsystemInfo GetSubsystemInfo(SubsystemKind kind)
{
if (s_subSystemKindMap.TryGetValue(kind, out SubsystemInfo? subsystemInfo))
{
return subsystemInfo;
}
throw new ArgumentException(
StringUtil.Format(
SubsystemStrings.SubsystemKindUnknown,
kind.ToString()));
}
#endregion
#region public - Subsystem registration
/// <summary>
/// Subsystem registration.
/// </summary>
/// <typeparam name="TConcreteSubsystem">The concrete subsystem base type.</typeparam>
/// <typeparam name="TImplementation">The implementation type of that concrete subsystem.</typeparam>
/// <param name="proxy">An instance of the implementation.</param>
public static void RegisterSubsystem<TConcreteSubsystem, TImplementation>(TImplementation proxy)
where TConcreteSubsystem : class, ISubsystem
where TImplementation : class, TConcreteSubsystem
{
Requires.NotNull(proxy, nameof(proxy));
RegisterSubsystem(GetSubsystemInfo(typeof(TConcreteSubsystem)), proxy);
}
/// <summary>
/// Register an implementation for a subsystem.
/// </summary>
/// <param name="kind">The target <see cref="SubsystemKind"/> of the registration.</param>
/// <param name="proxy">An instance of the implementation.</param>
public static void RegisterSubsystem(SubsystemKind kind, ISubsystem proxy)
{
Requires.NotNull(proxy, nameof(proxy));
if (kind != proxy.Kind)
{
throw new ArgumentException(
StringUtil.Format(
SubsystemStrings.ImplementationMismatch,
proxy.Kind.ToString(),
kind.ToString()),
nameof(proxy));
}
RegisterSubsystem(GetSubsystemInfo(kind), proxy);
}
private static void RegisterSubsystem(SubsystemInfo subsystemInfo, ISubsystem proxy)
{
if (proxy.Id == Guid.Empty)
{
throw new ArgumentException(
StringUtil.Format(
SubsystemStrings.EmptyImplementationId,
subsystemInfo.Kind.ToString()),
nameof(proxy));
}
if (string.IsNullOrEmpty(proxy.Name))
{
throw new ArgumentException(
StringUtil.Format(
SubsystemStrings.NullOrEmptyImplementationName,
subsystemInfo.Kind.ToString()),
nameof(proxy));
}
if (string.IsNullOrEmpty(proxy.Description))
{
throw new ArgumentException(
StringUtil.Format(
SubsystemStrings.NullOrEmptyImplementationDescription,
subsystemInfo.Kind.ToString()),
nameof(proxy));
}
if (subsystemInfo.RequiredCmdlets.Any() || subsystemInfo.RequiredFunctions.Any())
{
// Process 'proxy.CmdletImplementationAssembly' and 'proxy.FunctionsToDefine'
// Functions are added to global scope.
// Cmdlets are loaded in a way like a snapin, making the 'Source' of the cmdlets to be 'Microsoft.PowerShell.Core'.
//
// For example, let's say the Job adapter is made a subsystem, then all `*-Job` cmdlets will be moved out of S.M.A
// into a subsystem implementation DLL. After registration, all `*-Job` cmdlets should be back in the
// 'Microsoft.PowerShell.Core' namespace to keep backward compatibility.
//
// Both cmdlets and functions are added to the default InitialSessionState used for creating a new Runspace,
// so the subsystem works for all subsequent new runspaces after it's registered.
// Take the Job adapter subsystem as an instance again, so when creating another Runspace after the registration,
// all '*-Job' cmdlets should be available in the 'Microsoft.PowerShell.Core' namespace by default.
}
subsystemInfo.RegisterImplementation(proxy);
}
#endregion
#region public - Subsystem unregistration
/// <summary>
/// Subsystem unregistration.
/// Throw 'InvalidOperationException' when called for subsystems that cannot be unregistered.
/// </summary>
/// <typeparam name="TConcreteSubsystem">The base type of the target concrete subsystem of the un-registration.</typeparam>
/// <param name="id">The Id of the implementation to be unregistered.</param>
public static void UnregisterSubsystem<TConcreteSubsystem>(Guid id)
where TConcreteSubsystem : class, ISubsystem
{
UnregisterSubsystem(GetSubsystemInfo(typeof(TConcreteSubsystem)), id);
}
/// <summary>
/// Subsystem unregistration.
/// Throw 'InvalidOperationException' when called for subsystems that cannot be unregistered.
/// </summary>
/// <param name="kind">The target <see cref="SubsystemKind"/> of the un-registration.</param>
/// <param name="id">The Id of the implementation to be unregistered.</param>
public static void UnregisterSubsystem(SubsystemKind kind, Guid id)
{
UnregisterSubsystem(GetSubsystemInfo(kind), id);
}
private static void UnregisterSubsystem(SubsystemInfo subsystemInfo, Guid id)
{
if (subsystemInfo.RequiredCmdlets.Any() || subsystemInfo.RequiredFunctions.Any())
{
throw new NotSupportedException("NotSupported yet: unregister subsystem that introduced new cmdlets/functions.");
}
ISubsystem impl = subsystemInfo.UnregisterImplementation(id);
if (impl is IDisposable disposable)
{
disposable.Dispose();
}
}
#endregion
}
}

View file

@ -2278,4 +2278,34 @@ namespace System.Management.Automation.Internal
/// </summary>
internal static readonly ReadOnlyBag<T> Empty = new ReadOnlyBag<T>(new HashSet<T>(capacity: 0));
}
/// <summary>
/// Helper class for simple argument validations.
/// </summary>
internal static class Requires
{
internal static void NotNull(object value, string paramName)
{
if (value == null)
{
throw new ArgumentNullException(paramName);
}
}
internal static void NotNullOrEmpty(string value, string paramName)
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentNullException(paramName);
}
}
internal static void Condition([DoesNotReturnIf(false)] bool precondition, string paramName)
{
if (!precondition)
{
throw new ArgumentException(paramName);
}
}
}
}

View file

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="https://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="MultipleRegistrationNotAllowed" xml:space="preserve">
<value>The subsystem '{0}' does not allow more than one implementation to be registered.</value>
</data>
<data name="ImplementationAlreadyRegistered" xml:space="preserve">
<value>The implementation with Id '{0}' was already registered for the subsystem '{1}'.</value>
</data>
<data name="UnregistrationNotAllowed" xml:space="preserve">
<value>The subsystem '{0}' does not allow the unregistration of an implementation.</value>
</data>
<data name="NoImplementationRegistered" xml:space="preserve">
<value>No implementation was registered for the subsystem '{0}'.</value>
</data>
<data name="ImplementationNotFound" xml:space="preserve">
<value>A registered implementation with the Id '{0}' was not found.</value>
</data>
<data name="SubsystemTypeUnknown" xml:space="preserve">
<value>The specified subsystem type '{0}' is unknown.</value>
</data>
<data name="SubsystemKindUnknown" xml:space="preserve">
<value>The specified subsystem kind '{0}' is unknown.</value>
</data>
<data name="ImplementationMismatch" xml:space="preserve">
<value>The specified implementation instance implements the subsystem '{0}', which does not match the target subsystem '{1}'.</value>
</data>
<data name="InvalidSubsystemInfo" xml:space="preserve">
<value>The declared metadata for subsystem kind '{0}' is invalid. A subsystem that requires cmdlets or functions to be defined cannot allow multiple registrations because that would result in one implementation overwriting the commands defined by another implementation.</value>
</data>
<data name="EmptyImplementationId" xml:space="preserve">
<value>The 'Id' property of an implementation for the subsystem '{0}' cannot be an empty GUID.</value>
</data>
<data name="NullOrEmptyImplementationName" xml:space="preserve">
<value>The 'Name' property of an implementation for the subsystem '{0}' cannot be null or an empty string.</value>
</data>
<data name="NullOrEmptyImplementationDescription" xml:space="preserve">
<value>The 'Description' property of an implementation for the subsystem '{0}' cannot be null or an empty string.</value>
</data>
</root>

View file

@ -15,7 +15,8 @@ $script:cmdletsToSkip = @(
"Enable-PSRemoting",
"Get-ExperimentalFeature",
"Enable-ExperimentalFeature",
"Disable-ExperimentalFeature"
"Disable-ExperimentalFeature",
"Get-Subsystem"
)
function UpdateHelpFromLocalContentPath {

View file

@ -0,0 +1,193 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Management.Automation.Language;
using System.Management.Automation.Subsystem;
using System.Threading;
using Xunit;
namespace PSTests.Sequential
{
public class MyPredictor : ICommandPredictor
{
private readonly Guid _id;
private readonly string _name, _description;
private readonly bool _delay;
public List<string> History { get; }
public List<string> AcceptedSuggestions { get; }
public static readonly MyPredictor SlowPredictor;
public static readonly MyPredictor FastPredictor;
static MyPredictor()
{
SlowPredictor = new MyPredictor(
Guid.NewGuid(),
"Test Predictor #1",
"Description for #1 predictor.",
delay: true);
FastPredictor = new MyPredictor(
Guid.NewGuid(),
"Test Predictor #2",
"Description for #2 predictor.",
delay: false);
}
private MyPredictor(Guid id, string name, string description, bool delay)
{
_id = id;
_name = name;
_description = description;
_delay = delay;
History = new List<string>();
AcceptedSuggestions = new List<string>();
}
public Guid Id => _id;
public string Name => _name;
public string Description => _description;
bool ICommandPredictor.SupportEarlyProcessing => true;
bool ICommandPredictor.AcceptFeedback => true;
public void StartEarlyProcessing(IReadOnlyList<string> history)
{
History.AddRange(history);
}
public void OnSuggestionAccepted(string acceptedSuggestion)
{
AcceptedSuggestions.Add(acceptedSuggestion);
}
public List<PredictiveSuggestion> GetSuggestion(PredictionContext context, CancellationToken cancellationToken)
{
if (_delay)
{
// The delay is exaggerated to make the test reliable.
// xUnit must spin up a lot tasks, which makes the test unreliable when the time difference between 'delay' and 'timeout' is small.
Thread.Sleep(2000);
}
// You can get the user input from the AST.
var userInput = context.InputAst.Extent.Text;
return new List<PredictiveSuggestion> {
new PredictiveSuggestion($"{userInput} TEST-1 from {Name}"),
new PredictiveSuggestion($"{userInput} TeSt-2 from {Name}"),
};
}
}
public static class CommandPredictionTests
{
[Fact]
public static void PredictInput()
{
MyPredictor slow = MyPredictor.SlowPredictor;
MyPredictor fast = MyPredictor.FastPredictor;
Ast ast = Parser.ParseInput("Hello world", out Token[] tokens, out _);
// Returns null when no predictor implementation registered
List<PredictionResult> results = CommandPrediction.PredictInput(ast, tokens).Result;
Assert.Null(results);
try
{
// Register 2 predictor implementations
SubsystemManager.RegisterSubsystem<ICommandPredictor, MyPredictor>(slow);
SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, fast);
// Expect the results from 'fast' predictor only b/c the 'slow' one
// cannot finish before the specified timeout.
// The specified timeout is exaggerated to make the test reliable.
// xUnit must spin up a lot tasks, which makes the test unreliable when the time difference between 'delay' and 'timeout' is small.
results = CommandPrediction.PredictInput(ast, tokens, millisecondsTimeout: 1000).Result;
Assert.Single(results);
PredictionResult res = results[0];
Assert.Equal(fast.Id, res.Id);
Assert.Equal(2, res.Suggestions.Count);
Assert.Equal($"Hello world TEST-1 from {fast.Name}", res.Suggestions[0].SuggestionText);
Assert.Equal($"Hello world TeSt-2 from {fast.Name}", res.Suggestions[1].SuggestionText);
// Expect the results from both 'slow' and 'fast' predictors
// Same here -- the specified timeout is exaggerated to make the test reliable.
// xUnit must spin up a lot tasks, which makes the test unreliable when the time difference between 'delay' and 'timeout' is small.
results = CommandPrediction.PredictInput(ast, tokens, millisecondsTimeout: 4000).Result;
Assert.Equal(2, results.Count);
PredictionResult res1 = results[0];
Assert.Equal(slow.Id, res1.Id);
Assert.Equal(2, res1.Suggestions.Count);
Assert.Equal($"Hello world TEST-1 from {slow.Name}", res1.Suggestions[0].SuggestionText);
Assert.Equal($"Hello world TeSt-2 from {slow.Name}", res1.Suggestions[1].SuggestionText);
PredictionResult res2 = results[1];
Assert.Equal(fast.Id, res2.Id);
Assert.Equal(2, res2.Suggestions.Count);
Assert.Equal($"Hello world TEST-1 from {fast.Name}", res2.Suggestions[0].SuggestionText);
Assert.Equal($"Hello world TeSt-2 from {fast.Name}", res2.Suggestions[1].SuggestionText);
}
finally
{
SubsystemManager.UnregisterSubsystem<ICommandPredictor>(slow.Id);
SubsystemManager.UnregisterSubsystem(SubsystemKind.CommandPredictor, fast.Id);
}
}
[Fact]
public static void Feedback()
{
MyPredictor slow = MyPredictor.SlowPredictor;
MyPredictor fast = MyPredictor.FastPredictor;
try
{
// Register 2 predictor implementations
SubsystemManager.RegisterSubsystem<ICommandPredictor, MyPredictor>(slow);
SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, fast);
var history = new[] { "hello", "world" };
var ids = new HashSet<Guid> { slow.Id, fast.Id };
CommandPrediction.OnCommandLineAccepted(history);
CommandPrediction.OnSuggestionAccepted(slow.Id, "Yeah");
// The calls to 'StartEarlyProcessing' and 'OnSuggestionAccepted' are queued in thread pool,
// so we wait a bit to make sure the calls are done.
while (slow.History.Count == 0 || slow.AcceptedSuggestions.Count == 0)
{
Thread.Sleep(10);
}
Assert.Equal(2, slow.History.Count);
Assert.Equal(history[0], slow.History[0]);
Assert.Equal(history[1], slow.History[1]);
Assert.Equal(2, fast.History.Count);
Assert.Equal(history[0], fast.History[0]);
Assert.Equal(history[1], fast.History[1]);
Assert.Single(slow.AcceptedSuggestions);
Assert.Equal("Yeah", slow.AcceptedSuggestions[0]);
Assert.Empty(fast.AcceptedSuggestions);
}
finally
{
SubsystemManager.UnregisterSubsystem<ICommandPredictor>(slow.Id);
SubsystemManager.UnregisterSubsystem(SubsystemKind.CommandPredictor, fast.Id);
}
}
}
}

View file

@ -0,0 +1,189 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.ObjectModel;
using System.Management.Automation.Subsystem;
using System.Threading;
using Xunit;
namespace PSTests.Sequential
{
public static class SubsystemTests
{
private static readonly MyPredictor predictor1, predictor2;
static SubsystemTests()
{
predictor1 = MyPredictor.FastPredictor;
predictor2 = MyPredictor.SlowPredictor;
}
// This method needs to be updated when there are more than 1 subsystem defined.
private static void VerifySubsystemMetadata(SubsystemInfo ssInfo)
{
Assert.Equal(SubsystemKind.CommandPredictor, ssInfo.Kind);
Assert.Equal(typeof(ICommandPredictor), ssInfo.SubsystemType);
Assert.True(ssInfo.AllowUnregistration);
Assert.True(ssInfo.AllowMultipleRegistration);
Assert.Empty(ssInfo.RequiredCmdlets);
Assert.Empty(ssInfo.RequiredFunctions);
}
[Fact]
public static void GetSubsystemInfo()
{
SubsystemInfo ssInfo = SubsystemManager.GetSubsystemInfo(typeof(ICommandPredictor));
VerifySubsystemMetadata(ssInfo);
Assert.False(ssInfo.IsRegistered);
Assert.Empty(ssInfo.Implementations);
SubsystemInfo ssInfo2 = SubsystemManager.GetSubsystemInfo(SubsystemKind.CommandPredictor);
Assert.Same(ssInfo2, ssInfo);
ReadOnlyCollection<SubsystemInfo> ssInfos = SubsystemManager.GetAllSubsystemInfo();
Assert.Single(ssInfos);
Assert.Same(ssInfos[0], ssInfo);
ICommandPredictor impl = SubsystemManager.GetSubsystem<ICommandPredictor>();
Assert.Null(impl);
ReadOnlyCollection<ICommandPredictor> impls = SubsystemManager.GetSubsystems<ICommandPredictor>();
Assert.Empty(impls);
}
[Fact]
public static void RegisterSubsystem()
{
try
{
Assert.Throws<ArgumentNullException>(
paramName: "proxy",
() => SubsystemManager.RegisterSubsystem<ICommandPredictor, MyPredictor>(null));
Assert.Throws<ArgumentNullException>(
paramName: "proxy",
() => SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, null));
Assert.Throws<ArgumentException>(
paramName: "proxy",
() => SubsystemManager.RegisterSubsystem((SubsystemKind)0, predictor1));
// Register 'predictor1'
SubsystemManager.RegisterSubsystem<ICommandPredictor, MyPredictor>(predictor1);
// Now validate the SubsystemInfo of the 'ICommandPredictor' subsystem
SubsystemInfo ssInfo = SubsystemManager.GetSubsystemInfo(typeof(ICommandPredictor));
VerifySubsystemMetadata(ssInfo);
Assert.True(ssInfo.IsRegistered);
Assert.Single(ssInfo.Implementations);
// Now validate the 'ImplementationInfo'
var implInfo = ssInfo.Implementations[0];
Assert.Equal(predictor1.Id, implInfo.Id);
Assert.Equal(predictor1.Name, implInfo.Name);
Assert.Equal(predictor1.Description, implInfo.Description);
Assert.Equal(SubsystemKind.CommandPredictor, implInfo.Kind);
Assert.Same(typeof(MyPredictor), implInfo.ImplementationType);
// Now validate the all-subsystem-info collection.
ReadOnlyCollection<SubsystemInfo> ssInfos = SubsystemManager.GetAllSubsystemInfo();
Assert.Single(ssInfos);
Assert.Same(ssInfos[0], ssInfo);
// Now validate the subsystem implementation itself.
ICommandPredictor impl = SubsystemManager.GetSubsystem<ICommandPredictor>();
Assert.Same(impl, predictor1);
Assert.Null(impl.FunctionsToDefine);
Assert.Equal(SubsystemKind.CommandPredictor, impl.Kind);
var predCxt = PredictionContext.Create("Hello world");
var results = impl.GetSuggestion(predCxt, CancellationToken.None);
Assert.Equal($"Hello world TEST-1 from {impl.Name}", results[0].SuggestionText);
Assert.Equal($"Hello world TeSt-2 from {impl.Name}", results[1].SuggestionText);
// Now validate the all-subsystem-implementation collection.
ReadOnlyCollection<ICommandPredictor> impls = SubsystemManager.GetSubsystems<ICommandPredictor>();
Assert.Single(impls);
Assert.Same(predictor1, impls[0]);
// Register 'predictor2'
SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, predictor2);
// Now validate the SubsystemInfo of the 'ICommandPredictor' subsystem
VerifySubsystemMetadata(ssInfo);
Assert.True(ssInfo.IsRegistered);
Assert.Equal(2, ssInfo.Implementations.Count);
// Now validate the new 'ImplementationInfo'
implInfo = ssInfo.Implementations[1];
Assert.Equal(predictor2.Id, implInfo.Id);
Assert.Equal(predictor2.Name, implInfo.Name);
Assert.Equal(predictor2.Description, implInfo.Description);
Assert.Equal(SubsystemKind.CommandPredictor, implInfo.Kind);
Assert.Same(typeof(MyPredictor), implInfo.ImplementationType);
// Now validate the new subsystem implementation.
impl = SubsystemManager.GetSubsystem<ICommandPredictor>();
Assert.Same(impl, predictor2);
// Now validate the all-subsystem-implementation collection.
impls = SubsystemManager.GetSubsystems<ICommandPredictor>();
Assert.Equal(2, impls.Count);
Assert.Same(predictor1, impls[0]);
Assert.Same(predictor2, impls[1]);
}
finally
{
SubsystemManager.UnregisterSubsystem<ICommandPredictor>(predictor1.Id);
SubsystemManager.UnregisterSubsystem(SubsystemKind.CommandPredictor, predictor2.Id);
}
}
[Fact]
public static void UnregisterSubsystem()
{
// Exception expected when no implementation is registered
Assert.Throws<InvalidOperationException>(() => SubsystemManager.UnregisterSubsystem<ICommandPredictor>(predictor1.Id));
SubsystemManager.RegisterSubsystem<ICommandPredictor, MyPredictor>(predictor1);
SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, predictor2);
// Exception is expected when specified id cannot be found
Assert.Throws<InvalidOperationException>(() => SubsystemManager.UnregisterSubsystem<ICommandPredictor>(Guid.NewGuid()));
// Unregister 'predictor1'
SubsystemManager.UnregisterSubsystem<ICommandPredictor>(predictor1.Id);
SubsystemInfo ssInfo = SubsystemManager.GetSubsystemInfo(SubsystemKind.CommandPredictor);
VerifySubsystemMetadata(ssInfo);
Assert.True(ssInfo.IsRegistered);
Assert.Single(ssInfo.Implementations);
var implInfo = ssInfo.Implementations[0];
Assert.Equal(predictor2.Id, implInfo.Id);
Assert.Equal(predictor2.Name, implInfo.Name);
Assert.Equal(predictor2.Description, implInfo.Description);
Assert.Equal(SubsystemKind.CommandPredictor, implInfo.Kind);
Assert.Same(typeof(MyPredictor), implInfo.ImplementationType);
ICommandPredictor impl = SubsystemManager.GetSubsystem<ICommandPredictor>();
Assert.Same(impl, predictor2);
ReadOnlyCollection<ICommandPredictor> impls = SubsystemManager.GetSubsystems<ICommandPredictor>();
Assert.Single(impls);
Assert.Same(predictor2, impls[0]);
// Unregister 'predictor2'
SubsystemManager.UnregisterSubsystem(SubsystemKind.CommandPredictor, predictor2.Id);
VerifySubsystemMetadata(ssInfo);
Assert.False(ssInfo.IsRegistered);
Assert.Empty(ssInfo.Implementations);
impl = SubsystemManager.GetSubsystem<ICommandPredictor>();
Assert.Null(impl);
impls = SubsystemManager.GetSubsystems<ICommandPredictor>();
Assert.Empty(impls);
}
}
}

View file

@ -2325,46 +2325,63 @@ function CleanupGeneratedSourceCode
'[System.Runtime.CompilerServices.IsReadOnlyAttribute]'
'[System.Runtime.CompilerServices.NullableContextAttribute('
'[System.Runtime.CompilerServices.NullableAttribute((byte)0)]'
'[System.Runtime.CompilerServices.NullableAttribute(new byte[]{ (byte)2, (byte)1, (byte)1})]'
'[System.Runtime.CompilerServices.AsyncStateMachineAttribute'
)
$patternsToReplace = @(
@{
ApplyTo = "Microsoft.PowerShell.Commands.Utility"
ApplyTo = @("Microsoft.PowerShell.Commands.Utility")
Pattern = "[System.Runtime.CompilerServices.IsReadOnlyAttribute]ref Microsoft.PowerShell.Commands.JsonObject.ConvertToJsonContext"
Replacement = "in Microsoft.PowerShell.Commands.JsonObject.ConvertToJsonContext"
},
@{
ApplyTo = "Microsoft.PowerShell.Commands.Utility"
ApplyTo = @("Microsoft.PowerShell.Commands.Utility")
Pattern = "public partial struct ConvertToJsonContext"
Replacement = "public readonly struct ConvertToJsonContext"
},
@{
ApplyTo = "Microsoft.PowerShell.Commands.Utility"
ApplyTo = @("Microsoft.PowerShell.Commands.Utility")
Pattern = "Unable to resolve assembly 'Assembly(Name=Newtonsoft.Json"
Replacement = "// Unable to resolve assembly 'Assembly(Name=Newtonsoft.Json"
},
@{
ApplyTo = "System.Management.Automation"
ApplyTo = @("System.Management.Automation")
Pattern = "Unable to resolve assembly 'Assembly(Name=System.Security.Principal.Windows"
Replacement = "// Unable to resolve assembly 'Assembly(Name=System.Security.Principal.Windows"
},
@{
ApplyTo = "System.Management.Automation"
ApplyTo = @("System.Management.Automation")
Pattern = "Unable to resolve assembly 'Assembly(Name=Microsoft.Management.Infrastructure"
Replacement = "// Unable to resolve assembly 'Assembly(Name=Microsoft.Management.Infrastructure"
},
@{
ApplyTo = "System.Management.Automation"
ApplyTo = @("System.Management.Automation")
Pattern = "Unable to resolve assembly 'Assembly(Name=System.Security.AccessControl"
Replacement = "// Unable to resolve assembly 'Assembly(Name=System.Security.AccessControl"
},
@{
ApplyTo = "Microsoft.PowerShell.ConsoleHost"
ApplyTo = @("System.Management.Automation")
Pattern = "[System.Runtime.CompilerServices.NullableAttribute(new byte[]{ (byte)1, (byte)2, (byte)1})]"
Replacement = "/* [System.Runtime.CompilerServices.NullableAttribute(new byte[]{ (byte)1, (byte)2, (byte)1})] */ "
},
@{
ApplyTo = @("System.Management.Automation")
Pattern = "[System.Runtime.CompilerServices.NullableAttribute(new byte[]{ (byte)2, (byte)1})]"
Replacement = "/* [System.Runtime.CompilerServices.NullableAttribute(new byte[]{ (byte)2, (byte)1})] */ "
},
@{
ApplyTo = @("System.Management.Automation")
Pattern = "[System.Runtime.CompilerServices.CompilerGeneratedAttribute, System.Runtime.CompilerServices.NullableContextAttribute((byte)2)]"
Replacement = "/* [System.Runtime.CompilerServices.CompilerGeneratedAttribute, System.Runtime.CompilerServices.NullableContextAttribute((byte)2)] */ "
},
@{
ApplyTo = @("System.Management.Automation", "Microsoft.PowerShell.ConsoleHost")
Pattern = "[System.Runtime.CompilerServices.NullableAttribute((byte)2)]"
Replacement = "/* [System.Runtime.CompilerServices.NullableAttribute((byte)2)] */"
},
@{
ApplyTo = "Microsoft.PowerShell.ConsoleHost"
ApplyTo = @("System.Management.Automation", "Microsoft.PowerShell.ConsoleHost")
Pattern = "[System.Runtime.CompilerServices.NullableAttribute((byte)1)]"
Replacement = "/* [System.Runtime.CompilerServices.NullableAttribute((byte)1)] */"
}
@ -2378,7 +2395,7 @@ function CleanupGeneratedSourceCode
$lineWasProcessed = $false
foreach ($patternToReplace in $patternsToReplace)
{
if ($assemblyName -eq $patternToReplace.ApplyTo -and $line.Contains($patternToReplace.Pattern)) {
if ($assemblyName -in $patternToReplace.ApplyTo -and $line.Contains($patternToReplace.Pattern)) {
$line = $line.Replace($patternToReplace.Pattern, $patternToReplace.Replacement)
$lineWasProcessed = $true
break