Is there a way to customize the error message of a terminating error?
In the example below I would like to just end up with one Try section and collect all errors in one Catch section by combining both script blocks. My problem is that the $error generated by Import-csv is not descriptive enough and I would like to have the text Failed CSV-File import c:\test.csv within the $error message.
Thank you for your advise.
Script block 1
Try {
$File = (Import-Csv -Path c:\test.csv)
}
Catch {
throw $error[0].Exception.Message | Send-Mail $ScriptAdmin "FAILED CSV-File import"
}
Script block 2
try {
if(!(Test-Path $LogFolder -PathType Container)) {throw "Can't find the log folder: '$LogFolder'"}
$Credentials = Import-Credentials $UserName $PasswordFile
}
catch {
throw $Error[0].Exception.Message | Send-Mail $ScriptAdmin "FAILURE"
}
Workaround
A possible solution would be to check first with test-path if the import file exists and then create a throw with a customized message. But I would like to know if it's possible to handle it in one line of code without using test-path first.
Best solution (thanks to mjolinor):
try {
$File = (Import-Csv -Path $ImportFile -Header "A", "B", "C", "D" | Where { $_.A -NotLike "#*" })
if(!(Test-Path $LogFolder -PathType Container)) {throw "Can't find the log folder: '$LogFolder'"}
$Credentials = Import-Credentials $UserName $PasswordFile
}
catch {
Switch -Wildcard ($Error[0].Exception)
{
"*$ImportFile*"
{ $FailType = "FAILED CSV-File import" }
"*$LogFolder*"
{ $FailType = "FAILED Log folder not found" }
"*Import-Credentials*"
{ $FailType = "FAILED Credential import" }
Default
{ $FailType = "FAILED Unrecognized error" }
}
Write-Warning $Error[0].Exception.Message
throw $Error[0].Exception.Message | Send-Mail $ScriptAdmin $FailType
}
Make sure that your advanced-function which you created (like Import-Credentials above) contains the function name in the throw section. So we can filter it out in the catchblock.
Something like this, maybe?
Try {
$File = (Import-Csv -Path c:\test.csv)
if(!(Test-Path $LogFolder -PathType Container))
{ throw "Can't find the log folder: '$LogFolder'" }
$Credentials = Import-Credentials $UserName $PasswordFile
}
catch {
Switch -Wildcard ($Error[0].CategoryInfo)
{
'*[Import-CSV]*'
{ $FailType = 'Import CSV failed' }
'*[Test-Path]*'
{ $FailType = 'LogFolder not found' }
'*[Import-Credentials]*'
{ $FailType = 'Credential import failed' }
Default
{ $FailType = 'Unrecognized error' }
}
$Error[0].Exception.Message | Send-Mail $ScriptAdmin $FailType
}
Edit (for some reason I'm not able to post a comment):
I should have specified that it wasn't tested, and was intended more as a pattern than a finished solution, and it seems to have done what it was intended to do.
#BartekB - I habitually use $Error[0] instead of $_ because it seems more explicit and intuitive to read, and less likely to be misunderstood by someone less experienced who might inherit the code later.
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
I am trying to encrypt multiple files in the folder, however it will not working if more than one files the filepath folder. If it only one file exist in the folder, it will encrypt without any issue. The following if the code that I have so far. Any advise what did I missed. Thanks
#region Encrypt
function Encrypt()
{
$pgp_program = "${env:ProgramFiles(x86)}" + "\GnuPG\bin\gpg.exe"
$recipient = "PROD_20"
$filepath = "C:\filesToEncypt\"
$files = Get-ChildItem -Path $filepath -recurse
try
{
foreach ($item in $files)
{
try
{
write-host $item
Start-Process $pgp_program -ArgumentList "--always-trust -r $recipient -e $item"
}
finally
{
}
}
}
catch [Exception]
{
Write-Host $_.Exception.Message
exit 1
}
}
Encrypt
I want to test if condition for zip file got extracted properly or any error in extracting command in PowerShell v4. Please correct my code.
Add-Type -AssemblyName System.IO.Compression.FileSystem
$file = 'C:\PSScripts\raw_scripts\zipfold\test.zip'
$path = 'C:\PSScripts\raw_scripts\zipfold\extract'
if ( (Test-path $file) -and (Test-path $path) -eq $True ) {
if ((unzip $file $path)) {
echo "done with unzip of file"
} else {
echo "can not unzip the file"
}
} else {
echo "$file or $path is not available"
}
function unzip {
param([string]$zipfile, [string]$outpath)
$return = [System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath)
return $return
}
This script extract a zip file but displays "can not unzip file." as output.
Not sure what $return variable has as its value, my If condition is always getting fail.
The documentation confirms what #Matt was suspecting. ExtractToDirectory() is defined as a void method, so it doesn't return anything. Because of that $return is always $null, which evaluates to $false.
With that said, the method should throw exceptions if something goes wrong, so you could use try/catch and return $false in case an exception occurs:
function unzip {
param([string]$zipfile, [string]$outpath)
try {
[IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath)
$true
} catch {
$false
}
}
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 )
given the script.
$foo = #("bar")
try {
$foo | ForEach-Object {
Join-Path $null $null
}
} catch {
$_.InvocationInfo.Line
}
will print
$foo | ForEach-Object {
but I would like
Join-Path $null $null
How can I get where the exception was actually raised?
This will get you the actual line:
$_.Exception.CommandInvocation.Line
and exception message:
$_.Exception.Message
and line number:
$_.Exception.Line
and offset (column):
$_.Exception.Offset
So you could make a helpful little message:
} catch {
$msg = "Failed to do something. Failed on line number '{0}' column '{1}' ('{2}'). The error was '{3}'." -f
$_.Exception.Line, $_.Exception.Offset, $_.Exception.CommandInvocation.Line.Trim(), $_.Exception.Message
Write-Error $msg
}