Trapping Powershell Error in a function - powershell

I'm having a bit of trouble preventing a certain error message from bubbling up from a function to my main routine's 'Catch'. I would like to have my function react to a particular error, then do something, and continue processing as usual without alerting my main routine that there was an error. Currently, if the file this script is trying to read is in use (being written to), it will write a System.IO.IOException error to my log. But sometimes I expect this error to occur and it isn't an issue and I don't want to fill my log with these type of errors. I would expect from the code below that the checkFileLock function would catch the error, return 0 to the findErrorInFile function, and no error would be caught to my error log.
Function findErrorsInFile{
param(
[string]$dir,
[string]$file,
[String]$errorCode
)
If((Get-Item $($dir + "`\" + $file)) -is [System.IO.DirectoryInfo]){ #we dont want to look at directories, only files
}Else{
If($(checkFileLock -filePath $($dir + "`\" + $file))){
$reader = New-Object System.IO.StreamReader($($dir + "`\" + $file))
$content = $reader.ReadToEnd()
$results = $content | select-string -Pattern $errorCode #if there is no regex match (no matching error code found), then the string $results will be == $null
If($results){
Return 1 #we found the error in the file
}Else{
Return 0 #no error found in the file
}
}Else{
Return 0 #The file was being written to, we will skip it and assume no error. This is rare.
}
}
}
Function checkFileLock{
param(
[String]$filePath
)
try{
$openFile = New-Object System.IO.FileInfo $filePath
$testStream = $openFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) #try to open a filestream
If($testStream){ #If the filestream opens, then it isn't locked
$testStream.Close() #close the filestream
}
return $false #File is not locked
}
catch{
return $true #File is locked
}
}
#### START MAIN PROCESS ####
Try{
if($(findErrorsInFile -dir 'somepath' -file 'somefilename' -errorcode 'abc')){
write-host "found something"
}else{
write-host "didn't find anything"
}
}
Catch{
$_.Exception.ToString() >> mylogfile.txt
}

try/catch blocks only catch terminating errors. Is your code generating a terminating or non-terminating error?
ArcSet has highlighted essentially what is required: force a non-terminating error to be a terminating error. I suspect it needs to be added to this line, if allowed:
$testStream = $openFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) -ErrorAction Stop
Edit - solution as -ErrorAction not an accepted parameter
I tried the above and it's not allowed. An alternative is to set the $ErrorActionPreference to Stop. This will affect all errors, so recommend reverting. Someone with more experience using System.IO.FileInfo objects may have a more elegant solution.
try{
$currentErrorSetting = $ErrorActionPreference
$ErrorActionPreference = "Stop"
$openFile = New-Object System.IO.FileInfo $filePath
$testStream = $openFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) #try to open a filestream
If($testStream){ #If the filestream opens, then it isn't locked
$testStream.Close() #close the filestream
}
$ErrorActionPreference = $currentErrorSetting
return $false #File is not locked
}
catch{
$ErrorActionPreference = $currentErrorSetting
return $true #File is locked
}

Use to force a catch
-ErrorAction Stop
use to suppress a error
[Command with error] | out-null

Related

Sqljob, make powershell step fail if file does not contain certain text

This is an attempt to get a specific answer to a possible solution to sqljob, on failed step, retry previous step, but not indefinitely
I have a job, which transfer a file using
powershell -command "Invoke-WebRequest https://someserver.dom/fetch.php -OutFile c:/tmp/data.xml"
Sometime the data from upstream is not proper XML. I just want to "grep" for the final line in the file, if it is not the expected </records> I want the step to fail, so the step is retried a few times before it hard-fails.
The filesize could also be a error-criterium. A failed transfer is a few lines, a successfil is megabytes.
So basically i made a function just for this. Set the amount of attempts and the script to run.
function TryAgain($Attempts = 1, $Scriptblock){
while($Attempts -gt 0){
try{
Invoke-Command -ScriptBlock $Scriptblock
Break
}catch{
$_.Exception
$Attempts -= 1
}
}
}
In this case we want to find out if it is valid XML so we are going to get the .RawContent of the Invoke-WebRequest and test against $(new-object System.Xml.XmlDocument).LoadXml(). If it fails throw a error, if it passes then out the raw content to a file. Errors will not be saved to the file only valid XML
$Site = "https://someserver.dom/fetch.php"
$OutFile = "c:/test/data.xml"
(Invoke-WebRequest $Site).RawContent | %{
try{
$(new-object System.Xml.XmlDocument).LoadXml($_)
$_ | out-file $OutFile
}catch{
throw "Bad XML"
}
}
Now we combine into a final product
$Site = "https://someserver.dom/fetch.php"
$OutFile = "c:/test/data.xml"
function TryAgain($Attempts = 1, $Scriptblock){
while($Attempts -gt 0){
try{
Invoke-Command -ScriptBlock $Scriptblock
Break
}catch{
$_.Exception
$Attempts -= 1
}
}
}
TryAgain -Attempts 3 -Scriptblock {
(Invoke-WebRequest $Site).RawContent | %{
try{
$(new-object System.Xml.XmlDocument).LoadXml($_)
$_ | out-file $OutFile
}catch{
throw "Bad XML"
}
}
}
since you dont need the retries and all the glam here is it in basic
(Invoke-WebRequest https://someserver.dom/fetch.php).RawContent | %{
try{
(new-object System.Xml.XmlDocument).LoadXml($_)
$_ | out-file C:\test\test.xml
}catch{}
}

Catch "Cannot Find File"

I have been searching for a while, but I cannot find the exception in PowerShell that would catch a "Cannot find file" error.
I would also like to have this loop until the user types in the correct file name to get.
# Ask user for file to read from
Try {
$readFile = Read-Host "Name of file to read from: "
$ips = GC $env:USERPROFILE\Desktop\$readFile.txt
}
Catch {
}
The error you get is a non-terminating error, and thus not caught. Add -ErrorAction Stop to your Get-Content statement or set $ErrorActionPreference = 'Stop' and your code will work as you expect:
try {
$readFile = Read-Host "Name of file to read from: "
$ips = GC $env:USERPROFILE\Desktop\$readFile.txt -ErrorAction Stop
} catch {
}
Don't use try/catch blocks for flow control. That is a generally-frowned-on practice, especially in PowerShell, since PowerShell's cmdlets will write errors instead of throwing exceptions. Usually, only non-PowerShell .NET objects will throw exceptions.
Instead, test if the file exists. That gives you much greater error control:
do
{
$readFile = Read-Host "Name of file to read from: "
$path = '{0}\Desktop\{1}.txt' -f $env:USERPROFILE,$readFile
if( (Test-Path -Path $path -PathType Leaf) )
{
break
}
Write-Error -Message ('File ''{0}'' not found.' -f $path)
}
while( $true )

Start-DscConfiguration doesn't throw exceptions?

I noticed that if applying a configuration through Start-DscConfiguration fails, it writes to the error stream but doesn't
throw an Exception? That is, if I do the following:
try{
Start-DscConfiguration -Path ".\MyConfig" -Wait -Verbose
}catch{
#...
}
...it never ends up in the catch handler. I suspect this may have something to do with the fact that without the "-Wait",
Start-DscConfiguration starts an async job for this, and async commands probably don't throw exceptions, but in a synchronous
scenario, I would very much like to know if my configuration could be applied.
What is the proper way to determine if Start-DscConfiguration has completed succesfully?
The only way I know is to check the global "$error" variable and compare the number of error records before and after your call to Start-DscConfiguration. If there's more afterwards then something must have gone wrong during the call, so throw your own exception:
Configuration TestErrorHandling {
Node "localhost" {
Script ErroringResource {
GetScript = { return $null; }
TestScript = { return $false; }
SetScript = { throw new-object System.InvalidOperationException; }
}
}
}
$errorCount = $error.Count;
write-host "starting dsc configuration"
$mof = TestErrorHandling;
Start-DscConfiguration TestErrorHandling –Wait –Verbose;
write-host "dsc configuration finished"
if( $error.Count -gt $errorCount )
{
$dscErrors = $error[$errorCount..($error.Count - 1)];
write-host "the following errors occurred during dsc configuration";
write-host ($dscErrors | fl * | out-string);
throw $dscErrors[-1];
}
There's another way to make it cause an exception. Try saving it into the ErrorVariable like this :
try
{
Start-DscConfiguration -Path ".\MyConfig" -Wait -Verbose -ErrorVariable ev
}
catch
{
$myException = $_
}
Weirdly so, this throws the exception when there's an error (which is what you wanted). You can get the value of your exception in the $myexception variable, and also could get just a one liner description of your error using $ev
PS: Note that while mentioning ev in the errorVariable parameter, you do it without the '$' symbol - since you're only specifying the variable 'name'.
Start-DscConfiguration when used without -Wait will create a job object - with one child job for every computername. PowerShell job objects have an Error stream which contains all the errors. You can check this stream as well
$job = Start-DscConfiguration -Force -Verbose -Path C:\Temp\Demo\ -ComputerName localhost
Receive-Job $job -Wait
'Errors in job = ' + ($job.childjobs[0].Error.Count)

Foreach error handling in Powershell

Is there a way to catch and save bad names in a foreach loop? I have the following:
$CollectionName = "Import Test"
$PCName = Import-Csv "C:\Powershell\import_test.csv"
foreach($computer in $PCName) {
Add-CMDeviceCollectionDirectMembershipRule -CollectionName $CollectionName -ResourceID $(Get- CMDevice -Name $computer.computername).ResourceID
}
What I would like to do is that if there is a bad name in the csv then instead of displaying the "Cannot validate argument" error I currently get, just output the failed name to a text file.
Thanks
Yes. Put the statement(s) inside the loop in a try..catch block:
foreach($computer in $PCName) {
try {
Add-CMDeviceCollectionDirectMembershipRule ...
} catch {
"Bad name: $name" | Out-File 'C:\bad_names.txt' -Append
}
}
If the error is a non-terminating error (i.e. displays an error message, but isn't caught by try..catch), you can turn it into a terminating error by adding -ErrorAction Stop to the command or by setting $ErrorActionPreference = "Stop".

RPC_E_SERVERCALL_RETRYLATER during powershell automation

I'm using PowersHell to automate iTunes but find the error handling / waiting for com objects handling to be less than optimal.
Example code
#Cause an RPC error
$iTunes = New-Object -ComObject iTunes.Application
$LibrarySource = $iTunes.LibrarySource
# Get "playlist" objects for main sections
foreach ($PList in $LibrarySource.Playlists)
{
if($Plist.name -eq "Library") {
$Library = $Plist
}
}
do {
write-host -ForegroundColor Green "Running a loop"
foreach ($Track in $Library.Tracks)
{
foreach ($FoundTrack in $Library.search("$Track.name", 5)) {
# do nothing... we don't care...
write-host "." -nonewline
}
}
} while(1)
#END
Go into itunes and do something that makes it pop up a message - in my case I go into the Party Shuffle and I get a banner "Party shuffle automatically blah blah...." with a "Do not display" message.
At this point if running the script will do this repeatedly:
+ foreach ($FoundTrack in $Library.search( <<<< "$Track.name", 5)) {
Exception calling "Search" with "2" argument(s): "The message filter indicated
that the application is busy. (Exception from HRESULT: 0x8001010A (RPC_E_SERVER
CALL_RETRYLATER))"
At C:\Documents and Settings\Me\My Documents\example.ps1:17 char:45
+ foreach ($FoundTrack in $Library.search( <<<< "$Track.name", 5)) {
Exception calling "Search" with "2" argument(s): "The message filter indicated
that the application is busy. (Exception from HRESULT: 0x8001010A (RPC_E_SERVER
CALL_RETRYLATER))"
At C:\Documents and Settings\Me\My Documents\example.ps1:17 char:45
If you waited until you you had a dialog box before running the example then instead you'll get this repeatedly:
Running a loop
You cannot call a method on a null-valued expression.
At C:\Documents and Settings\Me\example.ps1:17 char:45
+ foreach ($FoundTrack in $Library.search( <<<< "$Track.name", 5)) {
That'll be because the $Library handle is invalid.
If my example was doing something important - like converting tracks and then deleting the old ones, not handling the error correctly could be fatal to tracks in itunes.
I want to harden up the code so that it handles iTunes being busy and will silently retry until it has success. Any suggestions?
Here's a function to retry operations, pausing in between failures:
function retry( [scriptblock]$action, [int]$wait=2, [int]$maxRetries=100 ) {
$results = $null
$currentRetry = 0
$success = $false
while( -not $success ) {
trap {
# Set status variables at function scope.
Set-Variable -scope 1 success $false
Set-Variable -scope 1 currentRetry ($currentRetry + 1)
if( $currentRetry -gt $maxRetries ) { break }
if( $wait ) { Start-Sleep $wait }
continue
}
$success = $true
$results = . $action
}
return $results
}
For the first error in your example, you could change the inner foreach loop like this:
$FoundTracks = retry { $Library.search( "$Track.name", 5 ) }
foreach ($FoundTrack in $FoundTracks) { ... }
This uses the default values for $wait and $maxRetries, so it will attempt to call $Library.search 100 times, waiting 2 seconds between each try. If all retries fail, the last error will propagate to the outer scope. You can set $ErrorActionPreference to Stop to prevent the script from executing any further statements.
COM support in PowerShell is not 100% reliable. But I think the real issue is iTunes itself. The application and COM model wasn't designed, IMO, for this type of management. That said, you could implement a Trap into your script. If an exception is raised, you could have the script sleep for a few seconds.
Part of your problem might be in how $Track.name is being evaluated. You could try forcing it to fully evaluate the name by using $($Track.name).
One other thing you might try is using the -strict parameter with your new-object command/