Skip to content

Commit e438e76

Browse files
committed
Expand variables on pipeline thread
So that expansions of embedded PowerShell code succeed.
1 parent 243f2ea commit e438e76

File tree

5 files changed

+137
-132
lines changed

5 files changed

+137
-132
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

+17-8
Original file line numberDiff line numberDiff line change
@@ -241,11 +241,12 @@ public async Task<CommandBreakpointDetails[]> SetCommandBreakpointsAsync(
241241
/// that is identified by the given referenced ID.
242242
/// </summary>
243243
/// <param name="variableReferenceId"></param>
244+
/// <param name="cancellationToken"></param>
244245
/// <returns>An array of VariableDetails instances which describe the requested variables.</returns>
245-
public VariableDetailsBase[] GetVariables(int variableReferenceId)
246+
public async Task<VariableDetailsBase[]> GetVariables(int variableReferenceId, CancellationToken cancellationToken)
246247
{
247248
VariableDetailsBase[] childVariables;
248-
debugInfoHandle.Wait();
249+
await debugInfoHandle.WaitAsync(cancellationToken).ConfigureAwait(false);
249250
try
250251
{
251252
if ((variableReferenceId < 0) || (variableReferenceId >= variables.Count))
@@ -257,7 +258,12 @@ public VariableDetailsBase[] GetVariables(int variableReferenceId)
257258
VariableDetailsBase parentVariable = variables[variableReferenceId];
258259
if (parentVariable.IsExpandable)
259260
{
260-
childVariables = parentVariable.GetChildren(_logger);
261+
// We execute this on the pipeline thread so the expansion of child variables works.
262+
childVariables = await _executionService.ExecuteDelegateAsync(
263+
$"Getting children of variable ${parentVariable.Name}",
264+
new ExecutionOptions { Priority = ExecutionPriority.Next },
265+
(_, _) => parentVariable.GetChildren(_logger), cancellationToken).ConfigureAwait(false);
266+
261267
foreach (VariableDetailsBase child in childVariables)
262268
{
263269
// Only add child if it hasn't already been added.
@@ -287,8 +293,9 @@ public VariableDetailsBase[] GetVariables(int variableReferenceId)
287293
/// walk the cached variable data for the specified stack frame.
288294
/// </summary>
289295
/// <param name="variableExpression">The variable expression string to evaluate.</param>
296+
/// <param name="cancellationToken"></param>
290297
/// <returns>A VariableDetailsBase object containing the result.</returns>
291-
public VariableDetailsBase GetVariableFromExpression(string variableExpression)
298+
public async Task<VariableDetailsBase> GetVariableFromExpression(string variableExpression, CancellationToken cancellationToken)
292299
{
293300
// NOTE: From a watch we will get passed expressions that are not naked variables references.
294301
// Probably the right way to do this would be to examine the AST of the expr before calling
@@ -302,7 +309,7 @@ public VariableDetailsBase GetVariableFromExpression(string variableExpression)
302309
IEnumerable<VariableDetailsBase> variableList;
303310

304311
// Ensure debug info isn't currently being built.
305-
debugInfoHandle.Wait();
312+
await debugInfoHandle.WaitAsync(cancellationToken).ConfigureAwait(false);
306313
try
307314
{
308315
variableList = variables;
@@ -331,7 +338,7 @@ public VariableDetailsBase GetVariableFromExpression(string variableExpression)
331338
if (resolvedVariable?.IsExpandable == true)
332339
{
333340
// Continue by searching in this variable's children.
334-
variableList = GetVariables(resolvedVariable.Id);
341+
variableList = await GetVariables(resolvedVariable.Id, cancellationToken).ConfigureAwait(false);
335342
}
336343
}
337344

@@ -491,18 +498,20 @@ public async Task<string> SetVariableAsync(int variableContainerReferenceId, str
491498
/// <param name="writeResultAsOutput">
492499
/// If true, writes the expression result as host output rather than returning the results.
493500
/// In this case, the return value of this function will be null.</param>
501+
/// <param name="cancellationToken"></param>
494502
/// <returns>A VariableDetails object containing the result.</returns>
495503
public async Task<VariableDetails> EvaluateExpressionAsync(
496504
string expressionString,
497-
bool writeResultAsOutput)
505+
bool writeResultAsOutput,
506+
CancellationToken cancellationToken)
498507
{
499508
PSCommand command = new PSCommand().AddScript(expressionString);
500509
IReadOnlyList<PSObject> results;
501510
try
502511
{
503512
results = await _executionService.ExecutePSCommandAsync<PSObject>(
504513
command,
505-
CancellationToken.None,
514+
cancellationToken,
506515
new PowerShellExecutionOptions { WriteOutputToHost = writeResultAsOutput, ThrowOnError = !writeResultAsOutput }).ConfigureAwait(false);
507516
}
508517
catch (Exception e)

src/PowerShellEditorServices/Services/DebugAdapter/Debugging/VariableDetails.cs

+64-68
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ private static string InsertDimensionSize(string value, int dimensionSize)
229229
return value + ": " + dimensionSize;
230230
}
231231

232-
private VariableDetails[] GetChildren(object obj, ILogger logger)
232+
private static VariableDetails[] GetChildren(object obj, ILogger logger)
233233
{
234234
List<VariableDetails> childVariables = new();
235235

@@ -238,86 +238,82 @@ private VariableDetails[] GetChildren(object obj, ILogger logger)
238238
return childVariables.ToArray();
239239
}
240240

241-
try
242-
{
243-
PSObject psObject = obj as PSObject;
241+
// NOTE: Variable expansion now takes place on the pipeline thread as an async delegate,
242+
// so expansion of children that cause PowerShell script code to execute should
243+
// generally work. However, we might need more error handling.
244+
PSObject psObject = obj as PSObject;
244245

245-
if ((psObject != null) &&
246-
(psObject.TypeNames[0] == typeof(PSCustomObject).ToString()))
246+
if ((psObject != null) &&
247+
(psObject.TypeNames[0] == typeof(PSCustomObject).ToString()))
248+
{
249+
// PowerShell PSCustomObject's properties are completely defined by the ETS type system.
250+
logger.LogDebug("PSObject was a PSCustomObject");
251+
childVariables.AddRange(
252+
psObject
253+
.Properties
254+
.Select(p => new VariableDetails(p)));
255+
}
256+
else
257+
{
258+
// If a PSObject other than a PSCustomObject, unwrap it.
259+
if (psObject != null)
247260
{
248-
// PowerShell PSCustomObject's properties are completely defined by the ETS type system.
261+
// First add the PSObject's ETS properties
262+
logger.LogDebug("PSObject was something else, first getting ETS properties");
249263
childVariables.AddRange(
250264
psObject
251265
.Properties
266+
// Here we check the object's MemberType against the `Properties`
267+
// bit-mask to determine if this is a property. Hence the selection
268+
// will only include properties.
269+
.Where(p => (PSMemberTypes.Properties & p.MemberType) is not 0)
252270
.Select(p => new VariableDetails(p)));
271+
272+
obj = psObject.BaseObject;
253273
}
254-
else
274+
275+
// We're in the realm of regular, unwrapped .NET objects
276+
if (obj is IDictionary dictionary)
255277
{
256-
// If a PSObject other than a PSCustomObject, unwrap it.
257-
if (psObject != null)
278+
logger.LogDebug("PSObject was an IDictionary");
279+
// Buckle up kids, this is a bit weird. We could not use the LINQ
280+
// operator OfType<DictionaryEntry>. Even though R# will squiggle the
281+
// "foreach" keyword below and offer to convert to a LINQ-expression - DON'T DO IT!
282+
// The reason is that LINQ extension methods work with objects of type
283+
// IEnumerable. Objects of type Dictionary<,>, respond to iteration via
284+
// IEnumerable by returning KeyValuePair<,> objects. Unfortunately non-generic
285+
// dictionaries like HashTable return DictionaryEntry objects.
286+
// It turns out that iteration via C#'s foreach loop, operates on the variable's
287+
// type which in this case is IDictionary. IDictionary was designed to always
288+
// return DictionaryEntry objects upon iteration and the Dictionary<,> implementation
289+
// honors that when the object is reinterpreted as an IDictionary object.
290+
// FYI, a test case for this is to open $PSBoundParameters when debugging a
291+
// function that defines parameters and has been passed parameters.
292+
// If you open the $PSBoundParameters variable node in this scenario and see nothing,
293+
// this code is broken.
294+
foreach (DictionaryEntry entry in dictionary)
258295
{
259-
// First add the PSObject's ETS properties
260-
childVariables.AddRange(
261-
psObject
262-
.Properties
263-
// Here we check the object's MemberType against the `Properties`
264-
// bit-mask to determine if this is a property. Hence the selection
265-
// will only include properties.
266-
.Where(p => (PSMemberTypes.Properties & p.MemberType) is not 0)
267-
.Select(p => new VariableDetails(p)));
268-
269-
obj = psObject.BaseObject;
296+
childVariables.Add(
297+
new VariableDetails(
298+
"[" + entry.Key + "]",
299+
entry));
270300
}
271-
272-
// We're in the realm of regular, unwrapped .NET objects
273-
if (obj is IDictionary dictionary)
274-
{
275-
// Buckle up kids, this is a bit weird. We could not use the LINQ
276-
// operator OfType<DictionaryEntry>. Even though R# will squiggle the
277-
// "foreach" keyword below and offer to convert to a LINQ-expression - DON'T DO IT!
278-
// The reason is that LINQ extension methods work with objects of type
279-
// IEnumerable. Objects of type Dictionary<,>, respond to iteration via
280-
// IEnumerable by returning KeyValuePair<,> objects. Unfortunately non-generic
281-
// dictionaries like HashTable return DictionaryEntry objects.
282-
// It turns out that iteration via C#'s foreach loop, operates on the variable's
283-
// type which in this case is IDictionary. IDictionary was designed to always
284-
// return DictionaryEntry objects upon iteration and the Dictionary<,> implementation
285-
// honors that when the object is reinterpreted as an IDictionary object.
286-
// FYI, a test case for this is to open $PSBoundParameters when debugging a
287-
// function that defines parameters and has been passed parameters.
288-
// If you open the $PSBoundParameters variable node in this scenario and see nothing,
289-
// this code is broken.
290-
foreach (DictionaryEntry entry in dictionary)
291-
{
292-
childVariables.Add(
293-
new VariableDetails(
294-
"[" + entry.Key + "]",
295-
entry));
296-
}
297-
}
298-
else if (obj is IEnumerable enumerable and not string)
301+
}
302+
else if (obj is IEnumerable enumerable and not string)
303+
{
304+
logger.LogDebug("PSObject was an IEnumerable");
305+
int i = 0;
306+
foreach (object item in enumerable)
299307
{
300-
int i = 0;
301-
foreach (object item in enumerable)
302-
{
303-
childVariables.Add(
304-
new VariableDetails(
305-
"[" + i++ + "]",
306-
item));
307-
}
308+
childVariables.Add(
309+
new VariableDetails(
310+
"[" + i++ + "]",
311+
item));
308312
}
309-
310-
AddDotNetProperties(obj, childVariables);
311313
}
312-
}
313-
catch (GetValueInvocationException ex)
314-
{
315-
// This exception occurs when accessing the value of a
316-
// variable causes a script to be executed. Right now
317-
// we aren't loading children on the pipeline thread so
318-
// this causes an exception to be raised. In this case,
319-
// just return an empty list of children.
320-
logger.LogWarning($"Failed to get properties of variable {Name}, value invocation was attempted: {ex.Message}");
314+
315+
logger.LogDebug("Adding .NET properties to PSObject");
316+
AddDotNetProperties(obj, childVariables);
321317
}
322318

323319
return childVariables.ToArray();

src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation.
1+
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

44
using System;
@@ -49,7 +49,7 @@ public async Task<EvaluateResponseBody> Handle(EvaluateRequestArguments request,
4949
{
5050
await _executionService.ExecutePSCommandAsync(
5151
new PSCommand().AddScript(request.Expression),
52-
CancellationToken.None,
52+
cancellationToken,
5353
new PowerShellExecutionOptions { WriteOutputToHost = true, ThrowOnError = false, AddToHistory = true }).HandleErrorsAsync(_logger).ConfigureAwait(false);
5454
}
5555
else
@@ -61,12 +61,13 @@ await _executionService.ExecutePSCommandAsync(
6161
if (_debugContext.IsStopped)
6262
{
6363
// First check to see if the watch expression refers to a naked variable reference.
64-
result = _debugService.GetVariableFromExpression(request.Expression);
64+
result = await _debugService.GetVariableFromExpression(request.Expression, cancellationToken).ConfigureAwait(false);
6565

6666
// If the expression is not a naked variable reference, then evaluate the expression.
6767
result ??= await _debugService.EvaluateExpressionAsync(
6868
request.Expression,
69-
isFromRepl).ConfigureAwait(false);
69+
isFromRepl,
70+
cancellationToken).ConfigureAwait(false);
7071
}
7172

7273
if (result != null)

src/PowerShellEditorServices/Services/DebugAdapter/Handlers/VariablesHandler.cs

+3-5
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@ internal class VariablesHandler : IVariablesHandler
1818

1919
public VariablesHandler(DebugService debugService) => _debugService = debugService;
2020

21-
public Task<VariablesResponse> Handle(VariablesArguments request, CancellationToken cancellationToken)
21+
public async Task<VariablesResponse> Handle(VariablesArguments request, CancellationToken cancellationToken)
2222
{
23-
VariableDetailsBase[] variables =
24-
_debugService.GetVariables(
25-
(int)request.VariablesReference);
23+
VariableDetailsBase[] variables = await _debugService.GetVariables((int)request.VariablesReference, cancellationToken).ConfigureAwait(false);
2624

2725
VariablesResponse variablesResponse = null;
2826

@@ -41,7 +39,7 @@ public Task<VariablesResponse> Handle(VariablesArguments request, CancellationTo
4139
// TODO: This shouldn't be so broad
4240
}
4341

44-
return Task.FromResult(variablesResponse);
42+
return variablesResponse;
4543
}
4644
}
4745
}

0 commit comments

Comments
 (0)