Skip to content

Commit c0ff2ce

Browse files
Merge pull request #1975 from PowerShell/andschwa/debugger
Fix several bugs in the debugger
2 parents 3845d5f + f6b1ffa commit c0ff2ce

File tree

11 files changed

+350
-213
lines changed

11 files changed

+350
-213
lines changed

src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
#if DEBUG
1616
using System.Diagnostics;
1717
using System.Threading;
18-
1918
using Debugger = System.Diagnostics.Debugger;
2019
#endif
2120

@@ -197,9 +196,11 @@ protected override void BeginProcessing()
197196
#if DEBUG
198197
if (WaitForDebugger)
199198
{
199+
// NOTE: Ignore the suggestion to use Environment.ProcessId as it doesn't work for
200+
// .NET 4.6.2 (for Windows PowerShell), and this won't be caught in CI.
201+
Console.WriteLine($"Waiting for debugger to attach, PID: {Process.GetCurrentProcess().Id}");
200202
while (!Debugger.IsAttached)
201203
{
202-
Console.WriteLine($"PID: {Process.GetCurrentProcess().Id}");
203204
Thread.Sleep(1000);
204205
}
205206
}

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

+94-93
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;
@@ -68,7 +68,7 @@ public VariableDetails(PSObject psVariableObject)
6868
/// The PSPropertyInfo instance from which variable details will be obtained.
6969
/// </param>
7070
public VariableDetails(PSPropertyInfo psProperty)
71-
: this(psProperty.Name, psProperty.Value)
71+
: this(psProperty.Name, SafeGetValue(psProperty))
7272
{
7373
}
7474

@@ -98,16 +98,11 @@ public VariableDetails(string name, object value)
9898
/// If this variable instance is expandable, this method returns the
9999
/// details of its children. Otherwise it returns an empty array.
100100
/// </summary>
101-
/// <returns></returns>
102101
public override VariableDetailsBase[] GetChildren(ILogger logger)
103102
{
104103
if (IsExpandable)
105104
{
106-
if (cachedChildren == null)
107-
{
108-
cachedChildren = GetChildren(ValueObject, logger);
109-
}
110-
105+
cachedChildren ??= GetChildren(ValueObject, logger);
111106
return cachedChildren;
112107
}
113108

@@ -118,6 +113,20 @@ public override VariableDetailsBase[] GetChildren(ILogger logger)
118113

119114
#region Private Methods
120115

116+
private static object SafeGetValue(PSPropertyInfo psProperty)
117+
{
118+
try
119+
{
120+
return psProperty.Value;
121+
}
122+
catch (GetValueInvocationException ex)
123+
{
124+
// Sometimes we can't get the value, like ExitCode, for reasons beyond our control,
125+
// so just return the message from the exception that arises.
126+
return new UnableToRetrievePropertyMessage { Name = psProperty.Name, Message = ex.Message };
127+
}
128+
}
129+
121130
private static bool GetIsExpandable(object valueObject)
122131
{
123132
if (valueObject == null)
@@ -131,9 +140,7 @@ private static bool GetIsExpandable(object valueObject)
131140
valueObject = psobject.BaseObject;
132141
}
133142

134-
Type valueType =
135-
valueObject?.GetType();
136-
143+
Type valueType = valueObject?.GetType();
137144
TypeInfo valueTypeInfo = valueType.GetTypeInfo();
138145

139146
return
@@ -236,7 +243,7 @@ private static string InsertDimensionSize(string value, int dimensionSize)
236243
return value + ": " + dimensionSize;
237244
}
238245

239-
private VariableDetails[] GetChildren(object obj, ILogger logger)
246+
private static VariableDetails[] GetChildren(object obj, ILogger logger)
240247
{
241248
List<VariableDetails> childVariables = new();
242249

@@ -245,86 +252,82 @@ private VariableDetails[] GetChildren(object obj, ILogger logger)
245252
return childVariables.ToArray();
246253
}
247254

248-
try
249-
{
250-
PSObject psObject = obj as PSObject;
255+
// NOTE: Variable expansion now takes place on the pipeline thread as an async delegate,
256+
// so expansion of children that cause PowerShell script code to execute should
257+
// generally work. However, we might need more error handling.
258+
PSObject psObject = obj as PSObject;
251259

252-
if ((psObject != null) &&
253-
(psObject.TypeNames[0] == typeof(PSCustomObject).ToString()))
260+
if ((psObject != null) &&
261+
(psObject.TypeNames[0] == typeof(PSCustomObject).ToString()))
262+
{
263+
// PowerShell PSCustomObject's properties are completely defined by the ETS type system.
264+
logger.LogDebug("PSObject was a PSCustomObject");
265+
childVariables.AddRange(
266+
psObject
267+
.Properties
268+
.Select(p => new VariableDetails(p)));
269+
}
270+
else
271+
{
272+
// If a PSObject other than a PSCustomObject, unwrap it.
273+
if (psObject != null)
254274
{
255-
// PowerShell PSCustomObject's properties are completely defined by the ETS type system.
275+
// First add the PSObject's ETS properties
276+
logger.LogDebug("PSObject was something else, first getting ETS properties");
256277
childVariables.AddRange(
257278
psObject
258279
.Properties
280+
// Here we check the object's MemberType against the `Properties`
281+
// bit-mask to determine if this is a property. Hence the selection
282+
// will only include properties.
283+
.Where(p => (PSMemberTypes.Properties & p.MemberType) is not 0)
259284
.Select(p => new VariableDetails(p)));
285+
286+
obj = psObject.BaseObject;
260287
}
261-
else
262-
{
263-
// If a PSObject other than a PSCustomObject, unwrap it.
264-
if (psObject != null)
265-
{
266-
// First add the PSObject's ETS properties
267-
childVariables.AddRange(
268-
psObject
269-
.Properties
270-
// Here we check the object's MemberType against the `Properties`
271-
// bit-mask to determine if this is a property. Hence the selection
272-
// will only include properties.
273-
.Where(p => (PSMemberTypes.Properties & p.MemberType) is not 0)
274-
.Select(p => new VariableDetails(p)));
275-
276-
obj = psObject.BaseObject;
277-
}
278288

279-
// We're in the realm of regular, unwrapped .NET objects
280-
if (obj is IDictionary dictionary)
289+
// We're in the realm of regular, unwrapped .NET objects
290+
if (obj is IDictionary dictionary)
291+
{
292+
logger.LogDebug("PSObject was an IDictionary");
293+
// Buckle up kids, this is a bit weird. We could not use the LINQ
294+
// operator OfType<DictionaryEntry>. Even though R# will squiggle the
295+
// "foreach" keyword below and offer to convert to a LINQ-expression - DON'T DO IT!
296+
// The reason is that LINQ extension methods work with objects of type
297+
// IEnumerable. Objects of type Dictionary<,>, respond to iteration via
298+
// IEnumerable by returning KeyValuePair<,> objects. Unfortunately non-generic
299+
// dictionaries like HashTable return DictionaryEntry objects.
300+
// It turns out that iteration via C#'s foreach loop, operates on the variable's
301+
// type which in this case is IDictionary. IDictionary was designed to always
302+
// return DictionaryEntry objects upon iteration and the Dictionary<,> implementation
303+
// honors that when the object is reinterpreted as an IDictionary object.
304+
// FYI, a test case for this is to open $PSBoundParameters when debugging a
305+
// function that defines parameters and has been passed parameters.
306+
// If you open the $PSBoundParameters variable node in this scenario and see nothing,
307+
// this code is broken.
308+
foreach (DictionaryEntry entry in dictionary)
281309
{
282-
// Buckle up kids, this is a bit weird. We could not use the LINQ
283-
// operator OfType<DictionaryEntry>. Even though R# will squiggle the
284-
// "foreach" keyword below and offer to convert to a LINQ-expression - DON'T DO IT!
285-
// The reason is that LINQ extension methods work with objects of type
286-
// IEnumerable. Objects of type Dictionary<,>, respond to iteration via
287-
// IEnumerable by returning KeyValuePair<,> objects. Unfortunately non-generic
288-
// dictionaries like HashTable return DictionaryEntry objects.
289-
// It turns out that iteration via C#'s foreach loop, operates on the variable's
290-
// type which in this case is IDictionary. IDictionary was designed to always
291-
// return DictionaryEntry objects upon iteration and the Dictionary<,> implementation
292-
// honors that when the object is reinterpreted as an IDictionary object.
293-
// FYI, a test case for this is to open $PSBoundParameters when debugging a
294-
// function that defines parameters and has been passed parameters.
295-
// If you open the $PSBoundParameters variable node in this scenario and see nothing,
296-
// this code is broken.
297-
foreach (DictionaryEntry entry in dictionary)
298-
{
299-
childVariables.Add(
300-
new VariableDetails(
301-
"[" + entry.Key + "]",
302-
entry));
303-
}
310+
childVariables.Add(
311+
new VariableDetails(
312+
"[" + entry.Key + "]",
313+
entry));
304314
}
305-
else if (obj is IEnumerable enumerable and not string)
315+
}
316+
else if (obj is IEnumerable enumerable and not string)
317+
{
318+
logger.LogDebug("PSObject was an IEnumerable");
319+
int i = 0;
320+
foreach (object item in enumerable)
306321
{
307-
int i = 0;
308-
foreach (object item in enumerable)
309-
{
310-
childVariables.Add(
311-
new VariableDetails(
312-
"[" + i++ + "]",
313-
item));
314-
}
322+
childVariables.Add(
323+
new VariableDetails(
324+
"[" + i++ + "]",
325+
item));
315326
}
316-
317-
AddDotNetProperties(obj, childVariables);
318327
}
319-
}
320-
catch (GetValueInvocationException ex)
321-
{
322-
// This exception occurs when accessing the value of a
323-
// variable causes a script to be executed. Right now
324-
// we aren't loading children on the pipeline thread so
325-
// this causes an exception to be raised. In this case,
326-
// just return an empty list of children.
327-
logger.LogWarning($"Failed to get properties of variable {Name}, value invocation was attempted: {ex.Message}");
328+
329+
logger.LogDebug("Adding .NET properties to PSObject");
330+
AddDotNetProperties(obj, childVariables);
328331
}
329332

330333
return childVariables.ToArray();
@@ -342,9 +345,8 @@ protected static void AddDotNetProperties(object obj, List<VariableDetails> chil
342345
return;
343346
}
344347

345-
PropertyInfo[] properties = objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
346-
347-
foreach (PropertyInfo property in properties)
348+
// Search all the public instance properties and add those missing.
349+
foreach (PropertyInfo property in objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
348350
{
349351
// Don't display indexer properties, it causes an exception anyway.
350352
if (property.GetIndexParameters().Length > 0)
@@ -354,10 +356,11 @@ protected static void AddDotNetProperties(object obj, List<VariableDetails> chil
354356

355357
try
356358
{
357-
childVariables.Add(
358-
new VariableDetails(
359-
property.Name,
360-
property.GetValue(obj)));
359+
// Only add unique properties because we may have already added some.
360+
if (!childVariables.Exists(p => p.Name == property.Name))
361+
{
362+
childVariables.Add(new VariableDetails(property.Name, property.GetValue(obj)));
363+
}
361364
}
362365
catch (Exception ex)
363366
{
@@ -371,21 +374,19 @@ protected static void AddDotNetProperties(object obj, List<VariableDetails> chil
371374
childVariables.Add(
372375
new VariableDetails(
373376
property.Name,
374-
new UnableToRetrievePropertyMessage(
375-
"Error retrieving property - " + ex.GetType().Name)));
377+
new UnableToRetrievePropertyMessage { Name = property.Name, Message = ex.Message }));
376378
}
377379
}
378380
}
379381

380382
#endregion
381383

382-
private struct UnableToRetrievePropertyMessage
384+
private record UnableToRetrievePropertyMessage
383385
{
384-
public UnableToRetrievePropertyMessage(string message) => Message = message;
385-
386-
public string Message { get; }
386+
public string Name { get; init; }
387+
public string Message { get; init; }
387388

388-
public override string ToString() => "<" + Message + ">";
389+
public override string ToString() => $"Error retrieving property '${Name}': ${Message}";
389390
}
390391
}
391392

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

-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ internal abstract class VariableDetailsBase
5151
/// If this variable instance is expandable, this method returns the
5252
/// details of its children. Otherwise it returns an empty array.
5353
/// </summary>
54-
/// <returns></returns>
5554
public abstract VariableDetailsBase[] GetChildren(ILogger logger);
5655
}
5756
}

0 commit comments

Comments
 (0)