代码之家  ›  专栏  ›  技术社区  ›  zett42

如何确保可分级管道使用的文件即使在出现异常的情况下也已关闭?

  •  2
  • zett42  · 技术社区  · 1 年前

    这是一个 自我回答的 问题我只想为添加一个答案 PowerShell 7.3或更新版本 。请随意添加任何以前PowerShell版本的答案。


    我有一个自定义的日志记录函数,它接受管道输入并使用 可分级管道 对象以正确地将其输入转发到 Out-File This answer 提供了为什么在这种情况下需要可分级管道的背景。另请参阅博客文章 Mastering the (steppable) pipeline

    当我的日志记录功能所属的管道的任何命令, 引发异常 (又名 脚本终止错误 )并且捕获了异常,打开的文件由 输出文件 不会被关闭,也不会有任何后续的向文件添加内容的尝试(例如使用 Add-Content )失败,并显示错误消息:

    进程无法访问文件“C:\LogFile.log”,因为它是 被另一个过程使用。

    这是一个 可重复的例子 :

    $LOG_FILE_PATH = Join-Path $PSScriptRoot LogFile.log
    
    Function Test-SteppablePipeline {
    
        [CmdletBinding()]
        param (
            [Parameter( Mandatory, ValueFromPipeline )]
            [string] $InputObject
        )
    
        begin {
            Write-Host '[begin]'
    
            $steppablePipeline = {
                Out-File -LiteralPath $LOG_FILE_PATH
            }.GetSteppablePipeline( $MyInvocation.CommandOrigin )
    
            $steppablePipeline.Begin( $true )
        }
    
        process {
            Write-Host '[process]'
            
            $steppablePipeline.Process( $InputObject )
    
            $InputObject  # Forward to next command in pipeline of caller
        }
    
        end {
            Write-Host '[end]'
    
            $steppablePipeline.End()
        }
    }
    
    try {
        'foo', 'bar' | 
            ForEach-Object { throw 'my error' } |
            Test-SteppablePipeline
    }
    catch {
        '[catch]'
    }
    
    Write-Host '[add]'
    Add-Content -LiteralPath $LOG_FILE_PATH -Value 'baz'
    

    输出:

    [begin]
    [process]
    [catch]
    [add]
    Add-Content: C:\Test.ps1:48
    Line |
      48 |  Add-Content -LiteralPath $LOG_FILE_PATH -Value 'baz'
         |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         | The process cannot access the file 'C:\LogFile.log' because it is being used by another process.
    

    正如你所看到的 [messages] 这个 end 的块 Test-SteppablePipeline 由于异常而未被调用,这很可能是文件未关闭的原因。

    1 回复  |  直到 1 年前
        1
  •  2
  •   Santiago Squarzon    1 年前

    值得一提的是: 这不是的问题 只是 可分级的管道,但对于任何获得的PowerShell函数来说都是一个问题 handle 并且需要确保所述手柄在离开之前被适当地布置。


    这是PowerShell 7.3中解决的一个大问题 clean 阻止为 zett42 points out in his helpful answer 。对于任何想知道如何在PowerShell 7.2和以前的版本中解决此问题的人来说,答案是, 仅靠PowerShell无法解决 ,解决方案是创建一个二进制cmdlet,该cmdlet实现 IDisposable interface ,发动机将完成其余工作(确保 .Dispose() 被称为不变量,包括 CTRL+C 或者终止错误)。

    使用二进制cmdlet对zett42的代码进行最小限度的重新编程。注意,为了演示,这是使用 Add-Type 和用于在运行时编译的内联C#,理想情况下,此cmdlet将被预编译。

    Add-Type '
    using System;
    using System.Management.Automation;
    
    namespace Test
    {
        [Cmdlet(VerbsDiagnostic.Test, "SteppablePipeline")]
        public sealed class TestSteppablePipelineCommand : PSCmdlet, IDisposable
        {
            private SteppablePipeline _pipe;
    
            [Parameter(Mandatory = true, ValueFromPipeline = true)]
            public string InputObject { get; set; }
    
            protected override void BeginProcessing()
            {
                WriteInformation("[begin]", null);
                _pipe = ScriptBlock.Create("Out-File -LiteralPath $LOG_FILE_PATH")
                    .GetSteppablePipeline(MyInvocation.CommandOrigin);
                _pipe.Begin(true);
            }
    
            protected override void ProcessRecord()
            {
                WriteInformation("[process]", null);
                _pipe.Process(InputObject);
                WriteObject(InputObject, enumerateCollection: true);
            }
    
            protected override void EndProcessing()
            {
                WriteInformation("[end]", null);
                _pipe.End();
            }
    
            public void Dispose()
            {
                _pipe.Dispose();
            }
        }
    }' -PassThru -IgnoreWarnings -WA 0 | Import-Module -Assembly { $_.Assembly }
    
    $LOG_FILE_PATH = Join-Path $pwd.Path LogFile.log
    
    try {
        0..10 |
            Test-SteppablePipeline -InformationAction Continue |
            ForEach-Object {
                if ($_ -eq 5) {
                    throw 'my error'
                }
            }
    }
    catch {
        '[catch]'
    }
    
    Write-Host '[add]'
    Add-Content -LiteralPath $LOG_FILE_PATH -Value 'baz'
    Get-Content $LOG_FILE_PATH
    
        2
  •  2
  •   zett42    1 年前

    的解决方案 PowerShell 7.3或更新版本 是微不足道的。PowerShell 7.3添加了 clean 阻止到 先进的 功能。引用文件:

    这个 清洁的 阻止是用户清理资源的一种方便方式 跨越 begin , process end 块。它在语义上 类似于 finally 覆盖的所有其他命名块的块 脚本函数或脚本cmdlet。强制为执行资源清理 以下场景:

    1. 当管道执行正常完成而没有终止错误时
    2. 当管道执行由于终止错误而中断时
    3. 当管道被停止时 Select-Object -First
    4. 当管道被停止时 Ctrl+c StopProcessing()

    使用时 可分级管道 ,您只需调用 .Clean() 从中的可步进管道对象的方法 清洁的 使用可分级管道的函数块:

    $LOG_FILE_PATH = Join-Path $PSScriptRoot LogFile.log
    
    Function Test-SteppablePipeline {
    
        [CmdletBinding()]
        param (
            [Parameter( Mandatory, ValueFromPipeline )]
            [string] $InputObject
        )
    
        begin {
            $steppablePipeline = {
                Out-File -LiteralPath $LOG_FILE_PATH
            }.GetSteppablePipeline( $MyInvocation.CommandOrigin )
    
            $steppablePipeline.Begin( $true )
        }
    
        process {
            $steppablePipeline.Process( $InputObject )
    
            $InputObject  # Forward to next command in pipeline of caller
        }
    
        end {
            $steppablePipeline.End()
        }
    
        clean {
            # Makes sure the resources (e. g. files) allocated by the steppable pipeline 
            # are freed even in case of exceptions (script-terminating errors).
            $steppablePipeline.Clean()
        }
    }