1717using Microsoft . PowerShell . EditorServices . Services . PowerShell . Execution ;
1818using Microsoft . PowerShell . EditorServices . Services . PowerShell . Host ;
1919using Microsoft . PowerShell . EditorServices . Services . PowerShell . Debugging ;
20+ using System . Collections ;
2021
2122namespace Microsoft . PowerShell . EditorServices . Services
2223{
@@ -45,6 +46,7 @@ internal class DebugService
4546 private List < VariableDetailsBase > variables ;
4647 private VariableContainerDetails globalScopeVariables ;
4748 private VariableContainerDetails scriptScopeVariables ;
49+ private VariableContainerDetails localScopeVariables ;
4850 private StackFrameDetails [ ] stackFrameDetails ;
4951 private readonly PropertyInfo invocationTypeScriptPositionProperty ;
5052
@@ -445,11 +447,6 @@ public async Task<string> SetVariableAsync(int variableContainerReferenceId, str
445447 for ( int i = 0 ; i < stackFrames . Length ; i ++ )
446448 {
447449 var stackFrame = stackFrames [ i ] ;
448- if ( stackFrame . LocalVariables . ContainsVariable ( variable . Id ) )
449- {
450- scope = i . ToString ( ) ;
451- break ;
452- }
453450 }
454451 }
455452
@@ -626,13 +623,12 @@ internal async Task<StackFrameDetails[]> GetStackFramesAsync(CancellationToken c
626623 public VariableScope [ ] GetVariableScopes ( int stackFrameId )
627624 {
628625 var stackFrames = this . GetStackFrames ( ) ;
629- int localStackFrameVariableId = stackFrames [ stackFrameId ] . LocalVariables . Id ;
630626 int autoVariablesId = stackFrames [ stackFrameId ] . AutoVariables . Id ;
631627
632628 return new VariableScope [ ]
633629 {
634630 new VariableScope ( autoVariablesId , VariableContainerDetails . AutoVariablesName ) ,
635- new VariableScope ( localStackFrameVariableId , VariableContainerDetails . LocalScopeName ) ,
631+ new VariableScope ( this . localScopeVariables . Id , VariableContainerDetails . LocalScopeName ) ,
636632 new VariableScope ( this . scriptScopeVariables . Id , VariableContainerDetails . ScriptScopeName ) ,
637633 new VariableScope ( this . globalScopeVariables . Id , VariableContainerDetails . GlobalScopeName ) ,
638634 } ;
@@ -655,30 +651,27 @@ private async Task FetchStackFramesAndVariablesAsync(string scriptNameOverride)
655651 new VariableDetails ( "Dummy" , null )
656652 } ;
657653
658- // Must retrieve global/script variales before stack frame variables
659- // as we check stack frame variables against globals.
660- await FetchGlobalAndScriptVariablesAsync ( ) . ConfigureAwait ( false ) ;
654+
655+ // Must retrieve in order of broadest to narrowest scope for efficient deduplication: global, script, local
656+ this . globalScopeVariables =
657+ await FetchVariableContainerAsync ( VariableContainerDetails . GlobalScopeName ) . ConfigureAwait ( false ) ;
658+
659+ this . scriptScopeVariables =
660+ await FetchVariableContainerAsync ( VariableContainerDetails . ScriptScopeName ) . ConfigureAwait ( false ) ;
661+
662+ this . localScopeVariables =
663+ await FetchVariableContainerAsync ( VariableContainerDetails . LocalScopeName ) . ConfigureAwait ( false ) ;
664+
661665 await FetchStackFramesAsync ( scriptNameOverride ) . ConfigureAwait ( false ) ;
666+
662667 }
663668 finally
664669 {
665670 this . debugInfoHandle . Release ( ) ;
666671 }
667672 }
668673
669- private async Task FetchGlobalAndScriptVariablesAsync ( )
670- {
671- // Retrieve globals first as script variable retrieval needs to search globals.
672- this . globalScopeVariables =
673- await FetchVariableContainerAsync ( VariableContainerDetails . GlobalScopeName , null ) . ConfigureAwait ( false ) ;
674-
675- this . scriptScopeVariables =
676- await FetchVariableContainerAsync ( VariableContainerDetails . ScriptScopeName , null ) . ConfigureAwait ( false ) ;
677- }
678-
679- private async Task < VariableContainerDetails > FetchVariableContainerAsync (
680- string scope ,
681- VariableContainerDetails autoVariables )
674+ private async Task < VariableContainerDetails > FetchVariableContainerAsync ( string scope )
682675 {
683676 PSCommand psCommand = new PSCommand ( )
684677 . AddCommand ( "Get-Variable" )
@@ -704,11 +697,6 @@ private async Task<VariableContainerDetails> FetchVariableContainerAsync(
704697 var variableDetails = new VariableDetails ( psVariableObject ) { Id = this . nextVariableId ++ } ;
705698 this . variables . Add ( variableDetails ) ;
706699 scopeVariableContainer . Children . Add ( variableDetails . Name , variableDetails ) ;
707-
708- if ( ( autoVariables != null ) && AddToAutoVariables ( psVariableObject , scope ) )
709- {
710- autoVariables . Children . Add ( variableDetails . Name , variableDetails ) ;
711- }
712700 }
713701 }
714702
@@ -792,55 +780,90 @@ private bool AddToAutoVariables(PSObject psvariable, string scope)
792780 private async Task FetchStackFramesAsync ( string scriptNameOverride )
793781 {
794782 PSCommand psCommand = new PSCommand ( ) ;
783+ // The serialization depth to retrieve variables from remote runspaces.
784+ const int serializationDepth = 3 ;
795785
796786 // This glorious hack ensures that Get-PSCallStack returns a list of CallStackFrame
797787 // objects (or "deserialized" CallStackFrames) when attached to a runspace in another
798788 // process. Without the intermediate variable Get-PSCallStack inexplicably returns
799789 // an array of strings containing the formatted output of the CallStackFrame list.
800- var callStackVarName = $ "$global:{ PsesGlobalVariableNamePrefix } CallStack";
801- psCommand . AddScript ( $ "{ callStackVarName } = Get-PSCallStack; { callStackVarName } ") ;
790+ string callStackVarName = $ "$global:{ PsesGlobalVariableNamePrefix } CallStack";
791+
792+ string getPSCallStack = $ "Get-PSCallStack | ForEach-Object {{ [void]{ callStackVarName } .add(@($PSItem,$PSItem.GetFrameVariables())) }}";
793+
794+ // If we're attached to a remote runspace, we need to serialize the callstack prior to transport
795+ // because the default depth is too shallow
796+ bool isOnRemoteMachine = _psesHost . CurrentRunspace . IsOnRemoteMachine ;
797+ string returnSerializedIfOnRemoteMachine = isOnRemoteMachine
798+ ? $ "[Management.Automation.PSSerializer]::Serialize({ callStackVarName } , { serializationDepth } )"
799+ : callStackVarName ;
802800
803- var results = await _executionService . ExecutePSCommandAsync < PSObject > ( psCommand , CancellationToken . None ) . ConfigureAwait ( false ) ;
801+ // We have to deal with a shallow serialization depth with ExecutePSCommandAsync as well, hence the serializer to get full var information
802+ psCommand . AddScript ( $ "[Collections.ArrayList]{ callStackVarName } = @(); { getPSCallStack } ; { returnSerializedIfOnRemoteMachine } ") ;
804803
805- var callStackFrames = results . ToArray ( ) ;
806804
807- this . stackFrameDetails = new StackFrameDetails [ callStackFrames . Length ] ;
805+ // PSObject is used here instead of the specific type because we get deserialized objects from remote sessions and want a common interface
806+ IReadOnlyList < PSObject > results = await _executionService . ExecutePSCommandAsync < PSObject > ( psCommand , CancellationToken . None ) . ConfigureAwait ( false ) ;
808807
809- for ( int i = 0 ; i < callStackFrames . Length ; i ++ )
808+ IEnumerable callStack = isOnRemoteMachine
809+ ? ( PSSerializer . Deserialize ( results [ 0 ] . BaseObject as string ) as PSObject ) . BaseObject as IList
810+ : results ;
811+
812+ List < StackFrameDetails > stackFrameDetailList = new List < StackFrameDetails > ( ) ;
813+ foreach ( var callStackFrameItem in callStack )
810814 {
811- VariableContainerDetails autoVariables =
812- new VariableContainerDetails (
813- this . nextVariableId ++ ,
814- VariableContainerDetails . AutoVariablesName ) ;
815+ var callStackFrameComponents = ( callStackFrameItem as PSObject ) . BaseObject as IList ;
816+ var callStackFrame = callStackFrameComponents [ 0 ] as PSObject ;
817+ IDictionary callStackVariables = isOnRemoteMachine
818+ ? ( callStackFrameComponents [ 1 ] as PSObject ) . BaseObject as IDictionary
819+ : callStackFrameComponents [ 1 ] as IDictionary ;
815820
816- this . variables . Add ( autoVariables ) ;
821+ var autoVariables = new VariableContainerDetails (
822+ nextVariableId ++ ,
823+ VariableContainerDetails . AutoVariablesName ) ;
817824
818- VariableContainerDetails localVariables =
819- await FetchVariableContainerAsync ( i . ToString ( ) , autoVariables ) . ConfigureAwait ( false ) ;
825+ variables . Add ( autoVariables ) ;
820826
821- // When debugging, this is the best way I can find to get what is likely the workspace root.
822- // This is controlled by the "cwd:" setting in the launch config.
823- string workspaceRootPath = _psesHost . InitialWorkingDirectory ;
827+ foreach ( DictionaryEntry entry in callStackVariables )
828+ {
829+ // TODO: This should be deduplicated into a new function for the other variable handling as well
830+ object psVarValue = isOnRemoteMachine
831+ ? ( entry . Value as PSObject ) . Properties [ "Value" ] . Value
832+ : ( entry . Value as PSVariable ) . Value ;
833+ // The constructor we are using here does not automatically add the dollar prefix
834+ string psVarName = VariableDetails . DollarPrefix + entry . Key . ToString ( ) ;
835+ var variableDetails = new VariableDetails ( psVarName , psVarValue ) { Id = nextVariableId ++ } ;
836+ variables . Add ( variableDetails ) ;
837+
838+ if ( AddToAutoVariables ( new PSObject ( entry . Value ) , scope : null ) )
839+ {
840+ autoVariables . Children . Add ( variableDetails . Name , variableDetails ) ;
841+ }
842+ }
824843
825- this . stackFrameDetails [ i ] =
826- StackFrameDetails . Create ( callStackFrames [ i ] , autoVariables , localVariables , workspaceRootPath ) ;
844+ var stackFrameDetailsEntry = StackFrameDetails . Create ( callStackFrame , autoVariables ) ;
827845
828- string stackFrameScriptPath = this . stackFrameDetails [ i ] . ScriptPath ;
829- if ( scriptNameOverride != null &&
846+ string stackFrameScriptPath = stackFrameDetailsEntry . ScriptPath ;
847+ if ( scriptNameOverride is not null &&
830848 string . Equals ( stackFrameScriptPath , StackFrameDetails . NoFileScriptPath ) )
831849 {
832- this . stackFrameDetails [ i ] . ScriptPath = scriptNameOverride ;
850+ stackFrameDetailsEntry . ScriptPath = scriptNameOverride ;
833851 }
834- else if ( _psesHost . CurrentRunspace . IsOnRemoteMachine
835- && this . remoteFileManager != null
852+ else if ( isOnRemoteMachine
853+ && remoteFileManager is not null
836854 && ! string . Equals ( stackFrameScriptPath , StackFrameDetails . NoFileScriptPath ) )
837855 {
838- this . stackFrameDetails [ i ] . ScriptPath =
839- this . remoteFileManager . GetMappedPath (
856+ stackFrameDetailsEntry . ScriptPath =
857+ remoteFileManager . GetMappedPath (
840858 stackFrameScriptPath ,
841859 _psesHost . CurrentRunspace ) ;
842860 }
861+
862+ stackFrameDetailList . Add (
863+ stackFrameDetailsEntry ) ;
843864 }
865+
866+ stackFrameDetails = stackFrameDetailList . ToArray ( ) ;
844867 }
845868
846869 private static string TrimScriptListingLine ( PSObject scriptLineObj , ref int prefixLength )
0 commit comments