Replace command-error suggestion with new implementation based on subsystem plugin (#18252)

This commit is contained in:
Dongbo Wang 2022-10-24 11:59:32 -07:00 committed by GitHub
parent 1d129c9a7d
commit 53c3690c2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 836 additions and 122 deletions

View File

@ -3,5 +3,6 @@
"PSCustomTableHeaderLabelDecoration",
"PSLoadAssemblyFromNativeCode",
"PSNativeCommandErrorActionPreference",
"PSSubsystemPluginModel"
"PSSubsystemPluginModel",
"PSFeedbackProvider"
]

View File

@ -3,5 +3,6 @@
"PSCustomTableHeaderLabelDecoration",
"PSLoadAssemblyFromNativeCode",
"PSNativeCommandErrorActionPreference",
"PSSubsystemPluginModel"
"PSSubsystemPluginModel",
"PSFeedbackProvider"
]

View File

@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#pragma warning disable 1634, 1691
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@ -18,6 +16,7 @@ using System.Management.Automation.Language;
using System.Management.Automation.Remoting;
using System.Management.Automation.Remoting.Server;
using System.Management.Automation.Runspaces;
using System.Management.Automation.Subsystem.Feedback;
using System.Management.Automation.Tracing;
using System.Reflection;
using System.Runtime;
@ -2489,7 +2488,6 @@ namespace Microsoft.PowerShell
if (inBlockMode)
{
// use a special prompt that denotes block mode
prompt = ">> ";
}
else
@ -2502,7 +2500,14 @@ namespace Microsoft.PowerShell
// Evaluate any suggestions
if (previousResponseWasEmpty == false)
{
EvaluateSuggestions(ui);
if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSFeedbackProvider))
{
EvaluateFeedbacks(ui);
}
else
{
EvaluateSuggestions(ui);
}
}
// Then output the prompt
@ -2805,6 +2810,77 @@ namespace Microsoft.PowerShell
return remoteException.ErrorRecord.CategoryInfo.Reason == nameof(IncompleteParseException);
}
private void EvaluateFeedbacks(ConsoleHostUserInterface ui)
{
// Output any training suggestions
try
{
List<FeedbackEntry> feedbacks = FeedbackHub.GetFeedback(_parent.Runspace);
if (feedbacks is null || feedbacks.Count == 0)
{
return;
}
// Feedback section starts with a new line.
ui.WriteLine();
const string Indentation = " ";
string nameStyle = PSStyle.Instance.Formatting.FeedbackProvider;
string textStyle = PSStyle.Instance.Formatting.FeedbackText;
string ansiReset = PSStyle.Instance.Reset;
if (!ui.SupportsVirtualTerminal)
{
nameStyle = string.Empty;
textStyle = string.Empty;
ansiReset = string.Empty;
}
int count = 0;
var output = new StringBuilder();
foreach (FeedbackEntry entry in feedbacks)
{
if (count > 0)
{
output.AppendLine();
}
output.Append("Suggestion [")
.Append(nameStyle)
.Append(entry.Name)
.Append(ansiReset)
.AppendLine("]:")
.Append(textStyle);
string[] lines = entry.Text.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lines)
{
output.Append(Indentation)
.Append(line.AsSpan().TrimEnd())
.AppendLine();
}
output.Append(ansiReset);
ui.Write(output.ToString());
count++;
output.Clear();
}
// Feedback section ends with a new line.
ui.WriteLine();
}
catch (Exception e)
{
// Catch-all OK. This is a third-party call-out.
ui.WriteErrorLine(e.Message);
LocalRunspace localRunspace = (LocalRunspace)_parent.Runspace;
localRunspace.GetExecutionContext.AppendDollarError(e);
}
}
private void EvaluateSuggestions(ConsoleHostUserInterface ui)
{
// Output any training suggestions

View File

@ -2030,12 +2030,15 @@ namespace System.Management.Automation.Runspaces
.AddItemScriptBlock(@"""$($_.Strikethrough)$($_.Strikethrough.Replace(""""`e"""",'`e'))$($_.Reset)""", label: "Strikethrough")
.AddItemProperty(@"OutputRendering")
.AddItemScriptBlock(@"""$($_.Formatting.FormatAccent)$($_.Formatting.FormatAccent.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.FormatAccent")
.AddItemScriptBlock(@"""$($_.Formatting.TableHeader)$($_.Formatting.TableHeader.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.TableHeader")
.AddItemScriptBlock(@"""$($_.Formatting.ErrorAccent)$($_.Formatting.ErrorAccent.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.ErrorAccent")
.AddItemScriptBlock(@"""$($_.Formatting.Error)$($_.Formatting.Error.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.Error")
.AddItemScriptBlock(@"""$($_.Formatting.Warning)$($_.Formatting.Warning.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.Warning")
.AddItemScriptBlock(@"""$($_.Formatting.Verbose)$($_.Formatting.Verbose.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.Verbose")
.AddItemScriptBlock(@"""$($_.Formatting.Debug)$($_.Formatting.Debug.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.Debug")
.AddItemScriptBlock(@"""$($_.Formatting.TableHeader)$($_.Formatting.TableHeader.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.TableHeader")
.AddItemScriptBlock(@"""$($_.Formatting.CustomTableHeaderLabel)$($_.Formatting.CustomTableHeaderLabel.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.CustomTableHeaderLabel")
.AddItemScriptBlock(@"""$($_.Formatting.FeedbackProvider)$($_.Formatting.FeedbackProvider.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.FeedbackProvider")
.AddItemScriptBlock(@"""$($_.Formatting.FeedbackText)$($_.Formatting.FeedbackText.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Formatting.FeedbackText")
.AddItemScriptBlock(@"""$($_.Progress.Style)$($_.Progress.Style.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Progress.Style")
.AddItemScriptBlock(@"""$($_.Progress.MaxWidth)""", label: "Progress.MaxWidth")
.AddItemScriptBlock(@"""$($_.Progress.View)""", label: "Progress.View")
@ -2086,12 +2089,15 @@ namespace System.Management.Automation.Runspaces
ListControl.Create()
.StartEntry()
.AddItemScriptBlock(@"""$($_.FormatAccent)$($_.FormatAccent.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "FormatAccent")
.AddItemScriptBlock(@"""$($_.TableHeader)$($_.TableHeader.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "TableHeader")
.AddItemScriptBlock(@"""$($_.ErrorAccent)$($_.ErrorAccent.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "ErrorAccent")
.AddItemScriptBlock(@"""$($_.Error)$($_.Error.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Error")
.AddItemScriptBlock(@"""$($_.Warning)$($_.Warning.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Warning")
.AddItemScriptBlock(@"""$($_.Verbose)$($_.Verbose.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Verbose")
.AddItemScriptBlock(@"""$($_.Debug)$($_.Debug.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "Debug")
.AddItemScriptBlock(@"""$($_.TableHeader)$($_.TableHeader.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "TableHeader")
.AddItemScriptBlock(@"""$($_.CustomTableHeaderLabel)$($_.CustomTableHeaderLabel.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "CustomTableHeaderLabel")
.AddItemScriptBlock(@"""$($_.FeedbackProvider)$($_.FeedbackProvider.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "FeedbackProvider")
.AddItemScriptBlock(@"""$($_.FeedbackText)$($_.FeedbackText.Replace(""""`e"""",'`e'))$($PSStyle.Reset)""", label: "FeedbackText")
.EndEntry()
.EndList());
}

View File

@ -431,6 +431,28 @@ namespace System.Management.Automation
}
private string _debug = "\x1b[33;1m";
/// <summary>
/// Gets or sets the style for rendering feedback provider names.
/// </summary>
public string FeedbackProvider
{
get => _feedbackProvider;
set => _feedbackProvider = ValidateNoContent(value);
}
private string _feedbackProvider = "\x1b[33m";
/// <summary>
/// Gets or sets the style for rendering feedback text.
/// </summary>
public string FeedbackText
{
get => _feedbackText;
set => _feedbackText = ValidateNoContent(value);
}
private string _feedbackText = "\x1b[96m";
}
/// <summary>

View File

@ -23,6 +23,7 @@ namespace System.Management.Automation
internal const string EngineSource = "PSEngine";
internal const string PSNativeCommandErrorActionPreferenceFeatureName = "PSNativeCommandErrorActionPreference";
internal const string PSCustomTableHeaderLabelDecoration = "PSCustomTableHeaderLabelDecoration";
internal const string PSFeedbackProvider = "PSFeedbackProvider";
#endregion
@ -120,6 +121,9 @@ namespace System.Management.Automation
new ExperimentalFeature(
name: PSCustomTableHeaderLabelDecoration,
description: "Formatting differentiation for table header labels that aren't property members"),
new ExperimentalFeature(
name: PSFeedbackProvider,
description: "Replace the hard-coded suggestion framework with the extensible feedback provider"),
};
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);

View File

@ -0,0 +1,228 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Management.Automation.Internal;
using System.Management.Automation.Runspaces;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerShell.Commands;
namespace System.Management.Automation.Subsystem.Feedback
{
/// <summary>
/// The class represents a result from a feedback provider.
/// </summary>
public class FeedbackEntry
{
/// <summary>
/// Gets the Id of the feedback provider.
/// </summary>
public Guid Id { get; }
/// <summary>
/// Gets the name of the feedback provider.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the text of the feedback.
/// </summary>
public string Text { get; }
internal FeedbackEntry(Guid id, string name, string text)
{
Id = id;
Name = name;
Text = text;
}
}
/// <summary>
/// Provides a set of feedbacks for given input.
/// </summary>
public static class FeedbackHub
{
/// <summary>
/// Collect the feedback from registered feedback providers using the default timeout.
/// </summary>
public static List<FeedbackEntry>? GetFeedback(Runspace runspace)
{
return GetFeedback(runspace, millisecondsTimeout: 300);
}
/// <summary>
/// Collect the feedback from registered feedback providers using the specified timeout.
/// </summary>
public static List<FeedbackEntry>? GetFeedback(Runspace runspace, int millisecondsTimeout)
{
Requires.Condition(millisecondsTimeout > 0, nameof(millisecondsTimeout));
var localRunspace = runspace as LocalRunspace;
if (localRunspace is null)
{
return null;
}
// Get the last value of $?
bool questionMarkValue = localRunspace.ExecutionContext.QuestionMarkVariableValue;
if (questionMarkValue)
{
return null;
}
// Get the last history item
HistoryInfo[] histories = localRunspace.History.GetEntries(id: 0, count: 1, newest: true);
if (histories.Length == 0)
{
return null;
}
HistoryInfo lastHistory = histories[0];
// Get the last error
ArrayList errorList = (ArrayList)localRunspace.ExecutionContext.DollarErrorVariable;
if (errorList.Count == 0)
{
return null;
}
var lastError = errorList[0] as ErrorRecord;
if (lastError is null && errorList[0] is RuntimeException rtEx)
{
lastError = rtEx.ErrorRecord;
}
if (lastError?.InvocationInfo is null || lastError.InvocationInfo.HistoryId != lastHistory.Id)
{
return null;
}
var providers = SubsystemManager.GetSubsystems<IFeedbackProvider>();
int count = providers.Count;
if (count == 0)
{
return null;
}
IFeedbackProvider? generalFeedback = null;
List<Task<FeedbackEntry?>>? tasks = null;
CancellationTokenSource? cancellationSource = null;
Func<object?, FeedbackEntry?>? callBack = null;
for (int i = 0; i < providers.Count; i++)
{
IFeedbackProvider provider = providers[i];
if (provider is GeneralCommandErrorFeedback)
{
// This built-in feedback provider needs to run on the target Runspace.
generalFeedback = provider;
continue;
}
if (tasks is null)
{
tasks = new List<Task<FeedbackEntry?>>(capacity: count);
cancellationSource = new CancellationTokenSource();
callBack = GetCallBack(lastHistory.CommandLine, lastError, cancellationSource);
}
// Other feedback providers will run on background threads in parallel.
tasks.Add(Task.Factory.StartNew(
callBack!,
provider,
cancellationSource!.Token,
TaskCreationOptions.DenyChildAttach,
TaskScheduler.Default));
}
Task<Task>? waitTask = null;
if (tasks is not null)
{
waitTask = Task.WhenAny(
Task.WhenAll(tasks),
Task.Delay(millisecondsTimeout, cancellationSource!.Token));
}
List<FeedbackEntry>? resultList = null;
if (generalFeedback is not null)
{
bool changedDefault = false;
Runspace? oldDefault = Runspace.DefaultRunspace;
try
{
if (oldDefault != localRunspace)
{
changedDefault = true;
Runspace.DefaultRunspace = localRunspace;
}
string? text = generalFeedback.GetFeedback(lastHistory.CommandLine, lastError, CancellationToken.None);
if (text is not null)
{
resultList ??= new List<FeedbackEntry>(count);
resultList.Add(new FeedbackEntry(generalFeedback.Id, generalFeedback.Name, text));
}
}
finally
{
if (changedDefault)
{
Runspace.DefaultRunspace = oldDefault;
}
// Restore $? for the target Runspace.
localRunspace.ExecutionContext.QuestionMarkVariableValue = questionMarkValue;
}
}
if (waitTask is not null)
{
try
{
waitTask.Wait();
cancellationSource!.Cancel();
foreach (Task<FeedbackEntry?> task in tasks!)
{
if (task.IsCompletedSuccessfully)
{
FeedbackEntry? result = task.Result;
if (result is not null)
{
resultList ??= new List<FeedbackEntry>(count);
resultList.Add(result);
}
}
}
}
finally
{
cancellationSource!.Dispose();
}
}
return resultList;
}
// A local helper function to avoid creating an instance of the generated delegate helper class
// when no feedback provider is registered.
private static Func<object?, FeedbackEntry?> GetCallBack(
string commandLine,
ErrorRecord lastError,
CancellationTokenSource cancellationSource)
{
return state =>
{
var provider = (IFeedbackProvider)state!;
var text = provider.GetFeedback(commandLine, lastError, cancellationSource.Token);
return text is null ? null : new FeedbackEntry(provider.Id, provider.Name, text);
};
}
}
}

View File

@ -0,0 +1,285 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Management.Automation.Internal;
using System.Management.Automation.Runspaces;
using System.Management.Automation.Subsystem.Prediction;
using System.Threading;
namespace System.Management.Automation.Subsystem.Feedback
{
/// <summary>
/// Interface for implementing a feedback provider on command failures.
/// </summary>
public interface IFeedbackProvider : ISubsystem
{
/// <summary>
/// Default implementation. No function is required for a feedback provider.
/// </summary>
Dictionary<string, string>? ISubsystem.FunctionsToDefine => null;
/// <summary>
/// Default implementation for `ISubsystem.Kind`.
/// </summary>
SubsystemKind ISubsystem.Kind => SubsystemKind.FeedbackProvider;
/// <summary>
/// Gets feedback based on the given commandline and error record.
/// </summary>
/// <returns></returns>
string? GetFeedback(string commandLine, ErrorRecord lastError, CancellationToken token);
}
internal sealed class GeneralCommandErrorFeedback : IFeedbackProvider
{
private readonly Guid _guid;
private readonly object[] _args;
private ScriptBlock? _fuzzySb;
internal GeneralCommandErrorFeedback()
{
_guid = new Guid("A3C6B07E-4A89-40C9-8BE6-2A9AAD2786A4");
_args = new object[1];
}
public Guid Id => _guid;
public string Name => "General";
public string Description => "The built-in general feedback source for command errors.";
public string? GetFeedback(string commandLine, ErrorRecord lastError, CancellationToken token)
{
var rsToUse = Runspace.DefaultRunspace;
if (rsToUse is null)
{
return null;
}
if (lastError.FullyQualifiedErrorId == "CommandNotFoundException")
{
EngineIntrinsics context = rsToUse.ExecutionContext.EngineIntrinsics;
var target = (string)lastError.TargetObject;
CommandInvocationIntrinsics invocation = context.SessionState.InvokeCommand;
// See if target is actually an executable file in current directory.
var localTarget = Path.Combine(".", target);
var command = invocation.GetCommand(
localTarget,
CommandTypes.Application | CommandTypes.ExternalScript);
if (command is not null)
{
return StringUtil.Format(
SuggestionStrings.Suggestion_CommandExistsInCurrentDirectory,
target,
localTarget);
}
// Check fuzzy matching command names.
if (ExperimentalFeature.IsEnabled("PSCommandNotFoundSuggestion"))
{
_fuzzySb ??= ScriptBlock.CreateDelayParsedScriptBlock(@$"
param([string] $target)
$cmdNames = Get-Command $target -UseFuzzyMatching -FuzzyMinimumDistance 1 | Select-Object -First 5 -Unique -ExpandProperty Name
if ($cmdNames) {{
[string]::Join(', ', $cmdNames)
}}
", isProductCode: true);
_args[0] = target;
var result = _fuzzySb.InvokeReturnAsIs(_args);
if (result is not null && result != AutomationNull.Value)
{
return StringUtil.Format(
SuggestionStrings.Suggestion_CommandNotFound,
result.ToString());
}
}
}
return null;
}
}
internal sealed class UnixCommandNotFound : IFeedbackProvider, ICommandPredictor
{
private readonly Guid _guid;
private string? _notFoundFeedback;
private List<string>? _candidates;
internal UnixCommandNotFound()
{
_guid = new Guid("47013747-CB9D-4EBC-9F02-F32B8AB19D48");
}
Dictionary<string, string>? ISubsystem.FunctionsToDefine => null;
SubsystemKind ISubsystem.Kind => SubsystemKind.FeedbackProvider | SubsystemKind.CommandPredictor;
public Guid Id => _guid;
public string Name => "cmd-not-found";
public string Description => "The built-in feedback/prediction source for the Unix command utility.";
#region IFeedbackProvider
private static string? GetUtilityPath()
{
string cmd_not_found = "/usr/lib/command-not-found";
bool exist = IsFileExecutable(cmd_not_found);
if (!exist)
{
cmd_not_found = "/usr/share/command-not-found/command-not-found";
exist = IsFileExecutable(cmd_not_found);
}
return exist ? cmd_not_found : null;
static bool IsFileExecutable(string path)
{
var file = new FileInfo(path);
return file.Exists && file.UnixFileMode.HasFlag(UnixFileMode.OtherExecute);
}
}
/// <summary>
/// Gets feedback based on the given commandline and error record.
/// </summary>
public string? GetFeedback(string commandLine, ErrorRecord lastError, CancellationToken token)
{
if (Platform.IsWindows || lastError.FullyQualifiedErrorId != "CommandNotFoundException")
{
return null;
}
var target = (string)lastError.TargetObject;
if (target is null)
{
return null;
}
if (target.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase))
{
return null;
}
string? cmd_not_found = GetUtilityPath();
if (cmd_not_found is not null)
{
var startInfo = new ProcessStartInfo(cmd_not_found);
startInfo.ArgumentList.Add(target);
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
using var process = Process.Start(startInfo);
var stderr = process?.StandardError.ReadToEnd().Trim();
// The feedback contains recommended actions only if the output has multiple lines of text.
if (stderr?.IndexOf('\n') > 0)
{
_notFoundFeedback = stderr;
var stdout = process?.StandardOutput.ReadToEnd().Trim();
return string.IsNullOrEmpty(stdout) ? stderr : $"{stderr}\n{stdout}";
}
}
return null;
}
#endregion
#region ICommandPredictor
public bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback)
{
return feedback switch
{
PredictorFeedbackKind.CommandLineAccepted => true,
_ => false,
};
}
public SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken)
{
if (_candidates is null && _notFoundFeedback is not null)
{
var text = _notFoundFeedback.AsSpan();
// Set to null to avoid potential race condition.
_notFoundFeedback = null;
// This loop searches for candidate results with almost no allocation.
while (true)
{
// The line is a candidate if it starts with "sudo ", such as "sudo apt install python3".
// 'sudo' is a command name that remains the same, so this check should work for all locales.
bool isCandidate = text.StartsWith("sudo ", StringComparison.Ordinal);
int index = text.IndexOf('\n');
if (isCandidate)
{
var line = index != -1 ? text.Slice(0, index) : text;
_candidates ??= new List<string>();
_candidates.Add(new string(line.TrimEnd()));
}
// Break out the loop if we are done with the last line.
if (index == -1 || index == text.Length - 1)
{
break;
}
// Point to the rest of feedback text.
text = text.Slice(index + 1);
}
}
if (_candidates is not null)
{
string input = context.InputAst.Extent.Text;
List<PredictiveSuggestion>? result = null;
foreach (string c in _candidates)
{
if (c.StartsWith(input, StringComparison.OrdinalIgnoreCase))
{
result ??= new List<PredictiveSuggestion>(_candidates.Count);
result.Add(new PredictiveSuggestion(c));
}
}
if (result is not null)
{
return new SuggestionPackage(result);
}
}
return default;
}
public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList<string> history)
{
// Reset the candidate state.
_notFoundFeedback = null;
_candidates = null;
}
public void OnSuggestionDisplayed(PredictionClient client, uint session, int countOrIndex) { }
public void OnSuggestionAccepted(PredictionClient client, uint session, string acceptedSuggestion) { }
public void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success) { }
#endregion;
}
}

View File

@ -10,8 +10,10 @@ namespace System.Management.Automation.Subsystem
{
/// <summary>
/// Define the kinds of subsystems.
/// Allow composite enum values to enable one subsystem implementation to serve as multiple subystems.
/// </summary>
public enum SubsystemKind
[Flags]
public enum SubsystemKind : uint
{
/// <summary>
/// Component that provides predictive suggestions to commandline input.
@ -22,6 +24,11 @@ namespace System.Management.Automation.Subsystem
/// Cross platform desired state configuration component.
/// </summary>
CrossPlatformDsc = 2,
/// <summary>
/// Component that provides feedback when a command fails interactively.
/// </summary>
FeedbackProvider = 4,
}
/// <summary>

View File

@ -78,6 +78,8 @@ namespace System.Management.Automation.Subsystem
private protected SubsystemInfo(SubsystemKind kind, Type subsystemType)
{
Requires.OneSpecificSubsystemKind(kind);
_syncObj = new object();
_cachedImplInfos = Utils.EmptyReadOnlyCollection<ImplementationInfo>();

View File

@ -6,9 +6,9 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation.Internal;
using System.Management.Automation.Subsystem.DSC;
using System.Management.Automation.Subsystem.Feedback;
using System.Management.Automation.Subsystem.Prediction;
namespace System.Management.Automation.Subsystem
@ -35,6 +35,11 @@ namespace System.Management.Automation.Subsystem
SubsystemKind.CrossPlatformDsc,
allowUnregistration: true,
allowMultipleRegistration: false),
SubsystemInfo.Create<IFeedbackProvider>(
SubsystemKind.FeedbackProvider,
allowUnregistration: true,
allowMultipleRegistration: true),
};
var subSystemTypeMap = new Dictionary<Type, SubsystemInfo>(subsystems.Length);
@ -49,6 +54,9 @@ namespace System.Management.Automation.Subsystem
s_subsystems = new ReadOnlyCollection<SubsystemInfo>(subsystems);
s_subSystemTypeMap = new ReadOnlyDictionary<Type, SubsystemInfo>(subSystemTypeMap);
s_subSystemKindMap = new ReadOnlyDictionary<SubsystemKind, SubsystemInfo>(subSystemKindMap);
// Register built-in suggestion providers.
RegisterSubsystem(SubsystemKind.FeedbackProvider, new GeneralCommandErrorFeedback());
}
#region internal - Retrieve subsystem proxy object
@ -143,6 +151,8 @@ namespace System.Management.Automation.Subsystem
/// <returns>The <see cref="SubsystemInfo"/> object that represents the concrete subsystem.</returns>
public static SubsystemInfo GetSubsystemInfo(SubsystemKind kind)
{
Requires.OneSpecificSubsystemKind(kind);
if (s_subSystemKindMap.TryGetValue(kind, out SubsystemInfo? subsystemInfo))
{
return subsystemInfo;
@ -181,8 +191,9 @@ namespace System.Management.Automation.Subsystem
public static void RegisterSubsystem(SubsystemKind kind, ISubsystem proxy)
{
Requires.NotNull(proxy, nameof(proxy));
Requires.OneSpecificSubsystemKind(kind);
if (kind != proxy.Kind)
if (!proxy.Kind.HasFlag(kind))
{
throw new ArgumentException(
StringUtil.Format(
@ -267,6 +278,8 @@ namespace System.Management.Automation.Subsystem
/// <param name="id">The Id of the implementation to be unregistered.</param>
public static void UnregisterSubsystem(SubsystemKind kind, Guid id)
{
Requires.OneSpecificSubsystemKind(kind);
UnregisterSubsystem(GetSubsystemInfo(kind), id);
}

View File

@ -1788,5 +1788,19 @@ namespace System.Management.Automation.Internal
throw new ArgumentException(paramName);
}
}
internal static void OneSpecificSubsystemKind(Subsystem.SubsystemKind kind)
{
uint value = (uint)kind;
if (value == 0 || (value & (value - 1)) != 0)
{
// The value is either invalid or a composite value because it's not power of 2.
throw new ArgumentException(
StringUtil.Format(
SubsystemStrings.RequireOneSpecificSubsystemKind,
kind.ToString()),
nameof(kind));
}
}
}
}

View File

@ -69,7 +69,7 @@ namespace System.Management.Automation
[System.Diagnostics.DebuggerHidden()]
param([string] $formatString)
$formatString -f [string]::Join(', ', (Get-Command $lastError.TargetObject -UseFuzzyMatch | Select-Object -First 10 -Unique -ExpandProperty Name))
$formatString -f [string]::Join(', ', (Get-Command $lastError.TargetObject -UseFuzzyMatching -FuzzyMinimumDistance 1 | Select-Object -First 5 -Unique -ExpandProperty Name))
";
private static readonly List<Hashtable> s_suggestions = InitializeSuggestions();
@ -79,20 +79,6 @@ namespace System.Management.Automation
var suggestions = new List<Hashtable>(
new Hashtable[]
{
NewSuggestion(
id: 1,
category: "Transactions",
matchType: SuggestionMatchType.Command,
rule: "^Start-Transaction",
suggestion: SuggestionStrings.Suggestion_StartTransaction,
enabled: true),
NewSuggestion(
id: 2,
category: "Transactions",
matchType: SuggestionMatchType.Command,
rule: "^Use-Transaction",
suggestion: SuggestionStrings.Suggestion_UseTransaction,
enabled: true),
NewSuggestion(
id: 3,
category: "General",
@ -138,16 +124,6 @@ namespace System.Management.Automation
return returnValue;
}
/// <summary>
/// Gets an array of commands that can be run sequentially to set $profile and run the profile commands.
/// </summary>
/// <param name="shellId">The id identifying the host or shell used in profile file names.</param>
/// <returns></returns>
internal static PSCommand[] GetProfileCommands(string shellId)
{
return HostUtilities.GetProfileCommands(shellId, false);
}
/// <summary>
/// Gets the object that serves as a value to $profile and the paths on it.
/// </summary>
@ -561,30 +537,6 @@ namespace System.Management.Automation
return message;
}
/// <summary>
/// Create suggestion with string rule and suggestion.
/// </summary>
/// <param name="id">Identifier for the suggestion.</param>
/// <param name="category">Category for the suggestion.</param>
/// <param name="matchType">Suggestion match type.</param>
/// <param name="rule">Rule to match.</param>
/// <param name="suggestion">Suggestion to return.</param>
/// <param name="enabled">True if the suggestion is enabled.</param>
/// <returns>Hashtable representing the suggestion.</returns>
private static Hashtable NewSuggestion(int id, string category, SuggestionMatchType matchType, string rule, string suggestion, bool enabled)
{
Hashtable result = new Hashtable(StringComparer.CurrentCultureIgnoreCase);
result["Id"] = id;
result["Category"] = category;
result["MatchType"] = matchType;
result["Rule"] = rule;
result["Suggestion"] = suggestion;
result["Enabled"] = enabled;
return result;
}
/// <summary>
/// Create suggestion with string rule and scriptblock suggestion.
/// </summary>
@ -639,15 +591,6 @@ namespace System.Management.Automation
return result;
}
/// <summary>
/// Get suggestion text from suggestion scriptblock.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Need to keep this for legacy reflection based use")]
private static string GetSuggestionText(object suggestion, PSModuleInfo invocationModule)
{
return GetSuggestionText(suggestion, null, invocationModule);
}
/// <summary>
/// Get suggestion text from suggestion scriptblock with arguments.
/// </summary>
@ -702,43 +645,6 @@ namespace System.Management.Automation
return string.Format(CultureInfo.InvariantCulture, "[{0}]: {1}", runspace.ConnectionInfo.ComputerName, basePrompt);
}
internal static bool IsProcessInteractive(InvocationInfo invocationInfo)
{
#if CORECLR
return false;
#else
// CommandOrigin != Runspace means it is in a script
if (invocationInfo.CommandOrigin != CommandOrigin.Runspace)
return false;
// If we don't own the window handle, we've been invoked
// from another process that just calls "PowerShell -Command"
if (System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle == IntPtr.Zero)
return false;
// If the window has been idle for less than two seconds,
// they're probably still calling "PowerShell -Command"
// but from Start-Process, or the StartProcess API
try
{
System.Diagnostics.Process currentProcess = System.Diagnostics.Process.GetCurrentProcess();
TimeSpan timeSinceStart = DateTime.Now - currentProcess.StartTime;
TimeSpan idleTime = timeSinceStart - currentProcess.TotalProcessorTime;
// Making it 2 seconds because of things like delayed prompt
if (idleTime.TotalSeconds > 2)
return true;
}
catch (System.ComponentModel.Win32Exception)
{
// Don't have access to the properties
return false;
}
return false;
#endif
}
/// <summary>
/// Create a configured remote runspace from provided name.
/// </summary>

View File

@ -153,4 +153,7 @@
<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>
<data name="RequireOneSpecificSubsystemKind" xml:space="preserve">
<value>The subsystem kind here should represent one specific subsystem, but the given value is either invalid or a composite enum value: '{0}'.</value>
</data>
</root>

View File

@ -117,14 +117,8 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Suggestion_StartTransaction" xml:space="preserve">
<value>Once a transaction is started, only commands that get called with the -UseTransaction flag become part of that transaction.</value>
</data>
<data name="Suggestion_UseTransaction" xml:space="preserve">
<value>The Use-Transaction cmdlet is intended for scripting of transaction-enabled .NET objects. Its ScriptBlock should contain nothing else.</value>
</data>
<data name="Suggestion_CommandExistsInCurrentDirectory" xml:space="preserve">
<value>The command {0} was not found, but does exist in the current location. PowerShell does not load commands from the current location by default. If you trust this command, instead type: "{1}". See "get-help about_Command_Precedence" for more details.</value>
<value>The command "{0}" was not found, but does exist in the current location. PowerShell does not load commands from the current location by default. If you trust this command, instead type: "{1}". See "get-help about_Command_Precedence" for more details.</value>
</data>
<data name="Suggestion_CommandNotFound" xml:space="preserve">
<value>The most similar commands are: {0}.</value>

View File

@ -0,0 +1,120 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Subsystem;
using System.Management.Automation.Subsystem.Feedback;
using System.Threading;
using Xunit;
namespace PSTests.Sequential
{
public class MyFeedback : IFeedbackProvider
{
private readonly Guid _id;
private readonly string _name, _description;
private readonly bool _delay;
public static readonly MyFeedback SlowFeedback;
static MyFeedback()
{
SlowFeedback = new MyFeedback(
Guid.NewGuid(),
"Slow",
"Description for #1 feedback provider.",
delay: true);
}
private MyFeedback(Guid id, string name, string description, bool delay)
{
_id = id;
_name = name;
_description = description;
_delay = delay;
}
public Guid Id => _id;
public string Name => _name;
public string Description => _description;
public string GetFeedback(string commandLine, ErrorRecord errorRecord, CancellationToken token)
{
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);
}
return $"{commandLine}+{errorRecord.FullyQualifiedErrorId}";
}
}
public static class FeedbackProviderTests
{
[Fact]
public static void GetFeedback()
{
var pwsh = PowerShell.Create();
var settings = new PSInvocationSettings() { AddToHistory = true };
// Setup the context for the test.
// Change current working directory to the temp path.
pwsh.AddCommand("Set-Location")
.AddParameter("Path", Path.GetTempPath())
.Invoke(input: null, settings);
pwsh.Commands.Clear();
// Create an empty file 'feedbacktest.ps1' under the temp path;
pwsh.AddCommand("New-Item")
.AddParameter("Path", "feedbacktest.ps1")
.Invoke(input: null, settings);
pwsh.Commands.Clear();
// Run a command 'feedbacktest', so as to trigger the 'General' feedback.
pwsh.AddScript("feedbacktest").Invoke(input: null, settings);
pwsh.Commands.Clear();
try
{
// Register the slow feedback provider.
// The 'General' feedback provider is built-in and registered by default.
SubsystemManager.RegisterSubsystem(SubsystemKind.FeedbackProvider, MyFeedback.SlowFeedback);
// Expect the result from 'General' only because 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.
var feedbacks = FeedbackHub.GetFeedback(pwsh.Runspace, millisecondsTimeout: 1000);
string expectedCmd = Path.Combine(".", "feedbacktest");
// Test the result from the 'General' feedback provider.
Assert.Single(feedbacks);
Assert.Equal("General", feedbacks[0].Name);
Assert.Contains(expectedCmd, feedbacks[0].Text);
// Expect the result from both 'General' and the 'slow' feedback providers.
// 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.
feedbacks = FeedbackHub.GetFeedback(pwsh.Runspace, millisecondsTimeout: 4000);
Assert.Equal(2, feedbacks.Count);
FeedbackEntry entry1 = feedbacks[0];
Assert.Equal("General", entry1.Name);
Assert.Contains(expectedCmd, entry1.Text);
FeedbackEntry entry2 = feedbacks[1];
Assert.Equal("Slow", entry2.Name);
Assert.Equal("feedbacktest+CommandNotFoundException", entry2.Text);
}
finally
{
SubsystemManager.UnregisterSubsystem<IFeedbackProvider>(MyFeedback.SlowFeedback.Id);
}
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.ObjectModel;
using System.Management.Automation.Subsystem;
using System.Management.Automation.Subsystem.DSC;
using System.Management.Automation.Subsystem.Feedback;
using System.Management.Automation.Subsystem.Prediction;
using System.Threading;
using Xunit;
@ -41,37 +42,63 @@ namespace PSTests.Sequential
Assert.Empty(ssInfo.RequiredFunctions);
}
private static void VerifyFeedbackProviderMetadata(SubsystemInfo ssInfo)
{
Assert.Equal(SubsystemKind.FeedbackProvider, ssInfo.Kind);
Assert.Equal(typeof(IFeedbackProvider), ssInfo.SubsystemType);
Assert.True(ssInfo.AllowUnregistration);
Assert.True(ssInfo.AllowMultipleRegistration);
Assert.Empty(ssInfo.RequiredCmdlets);
Assert.Empty(ssInfo.RequiredFunctions);
}
[Fact]
public static void GetSubsystemInfo()
{
#region Predictor
SubsystemInfo predictorInfo = SubsystemManager.GetSubsystemInfo(typeof(ICommandPredictor));
SubsystemInfo predictorInfo2 = SubsystemManager.GetSubsystemInfo(SubsystemKind.CommandPredictor);
Assert.Same(predictorInfo2, predictorInfo);
VerifyCommandPredictorMetadata(predictorInfo);
Assert.False(predictorInfo.IsRegistered);
Assert.Empty(predictorInfo.Implementations);
#endregion
SubsystemInfo predictorInfo2 = SubsystemManager.GetSubsystemInfo(SubsystemKind.CommandPredictor);
Assert.Same(predictorInfo2, predictorInfo);
#region Feedback
SubsystemInfo feedbackProviderInfo = SubsystemManager.GetSubsystemInfo(typeof(IFeedbackProvider));
SubsystemInfo feedback2 = SubsystemManager.GetSubsystemInfo(SubsystemKind.FeedbackProvider);
Assert.Same(feedback2, feedbackProviderInfo);
VerifyFeedbackProviderMetadata(feedbackProviderInfo);
Assert.True(feedbackProviderInfo.IsRegistered);
Assert.Single(feedbackProviderInfo.Implementations);
#endregion
#region DSC
SubsystemInfo crossPlatformDscInfo = SubsystemManager.GetSubsystemInfo(typeof(ICrossPlatformDsc));
VerifyCrossPlatformDscMetadata(crossPlatformDscInfo);
Assert.False(crossPlatformDscInfo.IsRegistered);
Assert.Empty(crossPlatformDscInfo.Implementations);
SubsystemInfo crossPlatformDscInfo2 = SubsystemManager.GetSubsystemInfo(SubsystemKind.CrossPlatformDsc);
Assert.Same(crossPlatformDscInfo2, crossPlatformDscInfo);
VerifyCrossPlatformDscMetadata(crossPlatformDscInfo);
Assert.False(crossPlatformDscInfo.IsRegistered);
Assert.Empty(crossPlatformDscInfo.Implementations);
#endregion
ReadOnlyCollection<SubsystemInfo> ssInfos = SubsystemManager.GetAllSubsystemInfo();
Assert.Equal(2, ssInfos.Count);
Assert.Equal(3, ssInfos.Count);
Assert.Same(ssInfos[0], predictorInfo);
Assert.Same(ssInfos[1], crossPlatformDscInfo);
Assert.Same(ssInfos[2], feedbackProviderInfo);
ICommandPredictor predictorImpl = SubsystemManager.GetSubsystem<ICommandPredictor>();
Assert.Null(predictorImpl);
ReadOnlyCollection<ICommandPredictor> predictorImpls = SubsystemManager.GetSubsystems<ICommandPredictor>();
Assert.Empty(predictorImpls);
ReadOnlyCollection<IFeedbackProvider> feedbackImpls = SubsystemManager.GetSubsystems<IFeedbackProvider>();
Assert.Single(feedbackImpls);
ICrossPlatformDsc crossPlatformDscImpl = SubsystemManager.GetSubsystem<ICrossPlatformDsc>();
Assert.Null(crossPlatformDscImpl);
ReadOnlyCollection<ICrossPlatformDsc> crossPlatformDscImpls = SubsystemManager.GetSubsystems<ICrossPlatformDsc>();
@ -91,14 +118,19 @@ namespace PSTests.Sequential
() => SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, null));
Assert.Throws<ArgumentException>(
paramName: "proxy",
() => SubsystemManager.RegisterSubsystem(SubsystemKind.CrossPlatformDsc, predictor1));
Assert.Throws<ArgumentException>(
paramName: "kind",
() => SubsystemManager.RegisterSubsystem((SubsystemKind)0, predictor1));
Assert.Throws<ArgumentException>(
paramName: "kind",
() => SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor | SubsystemKind.FeedbackProvider, predictor1));
// Register 'predictor1'
SubsystemManager.RegisterSubsystem<ICommandPredictor, MyPredictor>(predictor1);
// Now validate the SubsystemInfo of the 'ICommandPredictor' subsystem
SubsystemInfo ssInfo = SubsystemManager.GetSubsystemInfo(typeof(ICommandPredictor));
SubsystemInfo crossPlatformDscInfo = SubsystemManager.GetSubsystemInfo(typeof(ICrossPlatformDsc));
VerifyCommandPredictorMetadata(ssInfo);
Assert.True(ssInfo.IsRegistered);
Assert.Single(ssInfo.Implementations);