如何确保在停止信号时对高级函数的局部变量调用 Dispose()?

How can I ensure Dispose() is called on an advanced function's local variable on stop signal?

我注意到当 "stop" 信号(例如按 CTRL+C) 在执行期间发送。当对象持有一个文件的句柄时,这会很痛苦。如果在不合时宜的时间收到停止信号,句柄不会关闭,文件将保持锁定状态,直到 PowerShell 会话关闭。

考虑以下 class 和函数:

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' }
}

假设 $f 持有一个文件句柄,并在其 Dispose() 方法被调用时释放它。我的目标是 $f 的生命周期与 g 的生命周期相匹配。 $f 通过以下方式调用 g 时正确处理:

g
'o' | g
'o' | g | throws

我能说的差不多,因为每个输出都 disposed

当在g的下游执行时发送停止信号,但是,$f不被处理。为了测试这一点,我调用了

'o' | g | blocks

which blocks at the Wait-Event inside blocks, then I pressed Ctrl+C停止执行.在那种情况下,Dispose() 似乎没有被调用(或者,至少 disposed 没有写入控制台)。

在此类函数的 C# 实现中,我的理解是 StopProcessing() 在停止信号上被调用以进行此类清理。但是,似乎没有可用于高级功能的 PowerShell 实现的 StopProcessing 模拟。

如何确保 $f 在所有情况下都得到处理,包括停止信号?

看来您只需添加一个 finally{} 块并将其放置在那里即可。您可能还想考虑设置 ErrorActionPreference,因为您正在进行自定义错误处理。

$ErrorActionPreference = 'SilentlyContinue'

try
{
    try
    {
        1/0
    }
    catch
    {
        throw New-Object System.Exception("Exception!")
    }
    finally
    {
        "You can dispose here!"
    }
}
catch
{
    $_.Exception.Message | Write-Output
}

如果函数接受管道输入,则不能。

如果函数接受管道输入,我认为实现这一点的可靠方法是不可能的。原因是当代码在管道上游执行时,可能会发生以下任何情况:

  • breakcontinuethrow
  • 终止错误
  • 收到停止信号

当这些发生在上游时,功能的任何部分都不会被干预。 begin{}process{} 块要么 运行 完成,要么根本没有 运行,end{} 块可能是也可能不是 运行。我找到的最接近 on-point 的解决方案如下:

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()}
}

但是,根据评论,仍然存在未调用 $f.Dispose() 的情况。您可以单步执行 this working example that includes such cases.

改为考虑 usingObject {} 这样的模式。

如果我们将使用限制在负责清理的函数不接受管道输入的情况下,那么我们可以将 lifetime-management 逻辑提取到类似于 C# 的 using 块的辅助函数中。 Here is a proof-of-concept that implements such a helper function called usingObject. 这是一个示例,说明如何在使用 usingObject 实现 .Dispose():

的稳健调用时大大简化 g
# 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
}