1- // Copyright (c) Microsoft Corporation.
1+ // Copyright (c) Microsoft Corporation.
22// Licensed under the MIT License.
33
44using 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
0 commit comments