Skip to content

Ignore file path validation on Set-PSBreakpoint for attach scenarios #2243

@jborean93

Description

@jborean93

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.ps1

In 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.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-EnhancementA feature request (enhancement).Needs: TriageMaintainer attention needed!

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions