Skip to content

Commit 800518c

Browse files
Merge pull request #1577 from rjmholt/review-fixes
Address review comments
2 parents aa263eb + 34836e8 commit 800518c

19 files changed

+172
-111
lines changed

src/PowerShellEditorServices/Server/PsesLanguageServer.cs

-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ public async Task StartAsync()
113113
// _Initialize_ request:
114114
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#initialize
115115
.OnInitialize(
116-
// TODO: Either fix or ignore "method lacks 'await'" warning.
117116
(languageServer, request, cancellationToken) =>
118117
{
119118
Log.Logger.Debug("Initializing OmniSharp Language Server");

src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public static IServiceCollection AddPsesLanguageServices(
4747
provider.GetService<EditorOperationsService>(),
4848
provider.GetService<PowerShellExecutionService>());
4949

50+
// This is where we create the $psEditor variable
51+
// so that when the console is ready, it will be available
52+
// TODO: Improve the sequencing here so that:
53+
// - The variable is guaranteed to be initialized when the console first appears
54+
// - Any errors that occur are handled rather than lost by the unawaited task
5055
extensionService.InitializeAsync();
5156

5257
return extensionService;

src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
1818
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
1919
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
20-
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Runspace;
2120

2221
namespace Microsoft.PowerShell.EditorServices.Services
2322
{
@@ -182,7 +181,7 @@ public async Task<BreakpointDetails[]> SetLineBreakpointsAsync(
182181

183182
// Fix for issue #123 - file paths that contain wildcard chars [ and ] need to
184183
// quoted and have those wildcard chars escaped.
185-
string escapedScriptPath = PathUtils.WildcardEscape(scriptPath);
184+
string escapedScriptPath = PathUtils.WildcardEscapePath(scriptPath);
186185

187186
if (dscBreakpoints == null || !dscBreakpoints.IsDscResourcePath(escapedScriptPath))
188187
{

src/PowerShellEditorServices/Services/PowerShell/Console/ColorConfiguration.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using System.Collections.Generic;
33
using System.Text;
44

5-
namespace PowerShellEditorServices.Services.PowerShell.Console
5+
namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Console
66
{
77
internal class ColorConfiguration
88
{

src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs

-2
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,6 @@ public static PowerShellVersionDetails GetVersionDetails(ILogger logger, PowerSh
103103

104104
try
105105
{
106-
var psVersionTableCommand = new PSCommand().AddScript("$PSVersionTable", useLocalScope: true);
107-
108106
Hashtable psVersionTable = pwsh
109107
.AddScript("$PSVersionTable", useLocalScope: true)
110108
.InvokeAndClear<Hashtable>()

src/PowerShellEditorServices/Services/PowerShell/Debugging/PowerShellDebugContext.cs

+23
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,29 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging
1010
using OmniSharp.Extensions.LanguageServer.Protocol.Server;
1111
using System.Threading.Tasks;
1212

13+
/// <summary>
14+
/// Handles the state of the PowerShell debugger.
15+
/// </summary>
16+
/// <remarks>
17+
/// <para>
18+
/// Debugging through a PowerShell Host is implemented by registering a handler
19+
/// for the <see cref="System.Management.Automation.Debugger.DebuggerStop"/> event.
20+
/// Registering that handler causes debug actions in PowerShell like Set-PSBreakpoint
21+
/// and Wait-Debugger to drop into the debugger and trigger the handler.
22+
/// The handler is passed a mutable <see cref="System.Management.Automation.DebuggerStopEventArgs"/> object
23+
/// and the debugger stop lasts for the duration of the handler call.
24+
/// The handler sets the <see cref="System.Management.Automation.DebuggerStopEventArgs.ResumeAction"/> property
25+
/// when after it returns, the PowerShell debugger uses that as the direction on how to proceed.
26+
/// </para>
27+
/// <para>
28+
/// When we handle the <see cref="System.Management.Automation.Debugger.DebuggerStop"/> event,
29+
/// we drop into a nested debug prompt and execute things in the debugger with <see cref="System.Management.Automation.Debugger.ProcessCommand(PSCommand, PSDataCollection{PSObject})"/>,
30+
/// which enables debugger commands like <c>l</c>, <c>c</c>, <c>s</c>, etc.
31+
/// <see cref="Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging.PowerShellDebugContext"/> saves the event args object in its state,
32+
/// and when one of the debugger commands is used, the result returned is used to set <see cref="System.Management.Automation.DebuggerStopEventArgs.ResumeAction"/>
33+
/// on the saved event args object so that when the event handler returns, the PowerShell debugger takes the correct action.
34+
/// </para>
35+
/// </remarks>
1336
internal class PowerShellDebugContext : IPowerShellDebugContext
1437
{
1538
private readonly ILogger _logger;

src/PowerShellEditorServices/Services/PowerShell/Execution/ExecutionCanceledException.cs

-12
This file was deleted.

src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs

+13
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,13 @@ private IReadOnlyList<TResult> ExecuteNormally(CancellationToken cancellationTok
7171
result = _pwsh.InvokeCommand<TResult>(_psCommand);
7272
cancellationToken.ThrowIfCancellationRequested();
7373
}
74+
// Test if we've been cancelled. If we're remoting, PSRemotingDataStructureException effectively means the pipeline was stopped.
7475
catch (Exception e) when (cancellationToken.IsCancellationRequested || e is PipelineStoppedException || e is PSRemotingDataStructureException)
7576
{
7677
throw new OperationCanceledException();
7778
}
79+
// We only catch RuntimeExceptions here in case writing errors to output was requested
80+
// Other errors are bubbled up to the caller
7881
catch (RuntimeException e)
7982
{
8083
Logger.LogWarning($"Runtime exception occurred while executing command:{Environment.NewLine}{Environment.NewLine}{e}");
@@ -107,6 +110,9 @@ private IReadOnlyList<TResult> ExecuteInDebugger(CancellationToken cancellationT
107110

108111
var outputCollection = new PSDataCollection<PSObject>();
109112

113+
// Out-Default doesn't work as needed in the debugger
114+
// Instead we add Out-String to the command and collect results in a PSDataCollection
115+
// and use the event handler to print output to the UI as its added to that collection
110116
if (_executionOptions.WriteOutputToHost)
111117
{
112118
_psCommand.AddDebugOutputCommand();
@@ -124,14 +130,21 @@ private IReadOnlyList<TResult> ExecuteInDebugger(CancellationToken cancellationT
124130
DebuggerCommandResults debuggerResult = null;
125131
try
126132
{
133+
// In the PowerShell debugger, extra debugger commands are made available, like "l", "s", "c", etc.
134+
// Executing those commands produces a result that needs to be set on the debugger stop event args.
135+
// So we use the Debugger.ProcessCommand() API to properly execute commands in the debugger
136+
// and then call DebugContext.ProcessDebuggerResult() later to handle the command appropriately
127137
debuggerResult = _pwsh.Runspace.Debugger.ProcessCommand(_psCommand, outputCollection);
128138
cancellationToken.ThrowIfCancellationRequested();
129139
}
140+
// Test if we've been cancelled. If we're remoting, PSRemotingDataStructureException effectively means the pipeline was stopped.
130141
catch (Exception e) when (cancellationToken.IsCancellationRequested || e is PipelineStoppedException || e is PSRemotingDataStructureException)
131142
{
132143
StopDebuggerIfRemoteDebugSessionFailed();
133144
throw new OperationCanceledException();
134145
}
146+
// We only catch RuntimeExceptions here in case writing errors to output was requested
147+
// Other errors are bubbled up to the caller
135148
catch (RuntimeException e)
136149
{
137150
Logger.LogWarning($"Runtime exception occurred while executing command:{Environment.NewLine}{Environment.NewLine}{e}");

src/PowerShellEditorServices/Services/PowerShell/Handlers/EvaluateHandler.cs

+7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010

1111
namespace Microsoft.PowerShell.EditorServices.Handlers
1212
{
13+
/// <summary>
14+
/// Handler for a custom request type for evaluating PowerShell.
15+
/// This is generally for F8 support, to allow execution of a highlighted code snippet in the console as if it were copy-pasted.
16+
/// </summary>
1317
internal class EvaluateHandler : IEvaluateHandler
1418
{
1519
private readonly ILogger _logger;
@@ -25,6 +29,9 @@ public EvaluateHandler(
2529

2630
public Task<EvaluateResponseBody> Handle(EvaluateRequestArguments request, CancellationToken cancellationToken)
2731
{
32+
// TODO: Understand why we currently handle this asynchronously and why we return a dummy result value
33+
// instead of awaiting the execution and returing a real result of some kind
34+
2835
_executionService.ExecutePSCommandAsync(
2936
new PSCommand().AddScript(request.Expression),
3037
new PowerShellExecutionOptions { WriteInputToHost = true, WriteOutputToHost = true, AddToHistory = true, InterruptCommandPrompt = true },

src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ internal class SessionDetails
2222
private const string Property_InstanceId = "instanceId";
2323

2424
/// <summary>
25-
/// Gets the PSCommand that gathers details from the
26-
/// current session.
25+
/// Runs a PowerShell command to gather details about the current session.
2726
/// </summary>
28-
/// <returns>A PSCommand used to gather session details.</returns>
27+
/// <returns>A data object containing details about the PowerShell session.</returns>
2928
public static SessionDetails GetFromPowerShell(PowerShell pwsh)
3029
{
3130
Hashtable detailsObject = pwsh

src/PowerShellEditorServices/Services/PowerShell/Utility/CancellationContext.cs

+17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@
44

55
namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility
66
{
7+
/// <summary>
8+
/// Encapsulates the scoping logic for cancellation tokens.
9+
/// As PowerShell commands nest, this class maintains a stack of cancellation scopes
10+
/// that allow each scope of logic to be cancelled at its own level.
11+
/// Implicitly handles the merging and cleanup of cancellation token sources.
12+
/// </summary>
13+
/// <example>
14+
/// The <see cref="Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility.CancellationContext"/> class
15+
/// and the <see cref="Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility.CancellationScope"/> struct
16+
/// are intended to be used with a <c>using</c> block so you can do this:
17+
/// <code>
18+
/// using (CancellationScope cancellationScope = _cancellationContext.EnterScope(_globalCancellationSource.CancellationToken, localCancellationToken))
19+
/// {
20+
/// ExecuteCommandAsync(command, cancellationScope.CancellationToken);
21+
/// }
22+
/// </code>
23+
/// </example>
724
internal class CancellationContext
825
{
926
private readonly ConcurrentStack<CancellationTokenSource> _cancellationSourceStack;

src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs

+1-6
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,7 @@ public static async Task<CommandInfo> GetCommandInfoAsync(
9898
.AddArgument(commandName)
9999
.AddParameter("ErrorAction", "Ignore");
100100

101-
CommandInfo commandInfo = null;
102-
foreach (CommandInfo result in await executionService.ExecutePSCommandAsync<CommandInfo>(command, new PowerShellExecutionOptions(), CancellationToken.None).ConfigureAwait(false))
103-
{
104-
commandInfo = result;
105-
break;
106-
}
101+
CommandInfo commandInfo = (await executionService.ExecutePSCommandAsync<CommandInfo>(command, new PowerShellExecutionOptions(), CancellationToken.None).ConfigureAwait(false)).FirstOrDefault();
107102

108103
// Only cache CmdletInfos since they're exposed in binaries they are likely to not change throughout the session.
109104
if (commandInfo?.CommandType == CommandTypes.Cmdlet)

src/PowerShellEditorServices/Services/PowerShell/Utility/ErrorRecordExtensions.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@ static ErrorRecordExtensions()
2424

2525
var errorObjectParameter = Expression.Parameter(typeof(PSObject));
2626

27+
// Generates a call like:
28+
// $errorPSObject.WriteStream = [System.Management.Automation.WriteStreamType]::Error
29+
// So that error record PSObjects will be rendered in the console properly
30+
// See https://github.com/PowerShell/PowerShell/blob/946341b2ebe6a61f081f4c9143668dc7be1f9119/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs#L2088-L2091
2731
s_setWriteStreamProperty = Expression.Lambda<Action<PSObject>>(
2832
Expression.Call(
2933
errorObjectParameter,
3034
writeStreamProperty.GetSetMethod(),
31-
Expression.Constant(errorStreamType)),
35+
Expression.Constant(errorStreamType)),
3236
errorObjectParameter)
3337
.Compile();
3438
}

src/PowerShellEditorServices/Services/PowerShell/Utility/PSCommandExtensions.cs

+8-4
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,26 @@ public static PSCommand AddDebugOutputCommand(this PSCommand psCommand)
2121

2222
public static PSCommand MergePipelineResults(this PSCommand psCommand)
2323
{
24+
// We need to do merge errors and output before rendering with an Out- cmdlet
2425
Command lastCommand = psCommand.Commands[psCommand.Commands.Count - 1];
2526
lastCommand.MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);
2627
lastCommand.MergeMyResults(PipelineResultTypes.Information, PipelineResultTypes.Output);
2728
return psCommand;
2829
}
2930

31+
/// <summary>
32+
/// Get a representation of the PSCommand, for logging purposes.
33+
/// </summary>
3034
public static string GetInvocationText(this PSCommand command)
3135
{
32-
Command lastCommand = command.Commands[0];
36+
Command currentCommand = command.Commands[0];
3337
var sb = new StringBuilder().AddCommandText(command.Commands[0]);
3438

3539
for (int i = 1; i < command.Commands.Count; i++)
3640
{
37-
sb.Append(lastCommand.IsEndOfStatement ? "; " : " | ");
38-
lastCommand = command.Commands[i];
39-
sb.AddCommandText(lastCommand);
41+
sb.Append(currentCommand.IsEndOfStatement ? "; " : " | ");
42+
currentCommand = command.Commands[i];
43+
sb.AddCommandText(currentCommand);
4044
}
4145

4246
return sb.ToString();

src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs

+15-5
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,21 @@ public static void InvokeCommand(this PowerShell pwsh, PSCommand psCommand)
7171
pwsh.InvokeAndClear();
7272
}
7373

74+
/// <summary>
75+
/// When running a remote session, waits for remote processing and output to complete.
76+
/// </summary>
7477
public static void WaitForRemoteOutputIfNeeded(this PowerShell pwsh)
7578
{
7679
if (!pwsh.Runspace.RunspaceIsRemote)
7780
{
7881
return;
7982
}
8083

84+
// These methods are required when running commands remotely.
85+
// Remote rendering from command output is done asynchronously.
86+
// So to ensure we wait for output to be rendered,
87+
// we need these methods to wait for rendering.
88+
// PowerShell does this in its own implementation: https://github.com/PowerShell/PowerShell/blob/883ca98dd74ea13b3d8c0dd62d301963a40483d6/src/System.Management.Automation/engine/debugger/debugger.cs#L4628-L4652
8189
s_waitForServicingComplete(pwsh);
8290
s_suspendIncomingData(pwsh);
8391
}
@@ -155,10 +163,10 @@ public static void LoadProfiles(this PowerShell pwsh, ProfilePathInfo profilePat
155163
{
156164
var profileVariable = new PSObject();
157165

158-
pwsh.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.AllUsersAllHosts), profilePaths.AllUsersAllHosts);
159-
pwsh.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.AllUsersCurrentHost), profilePaths.AllUsersCurrentHost);
160-
pwsh.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.CurrentUserAllHosts), profilePaths.CurrentUserAllHosts);
161-
pwsh.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.CurrentUserCurrentHost), profilePaths.CurrentUserCurrentHost);
166+
pwsh.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.AllUsersAllHosts), profilePaths.AllUsersAllHosts)
167+
.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.AllUsersCurrentHost), profilePaths.AllUsersCurrentHost)
168+
.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.CurrentUserAllHosts), profilePaths.CurrentUserAllHosts)
169+
.AddProfileMemberAndLoadIfExists(profileVariable, nameof(profilePaths.CurrentUserCurrentHost), profilePaths.CurrentUserCurrentHost);
162170

163171
pwsh.Runspace.SessionStateProxy.SetVariable("PROFILE", profileVariable);
164172
}
@@ -189,7 +197,7 @@ public static string GetErrorString(this PowerShell pwsh)
189197
return sb.ToString();
190198
}
191199

192-
private static void AddProfileMemberAndLoadIfExists(this PowerShell pwsh, PSObject profileVariable, string profileName, string profilePath)
200+
private static PowerShell AddProfileMemberAndLoadIfExists(this PowerShell pwsh, PSObject profileVariable, string profileName, string profilePath)
193201
{
194202
profileVariable.Members.Add(new PSNoteProperty(profileName, profilePath));
195203

@@ -201,6 +209,8 @@ private static void AddProfileMemberAndLoadIfExists(this PowerShell pwsh, PSObje
201209

202210
pwsh.InvokeCommand(psCommand);
203211
}
212+
213+
return pwsh;
204214
}
205215

206216
private static StringBuilder AddErrorString(this StringBuilder sb, ErrorRecord error, int errorIndex)

src/PowerShellEditorServices/Services/PowerShell/Utility/RunspaceExtensions.cs

+10-6
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,9 @@ internal static class RunspaceExtensions
1818
static RunspaceExtensions()
1919
{
2020
// PowerShell ApartmentState APIs aren't available in PSStandard, so we need to use reflection.
21-
if (!VersionUtils.IsNetCore || VersionUtils.IsPS7OrGreater)
22-
{
23-
MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod();
24-
Delegate setter = Delegate.CreateDelegate(typeof(Action<Runspace, ApartmentState>), firstArgument: null, method: setterInfo);
25-
s_runspaceApartmentStateSetter = (Action<Runspace, ApartmentState>)setter;
26-
}
21+
MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod();
22+
Delegate setter = Delegate.CreateDelegate(typeof(Action<Runspace, ApartmentState>), firstArgument: null, method: setterInfo);
23+
s_runspaceApartmentStateSetter = (Action<Runspace, ApartmentState>)setter;
2724

2825
MethodInfo getRemotePromptMethod = typeof(HostUtilities).GetMethod("GetRemotePrompt", BindingFlags.NonPublic | BindingFlags.Static);
2926
ParameterExpression runspaceParam = Expression.Parameter(typeof(Runspace));
@@ -45,6 +42,13 @@ public static void SetApartmentStateToSta(this Runspace runspace)
4542
s_runspaceApartmentStateSetter?.Invoke(runspace, ApartmentState.STA);
4643
}
4744

45+
/// <summary>
46+
/// Augment a given prompt string with a remote decoration.
47+
/// This is an internal method on <c>Runspace</c> in PowerShell that we reuse via reflection.
48+
/// </summary>
49+
/// <param name="runspace">The runspace the prompt is for.</param>
50+
/// <param name="basePrompt">The base prompt to decorate.</param>
51+
/// <returns>A prompt string decorated with remote connection details.</returns>
4852
public static string GetRemotePrompt(this Runspace runspace, string basePrompt)
4953
{
5054
return s_getRemotePromptFunc(runspace, basePrompt);

0 commit comments

Comments
 (0)