This question is about reliability of Dispose() being called from advanced functions in distributions of PowerShell (like Windows PowerShell 5.1) without the clean
block. This is distinct from related questions as follows:
clean
became available in PowerShell core and is not focused on reliability of calling Dispose() without clean
. Note, however, that this answer to that question does contain an answer suggesting the use of a binary cmdlet. That might be a workaround for this question, but does not answer the question for advanced functions which is the subject of this question.I have noticed that objects implementing IDisposable
in advanced functions aren't reliably disposed of when a "stop" signal (eg. pressing CTRL+C) is sent during execution. This is a pain when the object holds a handle to, for example, a file. If the stop signal is received at an inopportune time, the handle doesn't get closed and the file remains locked until the PowerShell session is closed.
Consider the following class and functions:
class f : System.IDisposable {
Dispose() { Write-Host 'disposed' }
}
function g {
param( [Parameter(ValueFromPipeline)]$InputObject )
begin { $f = [f]::new() }
process {
try {
$InputObject
}
catch {
$f.Dispose()
throw
}
}
end {$f.Dispose()}
}
function throws {
param ( [Parameter(ValueFromPipeline)] $InputObject )
process { throw 'something' }
}
function blocks {
param ( [Parameter(ValueFromPipeline)] $InputObject )
process { Wait-Event 'bogus' }
}
Imagine $f
holds a handle to a file and releases it when its Dispose()
method is called. My goal is that the lifetime of $f
matches the lifetime of g
. $f
is disposed correctly when g
is invoked in each the following ways:
g
'o' | g
'o' | g | throws
I can tell as much because each of these outputs disposed
.
When the stop signal is sent while execution is occuring downstream of g
, however, $f
is not disposed. To test that, I invoked
'o' | g | blocks
which blocks at the Wait-Event
inside blocks
, then I pressed Ctrl+C to stop execution. In that case, Dispose()
does not seem to get called (or, at least disposed
is not written to the console).
In C# implementations of such functions it is my understanding that StopProcessing()
gets called on a stop signal to do such cleanup. However, there seems to be no analog to StopProcessing
available for PowerShell implementations of advanced functions.
How can I ensure that $f
is disposed in all cases including a stop signal?
I don't think a robust way of achieving this is possible if the function accepts pipeline input. The reason is that any of the following could occur while code is executing upstream in the pipeline:
break
, continue
, or throw
When these occur upstream, no part of the function can be caused to intervene. The begin{}
and process{}
blocks have either run to completion or not run at all, and the end{}
block may or may not be run. The closest to an on-point solution I have found is the following:
function g {
param (
[Parameter(ValueFromPipeline)]
$InputObject
)
begin { $f = [f]::new() } # The local IDisposable is created when the pipeline is established.
process {
try
{
# flags to keep track of why finally was run
$success = $false
$caught = $false
$InputObject # output an object to exercise the pipeline downstream
# if we get here, nothing unusual happened downstream
$success = $true
}
catch
{
# we get here if an exception was thrown
$caught = $true
# !!!
# This is bad news. It's possible the exception will be
# handled by an upstream process{} block. The pipeline would
# survive and the next invocation of process{} would occur
# after $f is disposed.
# !!!
$f.Dispose()
# rethrow the exception
throw
}
finally
{
# !!!
# This finally block is not invoked when the PowerShell instance receives
# a stop signal while executing code upstream in the pipeline. In that
# situation cleanup $f.Dispose() is not invoked.
# !!!
if ( -not $success -and -not $caught )
{
# dispose only if finally{} is the only block remaining to run
$f.Dispose()
}
}
}
end {$f.Dispose()}
}
However, per the comments there are still cases where $f.Dispose()
is not invoked. You can step through this working example that includes such cases.
usingObject {}
instead.If we limit usage to the case where the function responsible for cleanup does not accept pipeline input, then we can factor out the lifetime-management logic into a helper function similar to C#'s using
block. Here is a proof-of-concept that implements such a helper function called usingObject
. This is an example of how g
could be substantially simplified when using usingObject
to achieve robust invokation of .Dispose()
:
# refactored function g
function g {
param (
[Parameter(ValueFromPipeline)]
$InputObject,
[Parameter(Mandatory)]
[f]
$f
)
process {
$InputObject
}
}
# usages of function g
usingObject { [f]::new() } {
g -f $_
}
usingObject { [f]::new() } {
'o' | g -f $_
}
try
{
usingObject { [f]::new() } {
'o' | g -f $_ | throws
}
}
catch {}
usingObject { [f]::new() } {
'o' | g -f $_ | blocks
}