#How to Run Code in a PSHostProcess Programmatically

76 messages · Page 1 of 1 (latest)

near kraken
#

I'm not sure if this is even possible, as everything I've found online seems to suggest it isn't. So, asking here is my last hope.

Is it possible to run code in a PSHostProcess in a similar way to how one can with Invoke-Command using the -Session or -ComputerName parameters? Of course, there’s Enter-PSHostProcess, but I’d like to do this in the middle of a script, not manually on the command line.

The reason I want to do this is so I can write a helper script that iterates through all the currently running pwsh processes. For each one, it would use Get-Runspace to find any runspaces currently in an InBreakpoint state, and then use Debug-Runspace to start the debugger.
The helper script could also check if multiple runspaces are in the InBreakpoint state and prompt for the correct one to debug.

Thanks in advance.

unkempt falcon
#

will also likely need to set up forwarding events to get debugging back to the interactive runspace

#

might be able to do PSHost.PushRunspace and then run Debug-Runspace

near kraken
#

create a runspace with NamedPipeConnectionInfo and utilize the SMA.PowerShell class with that runspace
Would this be the runspace that I'm trying to debug? If so the issue there is that I do not create the runsapce its created by a module I'm using .

unkempt falcon
near kraken
#

ok thanks I wll take a look at it .

unkempt falcon
wind quarry
#

If using pwsh 7.3+, you can create a PSSession from a NamedPipeConnectionInfo using public APIs. Older ones need to use reflection to achieve the same thing though.

$connInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($targetPid)
$runspace = [RunspaceFactory]::CreateRunspace($connInfo, $Host, $null)
$runspace.Open()
$session = [System.Management.Automation.Runspaces.PSSession]::Create($runspace, '', $null)
Invoke-Command -Session $session -ScriptBlock { $pid }
$session | Remove-PSSession
$runspace.Dispose()
unkempt falcon
#

curious, since you're already forced to hand make the runspace, does Invoke-Command get you much more than SMA.PowerShell?

wind quarry
#

Otherwise older versions can do this bit of reflection to create the PSSession object

$remoteRunspaceType = [PSObject].Assembly.GetType('System.Management.Automation.RemoteRunspace')
$pssessionCstr = [System.Management.Automation.Runspaces.PSSession].GetConstructor(
    'NonPublic, Instance',
    $null,
    [type[]]@($remoteRunspaceType),
    $null)
$session = $pssessionCstr.Invoke(@($runspace))
wind quarry
unkempt falcon
#

gotcha, makes sense. Do you know if it handles forwarding Debug-Runspace?

wind quarry
#

as in you want to debug what's in Invoke-Command?

unkempt falcon
#

they're trying to debug a background runspace in a different process, so presumably they'd want to do icm { Get-Runspace | ? in breakpoint psuedo code | Debug-Runspace }

wind quarry
#

ah ok, If you created the runspace with the $Host like in that example I would have thought there's a chance but I'm never tested it. Especially since debugging is more of an interactive thing so I would have used Enter-PSHostProcess

unkempt falcon
#

I would expect it needs PushRunspace to be called somewhere but I'm not 100% on that. Hell you might be able to just do PushRunspace and then icm

#

(or maybe it all just works, no idea)

near kraken
#

I am using PowerShell 7.5.4 so the approach from @wind quarry does allow we to run code in the PSHostProcess and I can see the runspace that InBreakpoint: True.
Thank you I am one step closer.

#

So the code below will get me to the debugger but as @unkempt falcon is suggesting I'm unable to interact with the debugger and it just hangs.

$connInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]::new(25309)
$runspace = [RunspaceFactory]::CreateRunspace($connInfo, $Host, $null)
$runspace.Open()
$session = [System.Management.Automation.Runspaces.PSSession]::Create($runspace, '', $null)
Invoke-Command -Session $session -ScriptBlock { Get-Runspace | Where-Object { $_.Debugger.InBreakpoint } | Select-Object -First 1 | Debug-Runspace -Confirm:$false }
wind quarry
#

Yea this is a tricky one as I think you need to PushRunspace but I'm unsure how to actually get that working inside a script and inside Invoke-Command

near kraken
#

Yeah, I have just found the PushRunspace Method and going to some testing now.

wind quarry
#

there are some weird rules with PushRunspace, it's hard to get right in a script and is the reason why Enter-PSSession only works interactively

near kraken
#

yeah PushRunspace is not happy as the runspace is Local rather than Remote.

Invoke-Command -Session $session -ScriptBlock { 
    $rs = Get-Runspace | Where-Object { $_.Debugger.InBreakpoint } | Select-Object -First 1
    $host.PushRunspace($rs)
    $rs | Debug-Runspace -Confirm:$false 
}
MethodInvocationException: Exception calling "PushRunspace" with "1" argument(s): "Cannot enter Runspace because it is not a remote Runspace."
unkempt falcon
#

sorry to be clear you want to push the runspace you are creating, not the one you want to debug

#

outside of the Invoke-Command

near kraken
#

ok thanks. I just tried pushing the created runspace but its giving differernt errors depending on where I put it.
I will keep doing some testing.

unkempt falcon
#

it might actually not be possible without rolling your own host

#

biggest problem is getting the host to understand the situation it's in. You need it to be able to accept the debugger stop event and enter a nested prompt targeting that runspace, while also having the main pipeline thread free

#

here's the closest I got

if (-not $targetPid) { throw }
$connInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($targetPid)
$runspace = [RunspaceFactory]::CreateRunspace($connInfo, $Host, $null)
$ps = [powershell]::Create()
$runspace.GetType().
    GetProperty('ShouldCloseOnPop', 60).GetSetMethod($true).
    Invoke($runspace, @($true))

$runspace.Name = 'PSAttachRunspace'
$runspace.Open()
$runspace.Debugger.SetDebugMode('LocalScript, RemoteScript')
$ps.Runspace = $runspace
$result = $ps.AddScript{
    Get-Runspace |
        ? { $_.Debugger.InBreakpoint } |
        Select-Object -First 1 -ExpandProperty Id
}.Invoke()

$ps.Commands.Clear()
$ps.Streams.ClearStreams()

if (-not $result) {
    return
}

$Host.PushRunspace($runspace)
$iar = $ps.AddCommand('Debug-Runspace').
    AddParameter('BreakAll').
    AddArgument($result[0]).
    BeginInvoke()

if you change to Invoke() it doesn't continue the REPL, and as is it tries to do both repls at the same time I think?

#

and really even with a custom host it's probably super annoying to set up. The code in the vscode extension for instance, is very complicated

wind quarry
#

curious, what happens if you do Debug-Runspace in the first $ps with BeginInvoke() and then do $Host.PushRunspace($runspace) (after some small sleep)

unkempt falcon
#

pretty sure the events won't be hooked up when fired so likely nothing, but I can try

wind quarry
#

looks like the task just completes anyway

#

because it has pushed the runspace but then nothing else was there to do

#

It is a tricky one, I suppose PSES gets away with it because it literally types in the Debug-Runspace in the repl after it has run Enter-PSHostProcess

unkempt falcon
#

it's not typing it, it's just a custom host so it can handle the events itself

#

(I know it kinda looks like it is typing it, or at least I assume it does, can't remember. But check the syntax highlighting, if it has none then we just made it look like it was typed)

wind quarry
#

Which means you could theoretically get it done by driving PSES in a debug launch scenario

Start-DebugAttachSession -ProcessId $targetPid -RunspaceId $targetRunspaceId
unkempt falcon
#

yep! that likely works

#

the ConsoleHost repl is incredibly finicky. If you do anything outside of what it was designed to do, things get weird. PSES has to account for that because of UI buttons and what not, many things can trigger debugging, not just what is currently evaluating

wind quarry
#

I do recall an annoying thing with Start-DebugAttachSession is that the existing launch session must still he running so you need some sleep or a way to wait for the attach session to end

unkempt falcon
#

I imagine if the runspace is already blocking as InBreakpoint it should be fine. I think that's mainly an issue if you're trying to target the REPL runspace

#

well, REPL or primary runspace in an unattended process

wind quarry
#

It was more a weird quirk with DAP or something. I honestly can't remember if that was a problem during my initial implementation, still there, or something else

#

my main use case had a background waiting mechanism anyway, it was just something I remember during testing

unkempt falcon
#

you aren't talking about the whole waiting for attach thing?

wind quarry
#

I don't think so, let me try it out

unkempt falcon
#

ah gotcha, I thought you were referring to how the main runspace would need the remote debugger attached before the debugger stop occurs (otherwise it just enters a REPL in that console window)

wind quarry
#

yea it exits as soon as the launch debug session that calls Start-DebugAttachSession ends (after the attach happens)

unkempt falcon
#

oh I got you, I assumed you would just call Start-DebugAttachSession interactively

wind quarry
#

checks if that works

#

nah, you need to be in a launched session

unkempt falcon
#

(note I don't actually know what Start-DebugAttachSession is, I assumed it was just a recently added analog to manually starting debug sessions)

wind quarry
#

It exposes startDebugging. I added it to support Ansible's remote debugging feature

#

basically I implemented it to support a pseudo listen like functionality for the debugger

unkempt falcon
#

anyway tl;dr: you probably need to implement your own proxy PSHost that mostly forwards to ConsoleHost but also is the main handler of debug events for the duration of your command. Would be a pretty sizable undertaking and troubleshooting would likely feel non-sensical

#

oh and annoyingly, a middle ground that would sadly be absurdly easier, instead of calling Debug-Runspace just do everything up to the Debug-Runspace call and have it define a short function like d that runs Debug-Runspace. So you do Enter-DebuggableRunspace and then when it connects just d and it'll be pretty much what you want

#

actually I can't get that working either, no idea why

#

definitely missing something small

wind quarry
#
function Enter-DebuggableRunspace {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [int]
        $ProcessId,

        [Parameter()]
        [int]
        $RunspaceId = 1
    )

    $connInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($ProcessId)
    $runspace = [RunspaceFactory]::CreateRunspace($connInfo, $Host, $null)
    $runspace.GetType().
        GetProperty('ShouldCloseOnPop', 60).GetSetMethod($true).
        Invoke($runspace, @($true))
    $runspace.Open()

    $ps = [powershell]::Create()
    $ps.Runspace = $runspace
    $null = $ps.AddScript("function d { Debug-Runspace -Id $RunspaceId }")
    $null = $ps.Invoke()

    $Host.UI.WriteLine(
        "Yellow",
        $Host.UI.RawUI.BackgroundColor,
        "Entering process $ProcessId, press d to start debugging")
    $Host.PushRunspace($runspace)
}

This seems to be close but the debug session never actually enters a prompt even if the target steps through a statement after the attach

unkempt falcon
#

type table is apparently the main missing thing. I got it working completely

#

if (-not $targetPid) { throw }
$connInfo = [System.Management.Automation.Runspaces.NamedPipeConnectionInfo]::new($targetPid)
$runspace = [RunspaceFactory]::CreateRunspace(
    $connInfo,
    $Host,
    [System.Management.Automation.Runspaces.TypeTable]::LoadDefaultTypeFiles())

$ps = [powershell]::Create()
$runspace.GetType().
    GetProperty('ShouldCloseOnPop', 60).GetSetMethod($true).
    Invoke($runspace, @($true))

$runspace.Name = 'PSAttachRunspace'
$runspace.Open()
$runspace.Debugger.SetDebugMode('LocalScript, RemoteScript')
$ps.Runspace = $runspace
$result = $ps.AddScript{
    Get-Runspace |
        ? { $_.Debugger.InBreakpoint } |
        Select-Object -First 1 -ExpandProperty Id
}.Invoke()

$ps.Commands.Clear()
$ps.Streams.ClearStreams()

if (-not $result) {
    return
}

$Host.PushRunspace($runspace)
$iar = $ps.AddCommand('Debug-Runspace').
    AddParameter('BreakAll').
    AddArgument($result[0]).
    BeginInvoke()
try {
    while ($true) {
        Start-Sleep -Milliseconds 200
    }
} finally {
    $null = $ps.BeginStop($null, $null)
    $Host.PopRunspace()
}
#

needs better handling of a lot of things but appears to work

wind quarry
#

I wonder why the type table is needed

unkempt falcon
#

no idea, but that's the whole reason it was skipping the REPL. Very strange

#

it was the last difference between what I was doing and what the command does lol

#

also the weird while loop at the end, is because otherwise you can't ctrl c out

#

the finally does appear to correctly stop and pop

wind quarry
#

probably can remove the reflection bit as well now that you are manually popping

unkempt falcon
#

yeah assuming you also dispose, good point

near kraken
#

Thanks for all the effort you both put in. A lot of your conversation went over my head, but it was interesting to read through. I had to step away last night while you were working on the problem, but I did try both of your solutions this evening. However, when running them from the command line, the interactive debugger didn’t appear. Maybe I'm calling them in the wrong way.

There are two things on my end that might be complicating this:
I’m running Linux, and
The runspace I want to debug is created as part of the Pode web framework, which might be doing something I’m not aware of with the runspaces it creates.

One unexpected thing that did happen was when I ran seeminglyscience’s solution from the PowerShell Extension shell in VS Code,it actually allowed me to use the VS Code debugger to step through the code.

I think I'll have to test using a Runspace outside of my Pode project to see if i can get it to work their first. I can also test the solution from my Windows machine to discount any Linux issues.

unkempt falcon
#

yeah any time the extension terminal enters debugging it should seamlessly handle it in vscode

#

it's very possible pode might be doing something differently. I tested with Start-ThreadJob

#

but tbh that shouldn't matter. Neither should OS I don't think

near kraken
#

Yeah that is evident from what I saw in VSCode. That in of itself will be very useful for me when debugging the web app. I'm very much one for using a debugger Vs write-output debugging.