Skip to content

Commit 540c9ff

Browse files
committed
Fetch script listings for debugger stop events with no ScriptName
This change adds new behavior to the DebugService which enables it to gather the listing of any source code that is being executed outside of a script file when the debugger hits a breakpoint. This is useful for allowing the user to step through code in this type of scenario. Currently this is being used for an unreleased feature in PowerShell 6.0 which allows you to step into ScriptBlocks run against remote sessions with Invoke-Command.
1 parent ffa27f8 commit 540c9ff

File tree

4 files changed

+197
-55
lines changed

4 files changed

+197
-55
lines changed

src/PowerShellEditorServices/Debugging/DebugService.cs

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using System.Threading.Tasks;
1313
using Microsoft.PowerShell.EditorServices.Debugging;
1414
using Microsoft.PowerShell.EditorServices.Utility;
15-
using System.IO;
1615
using Microsoft.PowerShell.EditorServices.Session;
1716

1817
namespace Microsoft.PowerShell.EditorServices
@@ -26,6 +25,7 @@ public class DebugService
2625
#region Fields
2726

2827
private const string PsesGlobalVariableNamePrefix = "__psEditorServices_";
28+
private const string TemporaryScriptFileName = "TemporaryScript.ps1";
2929

3030
private PowerShellContext powerShellContext;
3131
private RemoteFileManager remoteFileManager;
@@ -35,6 +35,7 @@ public class DebugService
3535
new Dictionary<string, List<Breakpoint>>();
3636

3737
private int nextVariableId;
38+
private string temporaryScriptListingPath;
3839
private List<VariableDetailsBase> variables;
3940
private VariableContainerDetails globalScopeVariables;
4041
private VariableContainerDetails scriptScopeVariables;
@@ -93,8 +94,8 @@ public DebugService(
9394
/// <param name="clearExisting">If true, causes all existing breakpoints to be cleared before setting new ones.</param>
9495
/// <returns>An awaitable Task that will provide details about the breakpoints that were set.</returns>
9596
public async Task<BreakpointDetails[]> SetLineBreakpoints(
96-
ScriptFile scriptFile,
97-
BreakpointDetails[] breakpoints,
97+
ScriptFile scriptFile,
98+
BreakpointDetails[] breakpoints,
9899
bool clearExisting = true)
99100
{
100101
var resultBreakpointDetails = new List<BreakpointDetails>();
@@ -111,22 +112,32 @@ public async Task<BreakpointDetails[]> SetLineBreakpoints(
111112
if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote &&
112113
this.remoteFileManager != null)
113114
{
114-
string mappedPath =
115-
this.remoteFileManager.GetMappedPath(
116-
scriptPath,
117-
this.powerShellContext.CurrentRunspace);
118-
119-
if (mappedPath == null)
115+
if (!this.remoteFileManager.IsUnderRemoteTempPath(scriptPath))
120116
{
121117
Logger.Write(
122-
LogLevel.Error,
123-
$"Could not map local path '{scriptPath}' to a remote path.");
118+
LogLevel.Verbose,
119+
$"Could not set breakpoints for local path '{scriptPath}' in a remote session.");
124120

125121
return resultBreakpointDetails.ToArray();
126122
}
127123

124+
string mappedPath =
125+
this.remoteFileManager.GetMappedPath(
126+
scriptPath,
127+
this.powerShellContext.CurrentRunspace);
128+
128129
scriptPath = mappedPath;
129130
}
131+
else if (
132+
this.temporaryScriptListingPath != null &&
133+
this.temporaryScriptListingPath.Equals(scriptPath, StringComparison.CurrentCultureIgnoreCase))
134+
{
135+
Logger.Write(
136+
LogLevel.Verbose,
137+
$"Could not set breakpoint on temporary script listing path '{scriptPath}'.");
138+
139+
return resultBreakpointDetails.ToArray();
140+
}
130141

131142
// Fix for issue #123 - file paths that contain wildcard chars [ and ] need to
132143
// quoted and have those wildcard chars escaped.
@@ -622,7 +633,7 @@ private async Task ClearCommandBreakpoints()
622633
await this.powerShellContext.ExecuteCommand<object>(psCommand);
623634
}
624635

625-
private async Task FetchStackFramesAndVariables()
636+
private async Task FetchStackFramesAndVariables(string scriptNameOverride)
626637
{
627638
this.nextVariableId = VariableDetailsBase.FirstVariableId;
628639
this.variables = new List<VariableDetailsBase>();
@@ -633,7 +644,7 @@ private async Task FetchStackFramesAndVariables()
633644
// Must retrieve global/script variales before stack frame variables
634645
// as we check stack frame variables against globals.
635646
await FetchGlobalAndScriptVariables();
636-
await FetchStackFrames();
647+
await FetchStackFrames(scriptNameOverride);
637648
}
638649

639650
private async Task FetchGlobalAndScriptVariables()
@@ -750,7 +761,7 @@ private bool AddToAutoVariables(PSObject psvariable, string scope)
750761
return true;
751762
}
752763

753-
private async Task FetchStackFrames()
764+
private async Task FetchStackFrames(string scriptNameOverride)
754765
{
755766
PSCommand psCommand = new PSCommand();
756767

@@ -782,7 +793,12 @@ private async Task FetchStackFrames()
782793
StackFrameDetails.Create(callStackFrames[i], autoVariables, localVariables);
783794

784795
string stackFrameScriptPath = this.stackFrameDetails[i].ScriptPath;
785-
if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote &&
796+
if (scriptNameOverride != null &&
797+
string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
798+
{
799+
this.stackFrameDetails[i].ScriptPath = scriptNameOverride;
800+
}
801+
else if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote &&
786802
this.remoteFileManager != null &&
787803
!string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
788804
{
@@ -979,6 +995,25 @@ private string FormatInvalidBreakpointConditionMessage(string condition, string
979995
return $"'{condition}' is not a valid PowerShell expression. {message}";
980996
}
981997

998+
private string TrimScriptListingLine(PSObject scriptLineObj, ref int prefixLength)
999+
{
1000+
string scriptLine = scriptLineObj.ToString();
1001+
1002+
if (!string.IsNullOrWhiteSpace(scriptLine))
1003+
{
1004+
if (prefixLength == 0)
1005+
{
1006+
// The prefix is a padded integer ending with ':', an asterisk '*'
1007+
// if this is the current line, and one character of padding
1008+
prefixLength = scriptLine.IndexOf(':') + 2;
1009+
}
1010+
1011+
return scriptLine.Substring(prefixLength);
1012+
}
1013+
1014+
return null;
1015+
}
1016+
9821017
#endregion
9831018

9841019
#region Events
@@ -990,11 +1025,56 @@ private string FormatInvalidBreakpointConditionMessage(string condition, string
9901025

9911026
private async void OnDebuggerStop(object sender, DebuggerStopEventArgs e)
9921027
{
1028+
bool noScriptName = false;
1029+
string localScriptPath = e.InvocationInfo.ScriptName;
1030+
1031+
// If there's no ScriptName, get the "list" of the current source
1032+
if (this.remoteFileManager != null && string.IsNullOrEmpty(localScriptPath))
1033+
{
1034+
// Get the current script listing and create the buffer
1035+
PSCommand command = new PSCommand();
1036+
command.AddScript($"list 1 {int.MaxValue}");
1037+
1038+
IEnumerable<PSObject> scriptListingLines =
1039+
await this.powerShellContext.ExecuteCommand<PSObject>(
1040+
command, false, false);
1041+
1042+
if (scriptListingLines != null)
1043+
{
1044+
int linePrefixLength = 0;
1045+
1046+
string scriptListing =
1047+
string.Join(
1048+
Environment.NewLine,
1049+
scriptListingLines
1050+
.Select(o => this.TrimScriptListingLine(o, ref linePrefixLength))
1051+
.Where(s => s != null));
1052+
1053+
this.temporaryScriptListingPath =
1054+
this.remoteFileManager.CreateTemporaryFile(
1055+
TemporaryScriptFileName,
1056+
scriptListing,
1057+
this.powerShellContext.CurrentRunspace);
1058+
1059+
localScriptPath =
1060+
this.temporaryScriptListingPath
1061+
?? StackFrameDetails.NoFileScriptPath;
1062+
1063+
noScriptName = localScriptPath != null;
1064+
}
1065+
else
1066+
{
1067+
Logger.Write(
1068+
LogLevel.Warning,
1069+
$"Could not load script context");
1070+
}
1071+
}
1072+
9931073
// Get call stack and variables.
994-
await this.FetchStackFramesAndVariables();
1074+
await this.FetchStackFramesAndVariables(
1075+
noScriptName ? localScriptPath : null);
9951076

9961077
// If this is a remote connection, get the file content
997-
string localScriptPath = e.InvocationInfo.ScriptName;
9981078
if (this.powerShellContext.CurrentRunspace.Location == RunspaceLocation.Remote &&
9991079
this.remoteFileManager != null)
10001080
{

src/PowerShellEditorServices/Session/PowerShellContext.cs

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -776,31 +776,29 @@ public void Dispose()
776776
// Clean up the active runspace
777777
this.CleanupRunspace(this.CurrentRunspace);
778778

779-
// Drain the runspace stack if any have been pushed
780-
if (this.runspaceStack.Count > 0)
779+
// Push the active runspace so it will be included in the loop
780+
this.runspaceStack.Push(this.CurrentRunspace);
781+
782+
while (this.runspaceStack.Count > 0)
781783
{
782-
// Push the active runspace so it will be included in the loop
783-
this.runspaceStack.Push(this.CurrentRunspace);
784+
RunspaceDetails poppedRunspace = this.runspaceStack.Pop();
784785

785-
while (this.runspaceStack.Count > 1)
786+
// Close the popped runspace if it isn't the initial runspace
787+
// or if it is the initial runspace and we own that runspace
788+
if (this.initialRunspace != poppedRunspace || this.ownsInitialRunspace)
786789
{
787-
RunspaceDetails poppedRunspace = this.runspaceStack.Pop();
788790
this.CloseRunspace(poppedRunspace);
789-
790-
this.OnRunspaceChanged(
791-
this,
792-
new RunspaceChangedEventArgs(
793-
RunspaceChangeAction.Shutdown,
794-
poppedRunspace,
795-
null));
796791
}
797-
}
798792

799-
if (this.ownsInitialRunspace && this.initialRunspace != null)
800-
{
801-
this.CloseRunspace(this.initialRunspace);
802-
this.initialRunspace = null;
793+
this.OnRunspaceChanged(
794+
this,
795+
new RunspaceChangedEventArgs(
796+
RunspaceChangeAction.Shutdown,
797+
poppedRunspace,
798+
null));
803799
}
800+
801+
this.initialRunspace = null;
804802
}
805803

806804
private void CloseRunspace(RunspaceDetails runspaceDetails)

src/PowerShellEditorServices/Session/PowerShellVersionDetails.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public static PowerShellVersionDetails GetVersionDetails(Runspace runspace)
117117
{
118118
powerShellVersion = (Version)version;
119119
}
120-
else if (string.Equals(powerShellEdition, "Core", StringComparison.CurrentCultureIgnoreCase))
120+
else if (version != null)
121121
{
122122
// Expected version string format is 6.0.0-alpha so build a simpler version from that
123123
powerShellVersion = new Version(version.ToString().Split('-')[0]);
@@ -148,7 +148,7 @@ public static PowerShellVersionDetails GetVersionDetails(Runspace runspace)
148148
{
149149
Logger.Write(
150150
LogLevel.Warning,
151-
"Failed to look up PowerShell version. Defaulting to version 5. " + ex.Message);
151+
"Failed to look up PowerShell version, defaulting to version 5.\r\n\r\n" + ex.ToString());
152152
}
153153

154154
return new PowerShellVersionDetails(

0 commit comments

Comments
 (0)