Repository navigation
Ignore file path validation on Set-PSBreakpoint for attach scenarios #2243
Description
Prerequisites
- I have written a descriptive issue title.
- I have searched all issues to ensure it has not already been requested.
Summary
I would live to avoid the file path validation that occurs when PSES sets a breakpoint on PowerShell 5.1. There is an API split where PowerShell 5.1 uses the Set-PSBreakpoint to set a line breakpoint whereas 7.x uses the PSRemoting based __Set-PSBreakpoint command which is treated specially by the server. The 7.x API does not do any validation on the file path whereas Set-PSBreakpoint used by 5.1 will fail if the path does not exist.
For an attach scenario, PSES could be attaching itself to a runspace where the scriptblocks are set to a path that may or may not exist, e.g.
$sbk = [System.Management.Automation.Language.Parser]::ParseInput(
'$PSCommandPath',
'foo.ps1',
[ref]$null,
[ref]$null).GetScriptBlock()
& $sbk # foo.ps1In the above case the script could be multiple lines and while 7.x will be able to set breakpoints on any of lines by targeting foo.ps1 as the script, WinPS 5.1 will fail because foo.ps1 does not exist.
Proposed Design
We can implement this by not calling Set-PSBreakpoint in the 5.1 fallback path.
PowerShellEditorServices/src/PowerShellEditorServices/Services/DebugAdapter/BreakpointService.cs
Lines 117 to 120 in 6bb322e
| psCommand | |
| .AddCommand(@"Microsoft.PowerShell.Utility\Set-PSBreakpoint") | |
| .AddParameter("Script", escapedScriptPath) | |
| .AddParameter("Line", breakpoint.LineNumber); |
Instead we could do something like
namespace Microsoft.PowerShell.EditorServices.Services
{
internal class BreakpointService
{
private readonly string _setPsBreakpointScript = """
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]
$Script,
[Parameter()]
[int]
$Line,
[Parameter()]
[int]
$Column,
[Parameter()]
[ScriptBlock]
$Action
)
$lineCtor = [System.Management.Automation.LineBreakpoint].GetConstructor(
[System.Reflection.BindingFlags]'NonPublic, Instance',
$null,
[type[]]@([string], [int], [int], [scriptblock]),
$null)
$b = $lineCtor.Invoke(@($Script, $Line, $Column, $Action))
[Runspace]::DefaultRunspace.Debugger.SetBreakpoints(
[System.Management.Automation.Breakpoint[]]@($b))
$b
""";
...
// In SetBreakpointsAsync
// Instead of psCommand.AddCommand(...) do this instead
psCommand
.AddScript(_setPsBreakpointScript, useLocalScope: true)
.AddParameter("Script", escapedScriptPath)
.AddParameter("Line", breakpoint.LineNumber);This uses a small bit of reflection to build the LineBreakpoint object which did not have a public constructor in 5.1. While reflection isn't great, 5.1 is mostly an EOL product and newer versions will be using the public API so we shouldn't have any future breakage issues.
Activity
JustinGrote commentedon Jul 24, 2025
Looks reasonable to me, again anything "net-new" that requires 7 support is totally allowed, so long as we inform a 5.1 user with an information message or something that they need to migrate to use that feature.
jborean93 commentedon Jul 24, 2025
Hmm maybe I wasn't clear, 7 would stay unchanged and use the "new" PSRemoting based API which does not care about path validation. This issue is about changing the 5.1 implementation to use reflection to generate the breakpoints and set them as
Set-PSBreakpointis the one doing the validation and could be skipped.JustinGrote commentedon Jul 24, 2025
You probably were clear and I still misinterpreted it :)
That makes sense, and I agree with your point that while this is a private API it could basically be considered stable at this point, I'll defer to Andy Jordan (@andyleejordan) if it gives him the heebee-jeebies. Would it be possible to only use this alternate implementation for remote runspaces in case some edge cases crop up?
jborean93 commentedon Jul 24, 2025
While I would be fine with that it does mean that breakpoints in untitled files still won't work in 5.1 which is probably the main thing people will notice with the change. I could update the script so that if reflection fails to find the constructor it falls back to
Set-PSBreakpointif that helps.