Consider the following background:
clean{}
running.Stop-Process
"jumps straight to kill" without even trying the standardized but unreliable methods of requesting termination like WM_CLOSE
and GenerateConsoleCtrlEvent
as described here.SIGTERM
on other platforms don't succeed at doing the same on Windows. Perhaps this comment most succinctly summarizes what that situation appears to be.Now consider a PowerShell session running some of my code. I can press CTRL+C at the console, the clean{}
blocks are executed, a cascade of Dispose()
calls occurs through any active Cmdlet
's and other object trees, and the process gracefully stops after all that cleanup is complete. I would like to invoke a similar degree of orderly stopping from another process.
How can I achieve that in a manner that is reliable?
As discussed extensively in comments from your own answer, an approach that would work consistently for a PowerShell process (including Jobs) that would allow to invoke their clean
block is via connecting to the process using a NamedPipeConnectionInfo
and calling CloseAsync()
on the process runspaces that are not the DefaultRunspace
.
This seem to work consistently however it's important to note that if the process is invoking a .NET method that doesn't allow interrupts (i.e.: Thread.Sleep(-1)
) then this fails.
using namespace System.Management.Automation
function Stop-PowerShell {
param(
[Parameter(Mandatory, ValueFromPipeline)]
[System.Diagnostics.Process] $Process,
[Parameter()]
[ValidateRange(1, [int]::MaxValue)]
[int] $TimeoutSeconds = 10)
process {
try {
$pipe = [Runspaces.NamedPipeConnectionInfo]::new($Process.Id)
$rs = [runspacefactory]::CreateRunspace($pipe)
$rs.Open()
$ps = [powershell]::Create($rs).AddScript({
Get-Runspace |
Where-Object { $_ -ne [runspace]::DefaultRunspace } |
ForEach-Object CloseAsync
})
$null = $ps.InvokeAsync()
if (-not $Process.WaitForExit([timespan]::FromSeconds($TimeoutSeconds))) {
$PSCmdlet.WriteError([ErrorRecord]::new(
"Failed to stop PowerShell with Process Id '$($Process.Id)'.",
'StopFailed',
[ErrorCategory]::OperationTimeout,
$null))
}
}
catch {
$PSCmdlet.WriteError($_)
}
finally {
${ps}?.Dispose()
${rs}?.Dispose()
}
}
}
A variant of this approach could be via reflection, by GetCurrentlyRunningPipeline()
and StopAsync()
:
$ps = [powershell]::Create($rs).AddScript({
$method = [runspace].GetMethod(
'GetCurrentlyRunningPipeline',
[System.Reflection.BindingFlags] 'NonPublic, Instance')
Get-Runspace |
Where-Object { $_ -ne [runspace]::DefaultRunspace } |
ForEach-Object {
if ($pipeline = $method.Invoke($_, $null)) {
$pipeline.StopAsync()
}
}
})