Skip to content

Commit 0837fa1

Browse files
committed
Add pathMappings option to debugger
Adds the `pathMappings` option to the debugger that can be used to map a local to remote path and vice versa. This is useful if the local environment has a checkout of the files being run on a remote target but at a different path. The mappings are used to translate the paths that will the breakpoint will be set to in the target PowerShell instance. It is also used to update the stack trace paths received from the remote. For a launch scenario, the path mappings are also used when launching a script if the integrated terminal has entered a remote runspace.
1 parent 79ff7dd commit 0837fa1

File tree

12 files changed

+493
-60
lines changed

12 files changed

+493
-60
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public async Task<IReadOnlyList<BreakpointDetails>> SetBreakpointsAsync(IReadOnl
195195
// path which may or may not exist.
196196
psCommand
197197
.AddScript(_setPSBreakpointLegacy, useLocalScope: true)
198-
.AddParameter("Script", breakpoint.Source)
198+
.AddParameter("Script", breakpoint.MappedSource ?? breakpoint.Source)
199199
.AddParameter("Line", breakpoint.LineNumber);
200200

201201
// Check if the user has specified the column number for the breakpoint.
@@ -219,7 +219,16 @@ public async Task<IReadOnlyList<BreakpointDetails>> SetBreakpointsAsync(IReadOnl
219219
IEnumerable<Breakpoint> setBreakpoints = await _executionService
220220
.ExecutePSCommandAsync<Breakpoint>(psCommand, CancellationToken.None)
221221
.ConfigureAwait(false);
222-
configuredBreakpoints.AddRange(setBreakpoints.Select((breakpoint) => BreakpointDetails.Create(breakpoint)));
222+
223+
int bpIdx = 0;
224+
foreach (Breakpoint setBp in setBreakpoints)
225+
{
226+
BreakpointDetails setBreakpoint = BreakpointDetails.Create(
227+
setBp,
228+
sourceBreakpoint: breakpoints[bpIdx]);
229+
configuredBreakpoints.Add(setBreakpoint);
230+
bpIdx++;
231+
}
223232
}
224233
return configuredBreakpoints;
225234
}

src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
1616
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host;
1717
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility;
18-
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
1918
using Microsoft.PowerShell.EditorServices.Utility;
2019

2120
namespace Microsoft.PowerShell.EditorServices.Services
@@ -49,6 +48,7 @@ internal class DebugService
4948
private VariableContainerDetails scriptScopeVariables;
5049
private VariableContainerDetails localScopeVariables;
5150
private StackFrameDetails[] stackFrameDetails;
51+
private PathMapping[] _pathMappings;
5252

5353
private readonly SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore();
5454
#endregion
@@ -123,22 +123,22 @@ public DebugService(
123123
/// <summary>
124124
/// Sets the list of line breakpoints for the current debugging session.
125125
/// </summary>
126-
/// <param name="scriptFile">The ScriptFile in which breakpoints will be set.</param>
126+
/// <param name="scriptPath">The path in which breakpoints will be set.</param>
127127
/// <param name="breakpoints">BreakpointDetails for each breakpoint that will be set.</param>
128128
/// <param name="clearExisting">If true, causes all existing breakpoints to be cleared before setting new ones.</param>
129+
/// <param name="skipRemoteMapping">If true, skips the remote file manager mapping of the script path.</param>
129130
/// <returns>An awaitable Task that will provide details about the breakpoints that were set.</returns>
130131
public async Task<IReadOnlyList<BreakpointDetails>> SetLineBreakpointsAsync(
131-
ScriptFile scriptFile,
132+
string scriptPath,
132133
IReadOnlyList<BreakpointDetails> breakpoints,
133-
bool clearExisting = true)
134+
bool clearExisting = true,
135+
bool skipRemoteMapping = false)
134136
{
135137
DscBreakpointCapability dscBreakpoints = await _debugContext.GetDscBreakpointCapabilityAsync().ConfigureAwait(false);
136138

137-
string scriptPath = scriptFile.FilePath;
138-
139139
_psesHost.Runspace.ThrowCancelledIfUnusable();
140140
// Make sure we're using the remote script path
141-
if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null)
141+
if (!skipRemoteMapping && _psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null)
142142
{
143143
if (!_remoteFileManager.IsUnderRemoteTempPath(scriptPath))
144144
{
@@ -162,7 +162,7 @@ public async Task<IReadOnlyList<BreakpointDetails>> SetLineBreakpointsAsync(
162162
{
163163
if (clearExisting)
164164
{
165-
await _breakpointService.RemoveAllBreakpointsAsync(scriptFile.FilePath).ConfigureAwait(false);
165+
await _breakpointService.RemoveAllBreakpointsAsync(scriptPath).ConfigureAwait(false);
166166
}
167167

168168
return await _breakpointService.SetBreakpointsAsync(breakpoints).ConfigureAwait(false);
@@ -603,6 +603,59 @@ public VariableScope[] GetVariableScopes(int stackFrameId)
603603
};
604604
}
605605

606+
internal void SetPathMappings(PathMapping[] pathMappings) => _pathMappings = pathMappings;
607+
608+
internal void UnsetPathMappings() => _pathMappings = null;
609+
610+
internal bool TryGetMappedLocalPath(string remotePath, out string localPath)
611+
{
612+
if (_pathMappings is not null)
613+
{
614+
foreach (PathMapping mapping in _pathMappings)
615+
{
616+
if (string.IsNullOrWhiteSpace(mapping.LocalRoot) || string.IsNullOrWhiteSpace(mapping.RemoteRoot))
617+
{
618+
// If either path mapping is null, we can't map the path.
619+
continue;
620+
}
621+
622+
if (remotePath.StartsWith(mapping.RemoteRoot, StringComparison.OrdinalIgnoreCase))
623+
{
624+
localPath = mapping.LocalRoot + remotePath.Substring(mapping.RemoteRoot.Length);
625+
return true;
626+
}
627+
}
628+
}
629+
630+
localPath = null;
631+
return false;
632+
}
633+
634+
internal bool TryGetMappedRemotePath(string localPath, out string remotePath)
635+
{
636+
if (_pathMappings is not null)
637+
{
638+
foreach (PathMapping mapping in _pathMappings)
639+
{
640+
if (string.IsNullOrWhiteSpace(mapping.LocalRoot) || string.IsNullOrWhiteSpace(mapping.RemoteRoot))
641+
{
642+
// If either path mapping is null, we can't map the path.
643+
continue;
644+
}
645+
646+
if (localPath.StartsWith(mapping.LocalRoot, StringComparison.OrdinalIgnoreCase))
647+
{
648+
// If the local path starts with the local path mapping, we can replace it with the remote path.
649+
remotePath = mapping.RemoteRoot + localPath.Substring(mapping.LocalRoot.Length);
650+
return true;
651+
}
652+
}
653+
}
654+
655+
remotePath = null;
656+
return false;
657+
}
658+
606659
#endregion
607660

608661
#region Private Methods
@@ -873,14 +926,19 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
873926
StackFrameDetails stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, commandVariables);
874927
string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath;
875928

876-
if (scriptNameOverride is not null
877-
&& string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
929+
bool isNoScriptPath = string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath);
930+
if (scriptNameOverride is not null && isNoScriptPath)
878931
{
879932
stackFrameDetailsEntry.ScriptPath = scriptNameOverride;
880933
}
934+
else if (TryGetMappedLocalPath(stackFrameScriptPath, out string localMappedPath)
935+
&& !isNoScriptPath)
936+
{
937+
stackFrameDetailsEntry.ScriptPath = localMappedPath;
938+
}
881939
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
882940
&& _remoteFileManager is not null
883-
&& !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
941+
&& !isNoScriptPath)
884942
{
885943
stackFrameDetailsEntry.ScriptPath =
886944
_remoteFileManager.GetMappedPath(stackFrameScriptPath, _psesHost.CurrentRunspace);
@@ -981,9 +1039,13 @@ await _executionService.ExecutePSCommandAsync<PSObject>(
9811039
// Begin call stack and variables fetch. We don't need to block here.
9821040
StackFramesAndVariablesFetched = FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null);
9831041

1042+
if (!noScriptName && TryGetMappedLocalPath(e.InvocationInfo.ScriptName, out string mappedLocalPath))
1043+
{
1044+
localScriptPath = mappedLocalPath;
1045+
}
9841046
// If this is a remote connection and the debugger stopped at a line
9851047
// in a script file, get the file contents
986-
if (_psesHost.CurrentRunspace.IsOnRemoteMachine
1048+
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
9871049
&& _remoteFileManager is not null
9881050
&& !noScriptName)
9891051
{
@@ -1034,8 +1096,12 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e)
10341096
{
10351097
// TODO: This could be either a path or a script block!
10361098
string scriptPath = lineBreakpoint.Script;
1037-
if (_psesHost.CurrentRunspace.IsOnRemoteMachine
1038-
&& _remoteFileManager is not null)
1099+
if (TryGetMappedLocalPath(scriptPath, out string mappedLocalPath))
1100+
{
1101+
scriptPath = mappedLocalPath;
1102+
}
1103+
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
1104+
&& _remoteFileManager is not null)
10391105
{
10401106
string mappedPath = _remoteFileManager.GetMappedPath(scriptPath, _psesHost.CurrentRunspace);
10411107

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public static Breakpoint SetBreakpoint(Debugger debugger, BreakpointDetailsBase
136136
{
137137
BreakpointDetails lineBreakpoint => SetLineBreakpointDelegate(
138138
debugger,
139-
lineBreakpoint.Source,
139+
lineBreakpoint.MappedSource ?? lineBreakpoint.Source,
140140
lineBreakpoint.LineNumber,
141141
lineBreakpoint.ColumnNumber ?? 0,
142142
actionScriptBlock,

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ internal sealed class BreakpointDetails : BreakpointDetailsBase
2424
/// </summary>
2525
public string Source { get; private set; }
2626

27+
/// <summary>
28+
/// Gets the source where the breakpoint is mapped to, will be null if no mapping exists. Used only for debug purposes.
29+
/// </summary>
30+
public string MappedSource { get; private set; }
31+
2732
/// <summary>
2833
/// Gets the line number at which the breakpoint is set.
2934
/// </summary>
@@ -50,14 +55,16 @@ private BreakpointDetails()
5055
/// <param name="condition"></param>
5156
/// <param name="hitCondition"></param>
5257
/// <param name="logMessage"></param>
58+
/// <param name="mappedSource"></param>
5359
/// <returns></returns>
5460
internal static BreakpointDetails Create(
5561
string source,
5662
int line,
5763
int? column = null,
5864
string condition = null,
5965
string hitCondition = null,
60-
string logMessage = null)
66+
string logMessage = null,
67+
string mappedSource = null)
6168
{
6269
Validate.IsNotNullOrEmptyString(nameof(source), source);
6370

@@ -69,7 +76,8 @@ internal static BreakpointDetails Create(
6976
ColumnNumber = column,
7077
Condition = condition,
7178
HitCondition = hitCondition,
72-
LogMessage = logMessage
79+
LogMessage = logMessage,
80+
MappedSource = mappedSource
7381
};
7482
}
7583

@@ -79,10 +87,12 @@ internal static BreakpointDetails Create(
7987
/// </summary>
8088
/// <param name="breakpoint">The Breakpoint instance from which details will be taken.</param>
8189
/// <param name="updateType">The BreakpointUpdateType to determine if the breakpoint is verified.</param>
90+
/// /// <param name="sourceBreakpoint">The breakpoint source from the debug client, if any.</param>
8291
/// <returns>A new instance of the BreakpointDetails class.</returns>
8392
internal static BreakpointDetails Create(
8493
Breakpoint breakpoint,
85-
BreakpointUpdateType updateType = BreakpointUpdateType.Set)
94+
BreakpointUpdateType updateType = BreakpointUpdateType.Set,
95+
BreakpointDetails sourceBreakpoint = null)
8696
{
8797
Validate.IsNotNull(nameof(breakpoint), breakpoint);
8898

@@ -96,10 +106,11 @@ internal static BreakpointDetails Create(
96106
{
97107
Id = breakpoint.Id,
98108
Verified = updateType != BreakpointUpdateType.Disabled,
99-
Source = lineBreakpoint.Script,
109+
Source = sourceBreakpoint?.MappedSource is not null ? sourceBreakpoint.Source : lineBreakpoint.Script,
100110
LineNumber = lineBreakpoint.Line,
101111
ColumnNumber = lineBreakpoint.Column,
102-
Condition = lineBreakpoint.Action?.ToString()
112+
Condition = lineBreakpoint.Action?.ToString(),
113+
MappedSource = sourceBreakpoint?.MappedSource,
103114
};
104115

105116
if (lineBreakpoint.Column > 0)

src/PowerShellEditorServices/Services/DebugAdapter/Handlers/BreakpointHandlers.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,20 @@ public async Task<SetBreakpointsResponse> Handle(SetBreakpointsArguments request
7979
}
8080

8181
// At this point, the source file has been verified as a PowerShell script.
82+
string mappedSource = null;
83+
if (_debugService.TryGetMappedRemotePath(scriptFile.FilePath, out string remoteMappedPath))
84+
{
85+
mappedSource = remoteMappedPath;
86+
}
8287
IReadOnlyList<BreakpointDetails> breakpointDetails = request.Breakpoints
8388
.Select((srcBreakpoint) => BreakpointDetails.Create(
8489
scriptFile.FilePath,
8590
srcBreakpoint.Line,
8691
srcBreakpoint.Column,
8792
srcBreakpoint.Condition,
8893
srcBreakpoint.HitCondition,
89-
srcBreakpoint.LogMessage)).ToList();
94+
srcBreakpoint.LogMessage,
95+
mappedSource: mappedSource)).ToList();
9096

9197
// If this is a "run without debugging (Ctrl+F5)" session ignore requests to set breakpoints.
9298
IReadOnlyList<BreakpointDetails> updatedBreakpointDetails = breakpointDetails;
@@ -98,8 +104,9 @@ public async Task<SetBreakpointsResponse> Handle(SetBreakpointsArguments request
98104
{
99105
updatedBreakpointDetails =
100106
await _debugService.SetLineBreakpointsAsync(
101-
scriptFile,
102-
breakpointDetails).ConfigureAwait(false);
107+
mappedSource ?? scriptFile.FilePath,
108+
breakpointDetails,
109+
skipRemoteMapping: mappedSource is not null).ConfigureAwait(false);
103110
}
104111
catch (Exception e)
105112
{

src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DisconnectHandler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public async Task<DisconnectResponse> Handle(DisconnectArguments request, Cancel
5050
// We should instead ensure that the debugger is in some valid state, lock it and then tear things down
5151

5252
_debugEventHandlerService.UnregisterEventHandlers();
53+
_debugService.UnsetPathMappings();
5354

5455
if (!_debugStateService.ExecutionCompleted)
5556
{

0 commit comments

Comments
 (0)