Powershell function dispose or abort handler

旧巷老猫 提交于 2019-11-28 09:14:54

问题


I have a pipe function that allocates some resources in begin block that need to be disposed at the end. I've tried doing it in the end block but it's not called when function execution is aborted for example by ctrl+c.

How would I modify following code to ensure that $sw is always disposed:

function Out-UnixFile([string] $Path, [switch] $Append) {
    <#
    .SYNOPSIS
    Sends output to a file encoded with UTF-8 without BOM with Unix line endings.
    #>
    begin {
        $encoding = new-object System.Text.UTF8Encoding($false)
        $sw = new-object System.IO.StreamWriter($Path, $Append, $encoding)
        $sw.NewLine = "`n"
    }
    process { $sw.WriteLine($_) }
    # FIXME not called on Ctrl+C
    end { $sw.Close() }
}

EDIT: simplified function


回答1:


Unfortunately, there is no good solution for this. Deterministic cleanup seems to be a glaring omission in PowerShell. It could be as simple as introducing a new cleanup block that is always called regardless of how the pipeline ends, but alas, even version 5 seems to offer nothing new here (it introduces classes, but without cleanup mechanics).

That said, there are some not-so-good solutions. Simplest, if you enumerate over the $input variable rather than use begin/process/end you can use try/finally:

function Out-UnixFile([string] $Path, [switch] $Append) {
    <#
    .SYNOPSIS
    Sends output to a file encoded with UTF-8 without BOM with Unix line endings.
    #>
    $encoding = new-object System.Text.UTF8Encoding($false)
    $sw = $null
    try {
        $sw = new-object System.IO.StreamWriter($Path, $Append, $encoding)
        $sw.NewLine = "`n"
        foreach ($line in $input) {
            $sw.WriteLine($line)
        }
    } finally {
        if ($sw) { $sw.Close() }
    }
}

This has the big drawback that your function will hold up the entire pipeline until everything is available (basically the whole function is treated as a big end block), which is obviously a deal breaker if your function is intended to process lots of input.

The second approach is to stick with begin/process/end and manually process Control-C as input, since this is really the problematic bit. But by no means the only problematic bit, because you also want to handle exceptions in this case -- end is basically useless for purposes of cleanup, since it is only invoked if the entire pipeline is successfully processed. This requires an unholy mix of trap, try/finally and flags:

function Out-UnixFile([string] $Path, [switch] $Append) {
  <#
  .SYNOPSIS
  Sends output to a file encoded with UTF-8 without BOM with Unix line endings.
  #>
  begin {
    $old_treatcontrolcasinput = [console]::TreatControlCAsInput
    [console]::TreatControlCAsInput = $true
    $encoding = new-object System.Text.UTF8Encoding($false)
    $sw = new-object System.IO.StreamWriter($Path, $Append, $encoding)
    $sw.NewLine = "`n"
    $end = {
      [console]::TreatControlCAsInput = $old_treatcontrolcasinput
      $sw.Close()
    }
  }
  process {
    trap {
      &$end
      break
    }
    try {
      if ($break) { break }
      $sw.WriteLine($_)
    } finally {
      if ([console]::KeyAvailable) {
        $key = [console]::ReadKey($true)
        if (
          $key.Modifiers -band [consolemodifiers]"control" -and 
          $key.key -eq "c"
        ) { 
          $break = $true
        }
      }
    }
  }
  end {
    &$end
  }
}

Verbose as it is, this is the shortest "correct" solution I can come up with. It does go through contortions to ensure the Control-C status is restored properly and we never attempt to catch an exception (because PowerShell is bad at rethrowing them); the solution could be slightly simpler if we didn't care about such niceties. I'm not even going to try to make a statement about performance. :-)

If someone has ideas on how to improve this, I'm all ears. Obviously checking for Control-C could be factored out to a function, but beyond that it seems hard to make it simpler (or at least more readable) because we're forced to use the begin/process/end mold.




回答2:


It's possible to write it in C# where one can implement IDisposable - confirmed to be called by powershell in case of ctrl-c.

I'll leave the question open in case someone comes up with some way of doing it in powershell.

using System;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Text;

namespace MarcWi.PowerShell
{
    [Cmdlet(VerbsData.Out, "UnixFile")]
    public class OutUnixFileCommand : PSCmdlet, IDisposable
    {
        [Parameter(Mandatory = true, Position = 0)]
        public string FileName { get; set; }

        [Parameter(ValueFromPipeline = true)]
        public PSObject InputObject { get; set; }

        [Parameter]
        public SwitchParameter Append { get; set; }

        public OutUnixFileCommand()
        {
            InputObject = AutomationNull.Value;
        }

        public void Dispose()
        {
            if (sw != null)
            {
                sw.Close();
                sw = null;
            }
        }

        private StreamWriter sw;

        protected override void BeginProcessing()
        {
            base.BeginProcessing();
            var encoding = new UTF8Encoding(false);
            sw = new StreamWriter(FileName, Append, encoding);
            sw.NewLine = "\n";
        }

        protected override void ProcessRecord()
        {
            sw.WriteLine(InputObject);
        }

        protected override void EndProcessing()
        {
            base.EndProcessing();
            Dispose();
        }
    }
}



回答3:


The following is an implementation of "using" for PowerShell (from Solutionizing .Net). using is a reserved word in PowerShell, hence the alias PSUsing:

function Using-Object {
    param (
        [Parameter(Mandatory = $true)]
        [Object]
        $inputObject = $(throw "The parameter -inputObject is required."),

        [Parameter(Mandatory = $true)]
        [ScriptBlock]
        $scriptBlock
    )

    if ($inputObject -is [string]) {
        if (Test-Path $inputObject) {
            [system.reflection.assembly]::LoadFrom($inputObject)
        } elseif($null -ne (
              new-object System.Reflection.AssemblyName($inputObject)
              ).GetPublicKeyToken()) {
            [system.reflection.assembly]::Load($inputObject)
        } else {
            [system.reflection.assembly]::LoadWithPartialName($inputObject)
        }
    } elseif ($inputObject -is [System.IDisposable] -and $scriptBlock -ne $null) {
        Try {
            &$scriptBlock
        } Finally {
            if ($inputObject -ne $null) {
                $inputObject.Dispose()
            }
            Get-Variable -scope script |
                Where-Object {
                    [object]::ReferenceEquals($_.Value.PSBase, $inputObject.PSBase)
                } |
                Foreach-Object {
                    Remove-Variable $_.Name -scope script
                }
        }
    } else {
        $inputObject
    }
}

New-Alias -Name PSUsing -Value Using-Object

With example usage:

psusing ($stream = new-object System.IO.StreamReader $PSHOME\types.ps1xml) {             
    foreach ($_ in 1..5) { $stream.ReadLine() }             
}

Obviously this is really just some packaging around Jeroen's first answer but may be useful for others who find their way here.



来源:https://stackoverflow.com/questions/28522507/powershell-function-dispose-or-abort-handler

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!