Powershell catches wrong exception? What am I doing wrong? - powershell

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

Related

Powershell pass code to a function, add -errorAction and process in try/catch block

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

collections.ArrayList Add method producing a string on first Add

I am attempting to mock up a data structure and logic to handle deferred log writing, and I am running into an odd situation. if ([collections.arrayList]$deferredLog = Get-PxDeferredLogItems) { errors when the returned arrayList is only one item, with
Cannot convert the "logged: 07/20/2019 10:56:29" value of type "System.String" to type "System.Collections.ArrayList".
Once there are two items it's fine. Now I know an array of a single item needs to be forced to be an array because PS is sometimes too clever for it's own good, but I thought the Add method of a [collections.arrayList] produced an arrayList even on the first Add. AM I totally misunderstanding, or is my logic wrong somewhere else?
I also tried casting the results of the function call like this
if ([collections.arrayList]$deferredLog = [collections.arrayList](Get-PxDeferredLogItems)) { but that is even worse, it errors at every call. I also tried initializing the variable to an empty arrayList with Set-PxDeferredLogItems (New-Object collections.arrayList) in Main, rather the n initializing it on the first call of Add-PxDeferredLogItem, but that shows the same behavior, errors until the deferred log has two items. I verified this by changing the Write-Host directly after the erroring line to Write-Host "Process deferred items $($deferredLog.count)", and I get errors till it shows a 2, then everything works as expected.
function Get-PxDeferredLogItems {
return $script:deferredLogItems
}
function Set-PxDeferredLogItems {
param (
[collections.arrayList]$deferredLog
)
[collections.arrayList]$script:deferredLogItems = $deferredLog
}
function Add-PxDeferredLogItem {
param (
[string]$item
)
if (-not $script:deferredLogItems) {
[collections.arrayList]$script:deferredLogItems = New-Object collections.arrayList
}
[void]$script:deferredLogItems.Add($item)
}
function Add-PxLogContent {
param (
[string]$string
)
function Assert-PxWrite {
if ((Get-Random -minimum:1 -maximum:4) -gt 1) {
return $true
} else {
return $false
}
}
if ([collections.arrayList]$deferredLog = Get-PxDeferredLogItems) {
Write-Host "Process deferred items"
:deferredItemsWrite do {
if (Assert-PxWrite) {
Write-Host "$($deferredLog[0]) ($(Get-Date))"
$deferredLog.RemoveAt(0)
} else {
break :deferredItemsWrite
}
} while ($deferredLog.count -gt 0)
if ($deferredLog.count -eq 0) {
$deferredLogPending = $false
} else {
$deferredLogPending = $true
}
Set-PxDeferredLogItems $deferredLog
} else {
$deferredLogPending = $false
}
if (-not $deferredLogPending) {
if (Assert-PxWrite) {
Write-Host $string
} else {
Add-PxDeferredLogItem $string
}
} else {
Add-PxDeferredLogItem $string
}
}
### MAIN
$startTime = Get-Date
$endTime = $startTime + (New-TimeSpan -minutes:2)
Clear-Host
do {
Add-PxLogContent "logged: $(Get-Date)"
Start-SLeep -s:5
} while ((Get-Date) -lt $endTime)
if ($deferredLog = Get-PxDeferredLogItems) {
foreach ($item in $deferredLog) {
Write-Host "!$item"
}
}
EDIT: It seems to be related to passing the arrayList, as this works as expected when there are no functions involved.
Clear-Host
$deferredLogItems = New-Object collections.arrayList
Write-Host "$($deferredLogItems.getType()) $($deferredLogItems.count) $deferredLogItems"
[void]$deferredLogItems.Add('One')
Write-Host "$($deferredLogItems.getType()) $($deferredLogItems.count) $deferredLogItems"
[void]$deferredLogItems.Add('Two')
Write-Host "$($deferredLogItems.getType()) $($deferredLogItems.count) $deferredLogItems"
[void]$deferredLogItems.Add('Three')
Write-Host "$($deferredLogItems.getType()) $($deferredLogItems.count) $deferredLogItems"
function Get-DeferredLogItems {
if (-not $script:deferredLogItems) {
[collections.arrayList]$script:deferredLogItems = New-Object collections.arrayList
}
[void]$script:deferredLogItems.Add("$($script:deferredLogItems.count + 1)")
return $script:deferredLogItems
}
$script:deferredLogItems = $localDeferredLogItems = $null
foreach ($i in 1..5) {
[collections.arrayList]$localDeferredLogItems = Get-DeferredLogItems
Write-Host "$($localDeferredLogItems.getType()) $($localDeferredLogItems.count) $localDeferredLogItems"
}
EDIT2: Replaced the line # reference with the actual offending line of code, because Confusion.
EDIT3: For what it's worth, forcing the results of Get-DeferredLogItems into an array DOES work. But I still wonder why that's even needed. Why does the Add method produce a correct arrayList at first add but this gets #%#$ed up by PowerShell when passed as a return value?

Check if Read-Host is a number for Switch

I'm just learning switch to make my logic a bit cleaner, and it seems to work except I'm having trouble determining if my Read-Host value is a number (for the access point number to select).
## Give option to reset all aps on site
$continueVal = Read-Host "`nSpecify AP # to see more details or type 'Reset' to reset all APs in Store $Store"
## Start switch
$event = switch ($continueVal) {
[int]{
$apNumber = $continueVal
Query-AP($apNumber)
}
'Reset' {
Manage-Prelim($e = 2)
}
default {
Repeat
}
}
When I was using If/Else/ElseIf I'd use if($continueVal -gt 0) which would work, but still dirty. With switch it seems that -gt 0 is improper syntax and fails. How would I effectively check if the value of $continueVal is a number to pass it to the next function as $apNumber?
I don't want to pre-validate as possible options can come through as an integer or a string.
Here is another approach that uses parameters and parameter sets:
# testscript.ps1
[CmdletBinding(DefaultParameterSetName = "APNumber")]
param(
[Parameter(Mandatory = $true,ParameterSetName = "APNumber")]
[Int] $APNumber,
[Parameter(Mandatory = $true,ParameterSetName = "Controller")]
[String] $Controller,
[Parameter(Mandatory = $true,ParameterSetName = "Reset")]
[Switch] $Reset
)
switch ( $PSCmdlet.ParameterSetName ) {
"APNumber" {
"You specified -APNumber with value '$APNumber'"
break
}
"Controller" {
"You specified -Controller with value '$Controller'"
break
}
"Reset" {
"You specified -Reset"
break
}
}
This script is simple to use. Example usage:
testscript -APNumber 3
testscript -Controller "foo"
testscript -Reset
If you omit any parameters, it will prompt for the -APNumber parameter (since it specifies that as the default parameter set).
Now that I understand your question more, this can be done with switch -regex and parsing. Here is a short example:
do {
$response = Read-Host "Enter a response"
$valid = $true
switch -regex ( $response ) {
'^AP Number \d+$' {
$arg = [Regex]::Match($_,'\d+$').Value -as [Int]
Write-Host "You entered 'AP Number $arg'"
break
}
'^Controller \S+$' {
$arg = [Regex]::Match($_,'\S+$').Value
Write-Host "You entered 'Controller $arg'"
break
}
'^Reset$' {
Write-Host "You entered 'Reset'"
break
}
default {
$valid = $false
Write-Host "Invalid entry"
}
}
}
until ( $valid )
Note that this is more code than the parameter version, more complex, and you can't automate it.

Application runs in Windows 7 but not in Windows 10

I have created an app for back up and restore of computers. I also allows modification of ADObjects through the use of custom Profile.ps1 file. The app runs fine in the ISE with no errors and works properly no errors in Windows 7. However, when I try to run it in a newly imaged Windows 10 machine I get "Property Can Not Be Found" errors on all my object properties.
The thing is I can read all the properties when I fill comboboxes fine. The error only occurs when the form is submitted. I will attach 1 of the forms I am having a problem with. Again it runs fine in Windows 7, but not Windows 10.
Could this be a problem with Microsoft updates?
Also, yes, I am setting Set-ExecutionPolicy -Scope Process -ExecutionPolicy Unrestricted.
Error message:
The property 'company' cannot be found on this object. Verify that the
property exist and can be set.
+ $CO.company = $company
+ Categoryinfo :InvalidOperation: (:) [] RuntimeExeption
Code:
. \\iotsdsp01pw\installs$\MoveToOU\PcDeployment\Profile.ps1
#region Validation Functions
function Validate-IsEmail ([string]$Email) {
return $Email -match "^(?("")("".+?""#)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])#))(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$"
}
function Validate-IsURL ([string]$Url) {
if ($Url -eq $null) {
return $false
}
return $Url -match "^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_]*)?$"
}
function Validate-IsName ([string]$Name, [int]$MaxLength) {
if ($MaxLength -eq $null -or $MaxLength -le 0) {
#Set default length to 40
$MaxLength = 40
}
return $Name -match "^[a-zA-Z''-'\s]{1,$MaxLength}$"
}
function Validate-IsIP ([string]$IP) {
return $IP -match "\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b"
}
function Validate-IsEmptyTrim ([string]$Text) {
if ($text -eq $null -or $text.Trim().Length -eq 0) {
return $true
}
return $false
}
function Validate-IsEmpty ([string]$Text) {
return [string]::IsNullOrEmpty($Text)
}
function Validate-IsDate ([string]$Date) {
return [DateTime]::TryParse($Date, [ref](New-Object System.DateTime))
}
#endregion
$No_Load = {
$NewForm.Close()
#Initialize variables
$dateTime = Get-Date -Format G
$userName = (Get-WmiObject -Class Win32_ComputerSystem -Property UserName).UserName
$computerName = $env:computername
#Varables for display
$distinguishedName = (Get-dn computer cn $computerName)
$computerObject = (Get-ADObject $distinguishedName)
$organizationalUnit = (Get-ADObject "OU=Agencies, DC=state, DC=in, DC=us")
#Initialize Form Controls
$lblUserNameNewNo.Text = $userName
$lblComputerNameNewNo.Text = $computerName
$lblPhysicalLocationNewNo.Text = $computerObject.location
$txtBillingCodeNewNo.Text = $computerObject.departmentNumber
$comboboxAccountTypeNewNo.Text = $computerObject.extensionAttribute15
$comboboxOrganizationalUnitNewNo.Text = $computerObject.company
Load-ComboBox -ComboBox $comboboxOrganizationalUnitNewNo ($organizationalUnit.children | %{ $_.OU })
}
#region Control Helper Functions
function Load-ComboBox {
Param (
[ValidateNotNull()]
[Parameter(Mandatory = $true)]
[System.Windows.Forms.ComboBox]$ComboBox,
[ValidateNotNull()]
[Parameter(Mandatory = $true)]
$Items,
[Parameter(Mandatory = $false)]
[string]$DisplayMember,
[switch]$Append
)
if (-not $Append) {
$ComboBox.Items.Clear()
}
if ($Items -is [Object[]]) {
$ComboBox.Items.AddRange($Items)
} elseif ($Items -is [Array]) {
$ComboBox.BeginUpdate()
foreach ($obj in $Items) {
$ComboBox.Items.Add($obj)
}
$ComboBox.EndUpdate()
} else {
$ComboBox.Items.Add($Items)
}
$ComboBox.DisplayMember = $DisplayMember
}
#Validation
function ParameterValidate {
Param (
[Parameter(Mandatory = $true)]
[ValidateNotNull()]
[ValidateLength(1, 10)]
[String]$Text
)
return $true
}
$comboboxOrganizationalUnitNewNo_Validating = [System.ComponentModel.CancelEventHandler]{
#Check if the Name field is empty
$result = Validate-IsEmptyTrim $comboboxOrganizationalUnitNewNo.Text
if ($result -eq $true) {
#Mark a failure only if the Validation failed
$script:ValidationFailed = $true
#Display an error message
$errorprovider1.SetError($comboboxOrganizationalUnitNewNo, "Please select agency.");
} else {
#Clear the error message
$errorprovider1.SetError($comboboxOrganizationalUnitNewNo, "");
}
}
$txtBillingCodeNewNo_Validating = [System.ComponentModel.CancelEventHandler]{
#Check if the Name field is empty
$result = Validate-IsEmptyTrim $txtBillingCodeNewNo.Text
if ($result -eq $true) {
#Mark a failure only if the Validation failed
$script:ValidationFailed = $true
#Display an error message
$errorprovider1.SetError($txtBillingCodeNewNo, "Please enter billing code.");
} else {
#Clear the error message
$errorprovider1.SetError($txtBillingCodeNewNo, "");
}
}
$comboboxAccountTypeNewNo_Validating = [System.ComponentModel.CancelEventHandler]{
$result = Validate-IsEmptyTrim $comboboxAccountTypeNewNo.Text
if ($result -eq $true) {
#Mark a failure only if the Validation failed
$script:ValidationFailed = $true
#Display an error message
$errorprovider1.SetError($comboboxAccountTypeNewNo, "Please enter agency type.");
} else {
#Clear the error message
$errorprovider1.SetError($comboboxAccountTypeNewNo, "");
}
}
$control_Validated = {
#Pass the calling control and clear error message
$errorprovider1.SetError($this, "");
}
$No_FormClosing = [System.Windows.Forms.FormClosingEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.FormClosingEventArgs]
#Validate only on OK Button
if ($No.DialogResult -eq "OK") {
#Init the Validation Failed Variable
$script:ValidationFailed = $false
#Validate the Child Control and Cancel if any fail
$No.ValidateChildren()
#Cancel if Validation Failed
$_.Cancel = $script:ValidationFailed
}
}
#Events
$buttonColor_Click = {
#TODO: Place custom script here
$colordialog1.ShowDialog()
}
$linklblViewListNewNo_LinkClicked = [System.Windows.Forms.LinkLabelLinkClickedEventHandler]{
Start-Process "http://billingcodes.iot/"
}
$btnSubmitNewNo_Click = {
#TODO: Place custom script here
$company = $comboboxOrganizationalUnitNewNo.Text
$departmentNumber = $txtBillingCodeNewNo.Text
$accountType = $comboboxAccountTypeNewNo.Text
if ($accountType -eq "Seat") {
$accountType = " "
}
#Varables for Set-ADObject
$computerObject.company = $company
$computerObject.departmentNumber = $departmentNumber
$computerObject.extensionAttribute15 = $accountType
try {
$computerObject.SetInfo()
[Environment]::Exit(1)
} catch {
$labelDialogRedNewNo.Text = "AD computer object not found"
}
}
This is your culprit (from what I can see):
$No_Load = {
...
$computerObject = (Get-ADObject $distinguishedName)
...
}
...
$btnSubmitNewNo_Click = {
...
$computerObject.company = $company
...
}
You assign a computer object to the variable $computerObject in one scriptblock, and try to change one of its properties in another scriptblock. However, to be able to share a variable between scriptblocks you need to make it a global variable, otherwise you have two separate local variables that know nothing about each other.
$No_Load = {
...
$global:computerObject = Get-ADObject $distinguishedName
...
}
...
$btnSubmitNewNo_Click = {
...
$global:computerObject.company = $company
...
}
BTW, I doubt that this ever worked in Windows 7, since it failed with the same error on my Windows 7 test box.

Powershell function dispose or abort handler

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.