Skip to content

Commit 27b16d9

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 6bb322e commit 27b16d9

File tree

9 files changed

+364
-25
lines changed

9 files changed

+364
-25
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ public async Task<IReadOnlyList<Breakpoint>> GetBreakpointsAsync()
5757
.ConfigureAwait(false);
5858
}
5959

60-
public async Task<IReadOnlyList<BreakpointDetails>> SetBreakpointsAsync(string escapedScriptPath, IReadOnlyList<BreakpointDetails> breakpoints)
60+
public async Task<IReadOnlyList<BreakpointDetails>> SetBreakpointsAsync(string scriptPath, string escapedScriptPath, IReadOnlyList<BreakpointDetails> breakpoints)
6161
{
6262
if (BreakpointApiUtils.SupportsBreakpointApis(_editorServicesHost.CurrentRunspace))
6363
{
6464
foreach (BreakpointDetails breakpointDetails in breakpoints)
6565
{
6666
try
6767
{
68-
BreakpointApiUtils.SetBreakpoint(_editorServicesHost.Runspace.Debugger, breakpointDetails, _debugStateService.RunspaceId);
68+
BreakpointApiUtils.SetBreakpoint(_editorServicesHost.Runspace.Debugger, breakpointDetails, _debugStateService.RunspaceId, scriptPathOverride: scriptPath);
6969
}
7070
catch (InvalidOperationException e)
7171
{

src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ internal class DebugService
4848
private VariableContainerDetails scriptScopeVariables;
4949
private VariableContainerDetails localScopeVariables;
5050
private StackFrameDetails[] stackFrameDetails;
51+
private PathMapping[] _pathMappings;
5152

5253
private readonly SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore();
5354
#endregion
@@ -137,7 +138,11 @@ public async Task<IReadOnlyList<BreakpointDetails>> SetLineBreakpointsAsync(
137138

138139
_psesHost.Runspace.ThrowCancelledIfUnusable();
139140
// Make sure we're using the remote script path
140-
if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null)
141+
if (TryGetMappedRemotePath(scriptPath, out string remoteMappedPath))
142+
{
143+
scriptPath = remoteMappedPath;
144+
}
145+
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine && _remoteFileManager is not null)
141146
{
142147
if (!_remoteFileManager.IsUnderRemoteTempPath(scriptPath))
143148
{
@@ -164,7 +169,7 @@ public async Task<IReadOnlyList<BreakpointDetails>> SetLineBreakpointsAsync(
164169
await _breakpointService.RemoveAllBreakpointsAsync(scriptFile.FilePath).ConfigureAwait(false);
165170
}
166171

167-
return await _breakpointService.SetBreakpointsAsync(escapedScriptPath, breakpoints).ConfigureAwait(false);
172+
return await _breakpointService.SetBreakpointsAsync(scriptPath, escapedScriptPath, breakpoints).ConfigureAwait(false);
168173
}
169174

170175
return await dscBreakpoints
@@ -602,6 +607,59 @@ public VariableScope[] GetVariableScopes(int stackFrameId)
602607
};
603608
}
604609

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

607665
#region Private Methods
@@ -872,14 +930,19 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
872930
StackFrameDetails stackFrameDetailsEntry = StackFrameDetails.Create(callStackFrame, autoVariables, commandVariables);
873931
string stackFrameScriptPath = stackFrameDetailsEntry.ScriptPath;
874932

875-
if (scriptNameOverride is not null
876-
&& string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
933+
bool isNoScriptPath = string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath);
934+
if (scriptNameOverride is not null && isNoScriptPath)
877935
{
878936
stackFrameDetailsEntry.ScriptPath = scriptNameOverride;
879937
}
938+
else if (TryGetMappedLocalPath(stackFrameScriptPath, out string localMappedPath)
939+
&& !isNoScriptPath)
940+
{
941+
stackFrameDetailsEntry.ScriptPath = localMappedPath;
942+
}
880943
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
881944
&& _remoteFileManager is not null
882-
&& !string.Equals(stackFrameScriptPath, StackFrameDetails.NoFileScriptPath))
945+
&& !isNoScriptPath)
883946
{
884947
stackFrameDetailsEntry.ScriptPath =
885948
_remoteFileManager.GetMappedPath(stackFrameScriptPath, _psesHost.CurrentRunspace);
@@ -980,9 +1043,13 @@ await _executionService.ExecutePSCommandAsync<PSObject>(
9801043
// Begin call stack and variables fetch. We don't need to block here.
9811044
StackFramesAndVariablesFetched = FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null);
9821045

1046+
if (!noScriptName && TryGetMappedLocalPath(e.InvocationInfo.ScriptName, out string mappedLocalPath))
1047+
{
1048+
localScriptPath = mappedLocalPath;
1049+
}
9831050
// If this is a remote connection and the debugger stopped at a line
9841051
// in a script file, get the file contents
985-
if (_psesHost.CurrentRunspace.IsOnRemoteMachine
1052+
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
9861053
&& _remoteFileManager is not null
9871054
&& !noScriptName)
9881055
{
@@ -1033,8 +1100,12 @@ private void OnBreakpointUpdated(object sender, BreakpointUpdatedEventArgs e)
10331100
{
10341101
// TODO: This could be either a path or a script block!
10351102
string scriptPath = lineBreakpoint.Script;
1036-
if (_psesHost.CurrentRunspace.IsOnRemoteMachine
1037-
&& _remoteFileManager is not null)
1103+
if (TryGetMappedLocalPath(scriptPath, out string mappedLocalPath))
1104+
{
1105+
scriptPath = mappedLocalPath;
1106+
}
1107+
else if (_psesHost.CurrentRunspace.IsOnRemoteMachine
1108+
&& _remoteFileManager is not null)
10381109
{
10391110
string mappedPath = _remoteFileManager.GetMappedPath(scriptPath, _psesHost.CurrentRunspace);
10401111

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ static BreakpointApiUtils()
110110

111111
#region Public Static Methods
112112

113-
public static Breakpoint SetBreakpoint(Debugger debugger, BreakpointDetailsBase breakpoint, int? runspaceId = null)
113+
public static Breakpoint SetBreakpoint(Debugger debugger, BreakpointDetailsBase breakpoint, int? runspaceId = null, string scriptPathOverride = null)
114114
{
115115
ScriptBlock actionScriptBlock = null;
116116
string logMessage = breakpoint is BreakpointDetails bd ? bd.LogMessage : null;
@@ -136,7 +136,7 @@ public static Breakpoint SetBreakpoint(Debugger debugger, BreakpointDetailsBase
136136
{
137137
BreakpointDetails lineBreakpoint => SetLineBreakpointDelegate(
138138
debugger,
139-
lineBreakpoint.Source,
139+
scriptPathOverride ?? lineBreakpoint.Source,
140140
lineBreakpoint.LineNumber,
141141
lineBreakpoint.ColumnNumber ?? 0,
142142
actionScriptBlock,

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
{

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

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ internal record PsesLaunchRequestArguments : LaunchRequestArguments
7575
/// properties of the 'environmentVariables' are used as key/value pairs.
7676
/// </summary>
7777
public Dictionary<string, string> Env { get; set; }
78+
79+
/// <summary>
80+
/// Gets or sets the path mappings for the debugging session. This is
81+
/// only used when the current runspace is remote.
82+
/// </summary>
83+
public PathMapping[] PathMappings { get; set; } = [];
7884
}
7985

8086
internal record PsesAttachRequestArguments : AttachRequestArguments
@@ -88,6 +94,11 @@ internal record PsesAttachRequestArguments : AttachRequestArguments
8894
public string RunspaceName { get; set; }
8995

9096
public string CustomPipeName { get; set; }
97+
98+
/// <summary>
99+
/// Gets or sets the path mappings for the remote debugging session.
100+
/// </summary>
101+
public PathMapping[] PathMappings { get; set; } = [];
91102
}
92103

93104
internal class LaunchAndAttachHandler : ILaunchHandler<PsesLaunchRequestArguments>, IAttachHandler<PsesAttachRequestArguments>, IOnDebugAdapterServerStarted
@@ -128,6 +139,20 @@ public LaunchAndAttachHandler(
128139
}
129140

130141
public async Task<LaunchResponse> Handle(PsesLaunchRequestArguments request, CancellationToken cancellationToken)
142+
{
143+
_debugService.SetPathMappings(request.PathMappings);
144+
try
145+
{
146+
return await HandleImpl(request, cancellationToken).ConfigureAwait(false);
147+
}
148+
catch
149+
{
150+
_debugService.UnsetPathMappings();
151+
throw;
152+
}
153+
}
154+
155+
public async Task<LaunchResponse> HandleImpl(PsesLaunchRequestArguments request, CancellationToken cancellationToken)
131156
{
132157
// The debugger has officially started. We use this to later check if we should stop it.
133158
((PsesInternalHost)_executionService).DebugContext.IsActive = true;
@@ -205,10 +230,19 @@ public async Task<LaunchResponse> Handle(PsesLaunchRequestArguments request, Can
205230
if (_debugStateService.ScriptToLaunch != null
206231
&& _runspaceContext.CurrentRunspace.IsOnRemoteMachine)
207232
{
208-
_debugStateService.ScriptToLaunch =
209-
_remoteFileManagerService.GetMappedPath(
210-
_debugStateService.ScriptToLaunch,
211-
_runspaceContext.CurrentRunspace);
233+
if (_debugService.TryGetMappedRemotePath(_debugStateService.ScriptToLaunch, out string remoteMappedPath))
234+
{
235+
_debugStateService.ScriptToLaunch = remoteMappedPath;
236+
}
237+
else
238+
{
239+
// If the script is not mapped, we will map it to the remote path
240+
// using the RemoteFileManagerService.
241+
_debugStateService.ScriptToLaunch =
242+
_remoteFileManagerService.GetMappedPath(
243+
_debugStateService.ScriptToLaunch,
244+
_runspaceContext.CurrentRunspace);
245+
}
212246
}
213247

214248
// If no script is being launched, mark this as an interactive
@@ -233,11 +267,13 @@ public async Task<AttachResponse> Handle(PsesAttachRequestArguments request, Can
233267
_debugService.IsDebuggingRemoteRunspace = true;
234268
try
235269
{
270+
_debugService.SetPathMappings(request.PathMappings);
236271
return await HandleImpl(request, cancellationToken).ConfigureAwait(false);
237272
}
238273
catch
239274
{
240275
_debugService.IsDebuggingRemoteRunspace = false;
276+
_debugService.UnsetPathMappings();
241277
throw;
242278
}
243279
}
@@ -469,6 +505,7 @@ private async Task OnExecutionCompletedAsync(Task executeTask)
469505
_debugEventHandlerService.UnregisterEventHandlers();
470506

471507
_debugService.IsDebuggingRemoteRunspace = false;
508+
_debugService.UnsetPathMappings();
472509

473510
if (!isRunspaceClosed && _debugStateService.IsAttachSession)
474511
{

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ public async Task<StackTraceResponse> Handle(StackTraceArguments request, Cancel
3838
InvocationInfo invocationInfo = debugService.CurrentDebuggerStoppedEventArgs?.OriginalEvent?.InvocationInfo
3939
?? throw new RpcErrorException(0, null!, "InvocationInfo was not available on CurrentDebuggerStoppedEvent args. This is a bug and you should report it.");
4040

41-
StackFrame breakpointLabel = CreateBreakpointLabel(invocationInfo);
41+
string? scriptNameOverride = null;
42+
if (debugService.TryGetMappedLocalPath(invocationInfo.ScriptName, out string mappedLocalPath))
43+
{
44+
scriptNameOverride = mappedLocalPath;
45+
}
46+
StackFrame breakpointLabel = CreateBreakpointLabel(invocationInfo, scriptNameOverride: scriptNameOverride);
4247

4348
if (skip == 0 && take == 1) // This indicates the client is doing an initial fetch, so we want to return quickly to unblock the UI and wait on the remaining stack frames for the subsequent requests.
4449
{
@@ -116,13 +121,13 @@ public static StackFrame CreateStackFrame(StackFrameDetails stackFrame, long id)
116121
};
117122
}
118123

119-
public static StackFrame CreateBreakpointLabel(InvocationInfo invocationInfo, int id = 0) => new()
124+
public static StackFrame CreateBreakpointLabel(InvocationInfo invocationInfo, int id = 0, string? scriptNameOverride = null) => new()
120125
{
121126
Name = "<Breakpoint>",
122127
Id = id,
123128
Source = new()
124129
{
125-
Path = invocationInfo.ScriptName
130+
Path = scriptNameOverride ?? invocationInfo.ScriptName
126131
},
127132
Line = invocationInfo.ScriptLineNumber,
128133
Column = invocationInfo.OffsetInLine,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.PowerShell.EditorServices.Services;
5+
6+
/// <summary>
7+
/// Used for attach requests to map a local and remote path together.
8+
/// </summary>
9+
internal record PathMapping
10+
{
11+
/// <summary>
12+
/// Gets or sets the local root of this mapping entry.
13+
/// </summary>
14+
public string LocalRoot { get; set; }
15+
16+
/// <summary>
17+
/// Gets or sets the remote root of this mapping entry.
18+
/// </summary>
19+
public string RemoteRoot { get; set; }
20+
}

src/PowerShellEditorServices/Services/PowerShell/Debugging/DscBreakpointCapability.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,12 @@ public static async Task<DscBreakpointCapability> GetDscCapabilityAsync(
9191
.AddCommand(@"Microsoft.PowerShell.Core\Import-Module")
9292
.AddParameter("Name", "PSDesiredStateConfiguration")
9393
.AddParameter("PassThru")
94-
.AddParameter("ErrorAction", ActionPreference.Ignore);
94+
.AddParameter("ErrorAction", ActionPreference.Ignore)
95+
.AddCommand(@"Microsoft.PowerShell.Utility\Select-Object")
96+
.AddParameter("ExpandProperty", "Name");
9597

96-
IReadOnlyList<PSModuleInfo> dscModule =
97-
await executionService.ExecutePSCommandAsync<PSModuleInfo>(
98+
IReadOnlyList<string> dscModule =
99+
await executionService.ExecutePSCommandAsync<string>(
98100
psCommand,
99101
CancellationToken.None,
100102
new PowerShellExecutionOptions { ThrowOnError = false })

0 commit comments

Comments
 (0)