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
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.
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();
}
}
}
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.
Related
I have a scenario where I am doing any number of things with a network resource, copying files or folders, deleting a ZIP file after it has been unzipped locally, checking an EXE to see if it was downloaded from the internet and needs unblocked, running a software install, etc. And all of these tasks are impacted by things like an installer that hasn't released a file lock on a log file to be deleted, or the unzip hasn't released the file lock on the zip file, or the network is for a fleeting moment "unavailable" so a network file is not found.
I have a technique that works well for handling this scenario, I do a loop and react to the specific exception, and when that exception occurs I just sleep for 6 seconds. The loop happens up to 10 times before I abandon. Here is the code for dealing with an Autodesk log that is still locked by the Autodesk installer.
$waitRetryCount = 10
:waitForAccess do {
try {
Remove-Item $odisLogPath -Recurse -errorAction:Stop
$retry = $False
} catch {
if (($PSItem.Exception -is [System.IO.IOException]) -or ($PSItem.Exception.InnerException -and ($PSItem.Exception.InnerException -is [System.IO.IOException]))) {
if ($waitRetryCount -eq 0) {
$invokeODISLogManagement.log.Add('E_Error deleting ODIS logs: retry limit exceeded')
break waitForFolderAccess
} else {
$retry = $True
$waitRetryCount --
Start-Sleep -s:6
}
} else {
$invokeODISLogManagement.log.Add('E_Error deleting ODIS logs')
$invokeODISLogManagement.log.Add("=_$($PSItem.Exception.GetType().FullName)")
$invokeODISLogManagement.log.Add("=_$($PSItem.Exception.Message)")
if ($PSItem.Exception.InnerException) {
$invokeODISLogManagement.log.Add("=_$($PSItem.Exception.InnerException.GetType().FullName)")
$invokeODISLogManagement.log.Add("=_$($PSItem.Exception.InnerException.Message)")
}
$retry = $False
}
}
} while ($retry)
The thing is, I would like to convert this to a function, since it needs to be handled in a lot of places. So I would need to pass to the function the specific exception I am looking for and the code to be run in the try block, and get back a log (as a generic.list) that I can then add to the actual log list. The first and last aspects I have, but I am unsure the best approach for the code to try. In the above example it is a single line, Remove-Item $odisLogPath -Recurse -errorAction:Stop, but it could be multiple lines I suspect.
To start playing with this I verified that this does seem to work, at least with a single line of code.
$code = {Get-Item '\\noServer\folder\file.txt' -errorAction:Stop}
try {
& $code
} catch {
Write-Host "$($_.Exception.GetType().FullName)"
}
But the error action is going to be duplicated a lot, so I thought to maybe address that within the function, however
$code = {Get-Item '\noServer\folder\file.txt'}
try {
& $code -errorAction:Stop
} catch {
Write-Host "$($_.Exception.GetType().FullName)"
}
does NOT work. I get the exception uncaught.
So, my questions are
1: Is this the right direction? I am pretty sure it is but perhaps someone has a gotcha that I am not seeing, yet. :)
2: Is there a mechanism to add the -errorAction:Stop in the try, so I don't need to do it/remember to do it, at every use of this new function.
3: I seem to remember reading about a programming concept of passing code to a function, and I can't remember what that is called, but I would like to know the generic term. Indeed, it probably would help if I could tag it for this post. I had thought it might be lama, but a quick search suggests that is not the case? Being self taught sucks sometimes.
EDIT:
I have now implemented a function, that starts to do what I want.
function Invoke-PxWaitForAccess {
param (
[System.Management.Automation.ScriptBlock]$code,
[String]$path
)
try {
(& $code -path $path)
} catch {
Return "$($_.Exception.GetType().FullName)!!"
}
}
$path = '\\noServer\folder\file.txt'
$code = {param ([String]$path) Write-Host "$path!"; Get-Item $path}
Invoke-PxWaitForAccess -code $code -path $path
I do wonder if the path couldn't somehow be encapsulated in the $code variable itself, since this implementation means it can ONLY be used where the code being run has a single variable called $path.
And, still wondering if this really is the best, or even a good, way to proceed? Or are there arguments for just implementing my loop 50 some odd times in all the situations where I need this behavior.
Also worth noting that this code does not yet implement the loop or address the fact that different exceptions apply in different situations.
EDIT #2:
And here is a more complete implementation, though it fails because it seems I am not actually passing a type, even though it looks like I am. So I get an error because what is to the right of -is must be an actual type.
function Invoke-PxWaitForAccess {
param (
[System.Management.Automation.ScriptBlock]$code,
[String]$path,
[Type]$exceptionType
)
$invokeWaitForAccess = #{
success = $Null
log = [System.Collections.Generic.List[String]]::new()
}
$waitRetryCount = 2
:waitForAccess do {
try {
Write-Host "$path ($waitRetryCount)"
& $code -path $path
$retry = $False
} catch {
Write-Host "!$($PSItem.Exception.GetType().FullName)"
if (($PSItem.Exception -is $exceptionType) -or ($PSItem.Exception.InnerException -and ($PSItem.Exception.InnerException -is $exceptionType))) {
Write-Host "($waitRetryCount)"
if ($waitRetryCount -eq 0) {
$invokeWaitForAccess.log.Add('E_retry limit exceeded')
break waitForFolderAccess
} else {
$retry = $True
$waitRetryCount --
Start-Sleep -s:6
}
} else {
$invokeWaitForAccess.log.Add("=_$($PSItem.Exception.GetType().FullName)")
$invokeWaitForAccess.log.Add("=_$($PSItem.Exception.Message)")
if ($PSItem.Exception.InnerException) {
$invokeWaitForAccess.log.Add("=_$($PSItem.Exception.InnerException.GetType().FullName)")
$invokeWaitForAccess.log.Add("=_$($PSItem.Exception.InnerException.Message)")
}
$retry = $False
}
}
} while ($retry)
if ($invokeWaitForAccess.log.Count -eq 0) {
$invokeWaitForAccess.success = $True
} else {
$invokeWaitForAccess.success = $False
}
return $invokeWaitForAccess
}
$path = '\\noServer\folder\file.txt'
$code = {param ([String]$path) Get-Item $path -errorAction:Stop}
if ($invoke = (Invoke-PxWaitForAccess -code $code -path $path -type ([System.Management.Automation.ItemNotFoundException])).success) {
Write-Host 'Good'
} else {
foreach ($line in $invoke.log) {
Write-Host "$line"
}
}
EDIT #3: This is what I have now, and it seems to work fine. But the code I am passing will sometimes be something like Remove-Object and the error is [System.IO.IOException], but at other times I actually need to return more than an error, like here where the code involves Get-Item. And that means defining the code block outside the function with a reference to the variable inside the function, which seems, fugly, to me. It may be that what I am trying to do is just more complicated than PowerShell is really designed to handle, but it seems MUCH more likely that there is a more elegant way to do what I am trying to do? Without being able to manipulate the script block from within the function I don't see any good options.
For what it is worth this last example shows a failure where the exception I am accepting for the repeat occurs and hits the limit, as well as an exception that just immediately fails because it is not the exception I am looping on, and an example where I return something. A fourth condition would be when I am trying to delete, and waiting on [System.IO.IOException] and a success would return nothing, no item, and no error log.
function Invoke-PxWaitForAccess {
param (
[System.Management.Automation.ScriptBlock]$code,
[String]$path,
[Type]$exceptionType
)
$invokeWaitForAccess = #{
item = $null
errorLog = [System.Collections.Generic.List[String]]::new()
}
$waitRetryCount = 2
:waitForSuccess do {
try {
& $code -path $path
$retry = $False
} catch {
if (($PSItem.Exception -is $exceptionType) -or ($PSItem.Exception.InnerException -and ($PSItem.Exception.InnerException -is $exceptionType))) {
if ($waitRetryCount -eq 0) {
$invokeWaitForAccess.errorLog.Add('E_Retry limit exceeded')
break waitForSuccess
} else {
$retry = $True
$waitRetryCount --
Start-Sleep -s:6
}
} else {
$invokeWaitForAccess.errorLog.Add("=_$($PSItem.Exception.GetType().FullName)")
$invokeWaitForAccess.errorLog.Add("=_$($PSItem.Exception.Message)")
if ($PSItem.Exception.InnerException) {
$invokeWaitForAccess.errorLog.Add("=_$($PSItem.Exception.InnerException.GetType().FullName)")
$invokeWaitForAccess.errorLog.Add("=_$($PSItem.Exception.InnerException.Message)")
}
$retry = $False
}
}
} while ($retry)
return $invokeWaitForAccess
}
CLS
$path = '\\noServer\folder\file.txt'
$code = {param ([String]$path) Get-Item $path -errorAction:Stop}
$invoke = (Invoke-PxWaitForAccess -code $code -path $path -exceptionType:([System.Management.Automation.ItemNotFoundException]))
if ($invoke.errorLog.count -eq 0) {
Write-Host "Good $path"
} else {
foreach ($line in $invoke.errorLog) {
Write-Host "$line"
}
}
Write-Host
$path = '\\noServer\folder\file.txt'
$code = {param ([String]$path) Get-Item $path -errorAction:Stop}
$invoke = (Invoke-PxWaitForAccess -code $code -path $path -exceptionType:([System.IO.IOException]))
if ($invoke.errorLog.count -eq 0) {
Write-Host "Good $path"
} else {
foreach ($line in $invoke.errorLog) {
Write-Host "$line"
}
}
Write-Host
$path = '\\Mac\iCloud Drive\Px Tools 3.#\# Dev 3.4.5\Definitions.xml'
$code = {param ([String]$path) $invokeWaitForAccess.item = Get-Item $path -errorAction:Stop}
$invoke = (Invoke-PxWaitForAccess -code $code -path $path -exceptionType:([System.Management.Automation.ItemNotFoundException]))
if ($invoke.errorLog.count -eq 0) {
Write-Host "Good $path !"
Write-Host "$($invoke.item)"
} else {
foreach ($line in $invoke.errorLog) {
Write-Host "$line"
}
}
Write-Host
Considering the below Powershell code, is there a way to mock $host.ui.PromptForChoice without the internalMenuWrapper function?
<#
.Synopsis
wrap the menu so we can mock calls to it
#>
function internalMenuWrapper {
param (
[Parameter(Mandatory=$true)]
$prompt,
[Parameter(Mandatory=$true)]
$options
)
return = $host.ui.PromptForChoice("Waiting for user input", $prompt, [System.Management.Automation.Host.ChoiceDescription[]]$options, 0)
}
<#
.Synopsis
Create a menu with an array of choices and return the result
#>
function Invoke-Menu($prompt, $opts) {
$options = #()
foreach ($opt in $opts) {
$options += $(new-object System.Management.Automation.Host.ChoiceDescription $opt)
}
$index = internalMenuWrapper $prompt $options
$opts[$index]
}
Describe 'Invoke-Menu' {
Context "when called" {
It "returns the object that was selected" {
#mock fails
Mock internalMenuWrapper { return 0 }
$result = Invoke-Menu "test menu" #("pass", "fail")
$result | Should -Be "pass"
}
}
}
As Mike Shepard points out in a comment, mocking methods isn't supported in Pester, only commands can be mocked (cmdlets, functions, aliases, external programs).
You can work around the issue by using the Get-Host cmdlet instead of $host and mock that:
function Invoke-Menu($prompt, $choices) {
$choiceObjects = [System.Management.Automation.Host.ChoiceDescription[]] $choices
# Use Get-Host instead of $host here; $host cannot be mocked, but Get-Host can.
$index = (Get-Host).ui.PromptForChoice("Waiting for user input", $prompt, $choiceObjects, 0)
$choices[$index]
}
Describe 'Invoke-Menu' {
Context "when called" {
It "returns the object that was selected" {
# Mock Get-Host with a dummy .ui.PromptForChoice() method that instantly
# returns 0 (the first choice).
Mock Get-Host {
[pscustomobject] #{
ui = Add-Member -PassThru -Name PromptForChoice -InputObject ([pscustomobject] #{}) -Type ScriptMethod -Value { return 0 }
}
}
Invoke-Menu 'test menu' '&pass', '&fail' | Should -Be '&pass'
}
}
}
As you point out on GitHub, if the suggested Read-Choice cmdlet is ever implemented (as a PS-friendly wrapper around $host.ui.PromptForChoice()), it could be mocked directly (and there would be no custom code to test).
I'm working on a PS script (v5 on the machine I'm using) that uses Invoke-WebRequest to grab information from a web address and returns the results.
When attempting to pipe my output to Out-GridView with more than 9 results, the column containing data lists "..." on the 10th line.
I've tried doing several types of joins, and am just wondering if I need to have my result in a specific type to avoid having this effect (hashtable maybe?)
Checking MS forums has only yielded results about joining on line-end's, which doesn't seem to help in this case.
The pages I'm querying are simple HTML showing the output of .txt files, however the .Content property of my Invoke-WebRequest query seems to be one long string.
Here's my code thus far:
[cmdletBinding(
DefaultParameterSetName='FileWithURIs'
)]
Param(
[Parameter(ParameterSetName='FileWithURIs',
Mandatory=$true
)]
[ValidateNotNull()]
[ValidateNotNullOrEmpty()]
[ValidateScript({
if(-Not ($_ | Test-Path) ){
throw "File or folder does not exist"
}
if(-Not ($_ | Test-Path -PathType Leaf) ){
throw "The Path argument must be a file. Folder paths are not allowed."
}
if($_ -notmatch "(\.txt)"){
throw "The file specified in the path argument must be of type txt"
}
return $true
})]
[String]$FileWithURIs,
[Parameter(ParameterSetName='SingleURI',
Mandatory=$True)]
[ValidateNotNull()]
[ValidateNotNullOrEmpty()]
[ValidateScript({
if($_.StartsWith("http://") -eq $false -and $_.StartsWith("https://" -eq $false))
{
throw "User specified URI must start with http:// or https://"
}
else
{
return $true
}
})]
[String]$URI,
[Switch]$ViewAsGrid
)
BEGIN
{
Function Check-CustomType()
{
if("TrustAllCertsPolicy" -as [type])
{
Out-Null
}
else
{
Set-CustomType
}
}
Function Set-CustomType()
{
add-type #"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint srvPoint, X509Certificate certificate,
WebRequest request, int certificateProblem) {
return true;
}
}
"#
$script:newCertPolicy = [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
}
Function Evaluate-URIs()
{
if($URI)
{
Get-Blocklist -ListURI $URI
}
elseif($FileWithURIs)
{
$lines = Get-Content $FileWithURIs
foreach($line in $lines)
{
Get-Blocklist -ListURI $line
}
}
}
Function Create-Table()
{
$script:tabName = "ResultTable"
# Create Table object
$script:table = New-Object System.Data.DataTable “$script:tabName”
# Create first column
$script:col1 = New-Object System.Data.DataColumn SiteName,([string])
$script:table.columns.add($script:col1)
}
Function Add-RowToTable($Value)
{
# Create new row
$newRow = $script:table.NewRow()
# Add value to row
$newRow.SiteName = $Value
# Add row to table
$script:table.Rows.Add($newRow)
}
Function Get-Blocklist($ListURI)
{
try
{
$query = Invoke-WebRequest -Uri "$ListURI"
if($ViewAsGrid)
{
Create-Table
$content = #($query.Content.Split("`r`n"))
foreach($entry in $content)
{
Add-RowToTable -Value $entry
}
$script:table | Out-GridView -Title "Blocklist for $ListURI"
}
else
{
Write-Host "`nBlocklist for $ListURI " -ForegroundColor Yellow -NoNewline
Write-Host "`n`n$($query.Content | Sort -Descending)"
}
}
catch [Exception]
{
Write-Host "`nUnable to connect to resource " -ForegroundColor Yellow -NoNewline
Write-Host "$ListURI" -ForegroundColor Red
Write-Host "`nERROR: $($Error[0].Exception)"
}
}
Function Run-Stack()
{
Check-CustomType
Evaluate-URIs
}
}
PROCESS
{
Run-Stack
}
END { Write-Host "`nEnd of script" }
The idea is to only use Out-GridView if a single, user-entered address is the input. Otherwise it just becomes console output.
Any help would be appreciated!
Last week I ran into a strange issue.
I modified code that should test if a file is locked and which I once found on the Internet (essentially similar to https://superuser.com/questions/876288/how-do-i-detect-and-skip-locked-files-in-a-powershell-script) to
1) deal with timeout and
2) to handle non existing files (and directories) correctly.
So here is my code:
function Test-IsFileLocked {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $True, Position = 0)]
[ValidateNotNullOrEmpty()]
[System.IO.FileInfo] $File
, [ref] $MSecsPassed
)
try {
$Stream = $File.Open([System.IO.FileMode]"Open", [System.IO.FileAccess]"Read", [System.IO.FileShare]"None")
}
# The follwing catch block seems to cause the problem that the 'IO.FileNotFoundException' gets caught after some time (although it is still a simple 'IO.IOException.)
# Outcomment it by removing the '#' in front of '<# ...' to see how all works nicely...
#<#
catch [System.IO.DirectoryNotFoundException] {
Write-Host "System.IO.DirectoryNotFoundException: '$($_.Exception)'`nType = '$($_.Exception.GetType().FullName)'`nType = '$($_.GetType().FullName)'" -ForegroundColor:Yellow
return $False
}
#>
catch [System.IO.FileNotFoundException] {
Write-Host "Caught a 'FileNotFoundException' exception, although the exception still is of type of a simple 'System.IO.IOException'! MSecs passed: $($MSecsPassed.Value)" -ForegroundColor:Red
Write-Host "FileNotFoundException: '$($_.Exception)'`nType = '$($_.Exception.GetType().FullName)'`nType = '$($_.GetType().FullName)'" -ForegroundColor:Yellow
#if ($_.Exception.GetType().FullName -eq 'System.IO.IOException') {
# return $True
#} else {
return $False
#}
} catch [System.IO.IOException] {
#Write-Host "System.IO.IOException: '$($_.Exception)'`nType = '$($_.Exception.GetType().FullName)'`nType = '$($_.GetType().FullName)'"
if ($_.Exception.GetType().FullName -eq 'System.IO.IOException') {
return $True
} else {
return $False
}
} catch {
Write-Host "Any Exception: '$($_.Exception)'`nType = '$($_.Exception.GetType().FullName)'`nType = '$($_.GetType().FullName)'" -ForegroundColor:Yellow
if ($_.Exception.GetType().FullName -eq 'System.IO.IOException') {
return $True
} else {
return $False
}
} finally {
if ($Stream) {
$Stream.Dispose()
}
}
return $False
}
function Wait-UntilFileIsAccessible {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $True, Position = 0)]
[ValidateNotNullOrEmpty()]
[System.IO.FileInfo] $File
, [Parameter(Position = 1)]
[uint32] $CheckIntervalInMSecs
, [Parameter(Position = 2)]
[uint32] $Timeout
)
if (!$CheckIntervalInMSecs) {
$CheckIntervalInMSecs = 500
}
[double] $SecondsPassed = 0.0
[double] $SecondsAdder = [double]$CheckIntervalInMSecs / 1000.0
[uint32] $MSecsPassed = 0
#Write-Verbose "Waiting for '$($File.Fullname)' to get unlocked!"
while (Test-IsFileLocked -File:$File -MSecsPassed:([ref]$MSecsPassed)) {
Start-Sleep -MilliSeconds:$CheckIntervalInMSecs
$MSecsPassed += $CheckIntervalInMSecs
if ($Timeout) {
$SecondsPassed += $SecondsAdder
if (([uint32]$SecondsPassed) -ge $Timeout) {
return $False
}
}
}
#Write-Vebose "'$($File.Fullname)' now isn't locked anymore"
return $True
}
If I call this code with
Wait-UntilFileIsAccessible -File:'C:\LockedByMSWord.txt' -Timeout:30
where 'C:\LockedByMSWord.txt' is an existing file actually locked (e.g. opened with MS Word) then after some time (16500 ms in most cases for me) the 'IO.FileNotFoundException' gets caught although the exception thrown seems to be of type 'IO.IOException'.
If I then repeat the call to 'Wait-UntilFileIsAccessible' then usually Powershell immediately catches this 'wrong' exception.
It drove me nuts and I tried different things until I removed the code block that catches the 'IO.DirectoryNotFoundException' (for testing purposes initially) just to find out that then everything works as expected.
Am I doing something wrong here (I mean with the code that catches this 'IO.DirectoryNotFoundException' exception), do I misunderstand something or could this be a Powershell bug?
This is with Powershell 4 and ErrorActionPreference set to 'Continue'.
Also some code obviously is just in there for testing purposes (I wouldn't need ref parameter MSecsPassed in Test-IsFileLocked and output to host normally)
P.S.: I know how to workaround it, but I would like feedback to my question if I am doing something wrong in my code or if this possibly could be a bug in Powershell.
Thanks,
Patrik
I am trying to rename a few of my cmdlets and want to do it without breaking existing scripts. I want to do it without using Set-Alias/New-Alias because I do not want the Aliases to show up when we do Get-Command from the powershell prompt and I thought it might be possible to use exported functions to achieve the same thing that aliasing cmdlets would do.
Here is an example of what I want to do
Old cmdlet - Add-Foo
Renamed cmdlet - Add-FooBar
Expectation - Scripts using Add-Foo should continue to work the same way as it used to
I am thinking of introducing the following function
function Add-Foo()
{
# Delegate parameter inputs to cmdlet Add-FooBar
}
I have a simple version of it but I am not sure if it would work in more complex cases.
function Add-Foo()
{
$cmd = "Add-FooBar"
if ($arguments.Length -eq 0){
Invoke-Expression $cmd;
}
else{
# Concatentate cmdlet and arguments into an expression
$expr = "$($cmd) $($args)";
Write-Debug $expr;
Invoke-Expression $expr;
}
}
I am not sure if my function is going to be 100% compatible with existing usages. Can the function Add-Foo be made such that it behaves well with parameter attributes (pipeline binding) and any other possible usages? Essentially I want the function to take the arguments as is and pass it to the underlying renamed cmdlet.
Any help is appreciated.
Thanks
PowerShell has a built-in feature for this: Proxy commands.
The [System.Management.Automation.ProxyCommand] class has several static methods to help out with this. Below is a template you can use to generate a proxy command and add a condition choosing whether or not to call the original command.
function New-ProxyCommand($command)
{
$cmd = Get-Command $command
$blocks = #{
CmdletBinding = [System.Management.Automation.ProxyCommand]::GetCmdletBindingAttribute($cmd)
Params = [System.Management.Automation.ProxyCommand]::GetParamBlock($cmd)
Begin = [System.Management.Automation.ProxyCommand]::GetBegin($cmd)
Process = [System.Management.Automation.ProxyCommand]::GetProcess($cmd)
End = [System.Management.Automation.ProxyCommand]::GetEnd($cmd)
}
# Indent
filter Indent($indent=' ') { $_ | foreach { ($_ -split "`r`n" | foreach { "${indent}$_" }) -join "`r`n" } }
[array]$blocks.Keys | foreach { $blocks[$_] = $blocks[$_] | Indent }
#"
function $command
{
$($blocks.CmdletBinding)
param
($($blocks.Params)
)
begin
{
`$Reroute = `$false ### Put your conditions here ###
if (`$Reroute) { return }
$($blocks.Begin)}
process
{
if (`$Reroute) { return }
$($blocks.Process)}
end
{
if (`$Reroute) { return }
$($blocks.End)}
}
"#
}
Example:
PS> New-ProxyCommand Get-Item
function Get-Item
{
[CmdletBinding(DefaultParameterSetName='Path', SupportsTransactions=$true, HelpUri='http://go.microsoft.com/fwlink/?LinkID=113319')]
param
(
[Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[string[]]
${Path},
[Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[string[]]
${LiteralPath},
[string]
${Filter},
[string[]]
${Include},
[string[]]
${Exclude},
[switch]
${Force},
[Parameter(ValueFromPipelineByPropertyName=$true)]
[pscredential]
[System.Management.Automation.CredentialAttribute()]
${Credential}
)
begin
{
$Reroute = $false ### Put your conditions here ###
if ($Reroute) { return }
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-Item', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process
{
if ($Reroute) { return }
try {
$steppablePipeline.Process($_)
} catch {
throw
}
}
end
{
if ($Reroute) { return }
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
One option is to use a private function:
function Private:Add-Foo
{
Add-Foobar $args
}
Add-Foo will only call this function in the current scope. The function will not be visible within any child scope (like a called script), and they will use the Add-Foo cmdlet instead.