|
16 | 16 | using Microsoft.PowerShell.EditorServices.Services.PowerShell.Debugging;
|
17 | 17 | using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
|
18 | 18 | using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
|
| 19 | +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; |
19 | 20 | using Microsoft.PowerShell.EditorServices.Services.TextDocument;
|
20 | 21 | using Microsoft.PowerShell.EditorServices.Utility;
|
21 | 22 |
|
@@ -74,6 +75,15 @@ internal class DebugService
|
74 | 75 | /// </summary>
|
75 | 76 | public DebuggerStoppedEventArgs CurrentDebuggerStoppedEventArgs { get; private set; }
|
76 | 77 |
|
| 78 | + /// <summary> |
| 79 | + /// Tracks whether we are running <c>Debug-Runspace</c> in an out-of-process runspace. |
| 80 | + /// </summary> |
| 81 | + public bool IsDebuggingRemoteRunspace |
| 82 | + { |
| 83 | + get => _debugContext.IsDebuggingRemoteRunspace; |
| 84 | + set => _debugContext.IsDebuggingRemoteRunspace = value; |
| 85 | + } |
| 86 | + |
77 | 87 | #endregion
|
78 | 88 |
|
79 | 89 | #region Constructors
|
@@ -128,6 +138,8 @@ public async Task<BreakpointDetails[]> SetLineBreakpointsAsync(
|
128 | 138 | DscBreakpointCapability dscBreakpoints = await _debugContext.GetDscBreakpointCapabilityAsync(CancellationToken.None).ConfigureAwait(false);
|
129 | 139 |
|
130 | 140 | string scriptPath = scriptFile.FilePath;
|
| 141 | + |
| 142 | + _psesHost.Runspace.ThrowCancelledIfUnusable(); |
131 | 143 | // Make sure we're using the remote script path
|
132 | 144 | if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null)
|
133 | 145 | {
|
@@ -795,34 +807,35 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
|
795 | 807 | const string callStackVarName = $"$global:{PsesGlobalVariableNamePrefix}CallStack";
|
796 | 808 | const string getPSCallStack = $"Get-PSCallStack | ForEach-Object {{ [void]{callStackVarName}.Add(@($PSItem, $PSItem.GetFrameVariables())) }}";
|
797 | 809 |
|
| 810 | + _psesHost.Runspace.ThrowCancelledIfUnusable(); |
798 | 811 | // If we're attached to a remote runspace, we need to serialize the list prior to
|
799 | 812 | // transport because the default depth is too shallow. From testing, we determined the
|
800 |
| - // correct depth is 3. The script always calls `Get-PSCallStack`. On a local machine, we |
801 |
| - // just return its results. On a remote machine we serialize it first and then later |
| 813 | + // correct depth is 3. The script always calls `Get-PSCallStack`. In a local runspace, we |
| 814 | + // just return its results. In a remote runspace we serialize it first and then later |
802 | 815 | // deserialize it.
|
803 |
| - bool isOnRemoteMachine = _psesHost.CurrentRunspace.IsOnRemoteMachine; |
804 |
| - string returnSerializedIfOnRemoteMachine = isOnRemoteMachine |
| 816 | + bool isRemoteRunspace = _psesHost.CurrentRunspace.Runspace.RunspaceIsRemote; |
| 817 | + string returnSerializedIfInRemoteRunspace = isRemoteRunspace |
805 | 818 | ? $"[Management.Automation.PSSerializer]::Serialize({callStackVarName}, 3)"
|
806 | 819 | : callStackVarName;
|
807 | 820 |
|
808 | 821 | // PSObject is used here instead of the specific type because we get deserialized
|
809 | 822 | // objects from remote sessions and want a common interface.
|
810 |
| - var psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfOnRemoteMachine}"); |
| 823 | + var psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfInRemoteRunspace}"); |
811 | 824 | IReadOnlyList<PSObject> results = await _executionService.ExecutePSCommandAsync<PSObject>(psCommand, CancellationToken.None).ConfigureAwait(false);
|
812 | 825 |
|
813 |
| - IEnumerable callStack = isOnRemoteMachine |
814 |
| - ? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject).BaseObject as IList |
| 826 | + IEnumerable callStack = isRemoteRunspace |
| 827 | + ? (PSSerializer.Deserialize(results[0].BaseObject as string) as PSObject)?.BaseObject as IList |
815 | 828 | : results;
|
816 | 829 |
|
817 | 830 | var stackFrameDetailList = new List<StackFrameDetails>();
|
818 | 831 | bool isTopStackFrame = true;
|
819 | 832 | foreach (var callStackFrameItem in callStack)
|
820 | 833 | {
|
821 | 834 | // We have to use reflection to get the variable dictionary.
|
822 |
| - var callStackFrameComponents = (callStackFrameItem as PSObject).BaseObject as IList; |
| 835 | + var callStackFrameComponents = (callStackFrameItem as PSObject)?.BaseObject as IList; |
823 | 836 | var callStackFrame = callStackFrameComponents[0] as PSObject;
|
824 |
| - IDictionary callStackVariables = isOnRemoteMachine |
825 |
| - ? (callStackFrameComponents[1] as PSObject).BaseObject as IDictionary |
| 837 | + IDictionary callStackVariables = isRemoteRunspace |
| 838 | + ? (callStackFrameComponents[1] as PSObject)?.BaseObject as IDictionary |
826 | 839 | : callStackFrameComponents[1] as IDictionary;
|
827 | 840 |
|
828 | 841 | var autoVariables = new VariableContainerDetails(
|
@@ -885,7 +898,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
|
885 | 898 | {
|
886 | 899 | stackFrameDetailsEntry.ScriptPath = scriptNameOverride;
|
887 | 900 | }
|
888 |
| - else if (isOnRemoteMachine |
| 901 | + else if (_psesHost.CurrentRunspace.IsOnRemoteMachine |
889 | 902 | && _remoteFileManager is not null
|
890 | 903 | && !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
|
891 | 904 | {
|
@@ -929,83 +942,98 @@ private static string TrimScriptListingLine(PSObject scriptLineObj, ref int pref
|
929 | 942 |
|
930 | 943 | internal async void OnDebuggerStopAsync(object sender, DebuggerStopEventArgs e)
|
931 | 944 | {
|
932 |
| - bool noScriptName = false; |
933 |
| - string localScriptPath = e.InvocationInfo.ScriptName; |
934 |
| - |
935 |
| - // If there's no ScriptName, get the "list" of the current source |
936 |
| - if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath)) |
| 945 | + try |
937 | 946 | {
|
938 |
| - // Get the current script listing and create the buffer |
939 |
| - PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}"); |
| 947 | + bool noScriptName = false; |
| 948 | + string localScriptPath = e.InvocationInfo.ScriptName; |
940 | 949 |
|
941 |
| - IReadOnlyList<PSObject> scriptListingLines = |
942 |
| - await _executionService.ExecutePSCommandAsync<PSObject>( |
943 |
| - command, CancellationToken.None).ConfigureAwait(false); |
944 |
| - |
945 |
| - if (scriptListingLines is not null) |
| 950 | + // If there's no ScriptName, get the "list" of the current source |
| 951 | + if (_remoteFileManager is not null && string.IsNullOrEmpty(localScriptPath)) |
946 | 952 | {
|
947 |
| - int linePrefixLength = 0; |
| 953 | + // Get the current script listing and create the buffer |
| 954 | + PSCommand command = new PSCommand().AddScript($"list 1 {int.MaxValue}"); |
948 | 955 |
|
949 |
| - string scriptListing = |
950 |
| - string.Join( |
951 |
| - Environment.NewLine, |
952 |
| - scriptListingLines |
953 |
| - .Select(o => TrimScriptListingLine(o, ref linePrefixLength)) |
954 |
| - .Where(s => s is not null)); |
| 956 | + IReadOnlyList<PSObject> scriptListingLines = |
| 957 | + await _executionService.ExecutePSCommandAsync<PSObject>( |
| 958 | + command, CancellationToken.None).ConfigureAwait(false); |
955 | 959 |
|
956 |
| - temporaryScriptListingPath = |
957 |
| - _remoteFileManager.CreateTemporaryFile( |
958 |
| - $"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}", |
959 |
| - scriptListing, |
960 |
| - _psesHost.CurrentRunspace); |
| 960 | + if (scriptListingLines is not null) |
| 961 | + { |
| 962 | + int linePrefixLength = 0; |
| 963 | + |
| 964 | + string scriptListing = |
| 965 | + string.Join( |
| 966 | + Environment.NewLine, |
| 967 | + scriptListingLines |
| 968 | + .Select(o => TrimScriptListingLine(o, ref linePrefixLength)) |
| 969 | + .Where(s => s is not null)); |
| 970 | + |
| 971 | + temporaryScriptListingPath = |
| 972 | + _remoteFileManager.CreateTemporaryFile( |
| 973 | + $"[{_psesHost.CurrentRunspace.SessionDetails.ComputerName}] {TemporaryScriptFileName}", |
| 974 | + scriptListing, |
| 975 | + _psesHost.CurrentRunspace); |
| 976 | + |
| 977 | + localScriptPath = |
| 978 | + temporaryScriptListingPath |
| 979 | + ?? StackFrameDetails.NoFileScriptPath; |
| 980 | + |
| 981 | + noScriptName = localScriptPath is not null; |
| 982 | + } |
| 983 | + else |
| 984 | + { |
| 985 | + _logger.LogWarning("Could not load script context"); |
| 986 | + } |
| 987 | + } |
961 | 988 |
|
962 |
| - localScriptPath = |
963 |
| - temporaryScriptListingPath |
964 |
| - ?? StackFrameDetails.NoFileScriptPath; |
| 989 | + // Get call stack and variables. |
| 990 | + await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false); |
965 | 991 |
|
966 |
| - noScriptName = localScriptPath is not null; |
| 992 | + // If this is a remote connection and the debugger stopped at a line |
| 993 | + // in a script file, get the file contents |
| 994 | + if (_psesHost.CurrentRunspace.IsOnRemoteMachine |
| 995 | + && _remoteFileManager is not null |
| 996 | + && !noScriptName) |
| 997 | + { |
| 998 | + localScriptPath = |
| 999 | + await _remoteFileManager.FetchRemoteFileAsync( |
| 1000 | + e.InvocationInfo.ScriptName, |
| 1001 | + _psesHost.CurrentRunspace).ConfigureAwait(false); |
967 | 1002 | }
|
968 |
| - else |
| 1003 | + |
| 1004 | + if (stackFrameDetails.Length > 0) |
969 | 1005 | {
|
970 |
| - _logger.LogWarning("Could not load script context"); |
| 1006 | + // Augment the top stack frame with details from the stop event |
| 1007 | + if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent) |
| 1008 | + { |
| 1009 | + stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber; |
| 1010 | + stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber; |
| 1011 | + stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber; |
| 1012 | + stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber; |
| 1013 | + } |
971 | 1014 | }
|
972 |
| - } |
973 | 1015 |
|
974 |
| - // Get call stack and variables. |
975 |
| - await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false); |
| 1016 | + CurrentDebuggerStoppedEventArgs = |
| 1017 | + new DebuggerStoppedEventArgs( |
| 1018 | + e, |
| 1019 | + _psesHost.CurrentRunspace, |
| 1020 | + localScriptPath); |
976 | 1021 |
|
977 |
| - // If this is a remote connection and the debugger stopped at a line |
978 |
| - // in a script file, get the file contents |
979 |
| - if (_psesHost.CurrentRunspace.IsOnRemoteMachine |
980 |
| - && _remoteFileManager is not null |
981 |
| - && !noScriptName) |
| 1022 | + // Notify the host that the debugger is stopped. |
| 1023 | + DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs); |
| 1024 | + } |
| 1025 | + catch (OperationCanceledException) |
982 | 1026 | {
|
983 |
| - localScriptPath = |
984 |
| - await _remoteFileManager.FetchRemoteFileAsync( |
985 |
| - e.InvocationInfo.ScriptName, |
986 |
| - _psesHost.CurrentRunspace).ConfigureAwait(false); |
| 1027 | + // Ignore, likely means that a remote runspace has closed. |
987 | 1028 | }
|
988 |
| - |
989 |
| - if (stackFrameDetails.Length > 0) |
| 1029 | + catch (Exception exception) |
990 | 1030 | {
|
991 |
| - // Augment the top stack frame with details from the stop event |
992 |
| - if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent) |
993 |
| - { |
994 |
| - stackFrameDetails[0].StartLineNumber = scriptExtent.StartLineNumber; |
995 |
| - stackFrameDetails[0].EndLineNumber = scriptExtent.EndLineNumber; |
996 |
| - stackFrameDetails[0].StartColumnNumber = scriptExtent.StartColumnNumber; |
997 |
| - stackFrameDetails[0].EndColumnNumber = scriptExtent.EndColumnNumber; |
998 |
| - } |
| 1031 | + // Log in a catch all so we don't crash the process. |
| 1032 | + _logger.LogError( |
| 1033 | + exception, |
| 1034 | + "Error occurred while obtaining debug info. Message: {message}", |
| 1035 | + exception.Message); |
999 | 1036 | }
|
1000 |
| - |
1001 |
| - CurrentDebuggerStoppedEventArgs = |
1002 |
| - new DebuggerStoppedEventArgs( |
1003 |
| - e, |
1004 |
| - _psesHost.CurrentRunspace, |
1005 |
| - localScriptPath); |
1006 |
| - |
1007 |
| - // Notify the host that the debugger is stopped. |
1008 |
| - DebuggerStopped?.Invoke(sender, CurrentDebuggerStoppedEventArgs); |
1009 | 1037 | }
|
1010 | 1038 |
|
1011 | 1039 | private void OnDebuggerResuming(object sender, DebuggerResumingEventArgs debuggerResumingEventArgs)
|
|
0 commit comments