Initial work of the subsystem plugin model (for minimal powershell) (#13186)
This commit is contained in:
parent
10fdfc4ac6
commit
fc4c9cbfd7
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
156
src/System.Management.Automation/resources/SubsystemStrings.resx
Normal file
156
src/System.Management.Automation/resources/SubsystemStrings.resx
Normal 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>
|
|
@ -15,7 +15,8 @@ $script:cmdletsToSkip = @(
|
|||
"Enable-PSRemoting",
|
||||
"Get-ExperimentalFeature",
|
||||
"Enable-ExperimentalFeature",
|
||||
"Disable-ExperimentalFeature"
|
||||
"Disable-ExperimentalFeature",
|
||||
"Get-Subsystem"
|
||||
)
|
||||
|
||||
function UpdateHelpFromLocalContentPath {
|
||||
|
|
193
test/xUnit/csharp/test_Prediction.cs
Normal file
193
test/xUnit/csharp/test_Prediction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
189
test/xUnit/csharp/test_Subsystem.cs
Normal file
189
test/xUnit/csharp/test_Subsystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue