I'm trying to interrogate some service information. Sometimes the installer of the application fails to correctly install, so the registry does not contain a service entry. I want to find out which installer steps did get executed correctly, even on systems that do not have proper logging in the installer.
If MyService does not exist, the script below does not go to the catch block even though the exception handling documentation suggests a bare catch should be enough:
try {
$path = 'hklm:\SYSTEM\CurrentControlSet\services\MyService'
$key = Get-Item $path
$namevalues = $key | Select-Object -ExpandProperty Property |
ForEach-Object {
[PSCustomObject] #{
Name = $_;
Value = $key.GetValue($_)
}
}
$namevalues | Format-Table
}
catch {
$ProgramFilesX86 = [System.Environment]::GetFolderPath("ProgramFilesX86");
$ProgramFiles = [System.Environment]::GetFolderPath("ProgramFiles");
Write-Host $ProgramFilesX86
Write-Host $ProgramFiles
}
Why is that and how should I force it to end up in the catch?
This is what PowerShell outputs:
Get-Item : Cannot find path 'HKLM:\SYSTEM\CurrentControlSet\services\MyService' because it does not exist.
At C:\Users\Developer\...\GetMyServiceInfo.ps1:17 char:12
+ $key = Get-Item $path
+ ~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (HKLM:\SYSTEM\Cu...vices\MyService:String) [Get-Item], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand
Force the error to be terminating:
$key = Get-Item $path -ErrorAction Stop
That way it will throw and catch will get it.
Explanation and links to the official Microsoft documentation:
-ErrorAction is a Common Parameter that can be applied to any PowerShell command
The default value for -ErrorAction is Continue which prevented the exception to be thrown in the first place.
You can configure a global -ErrorAction setting using the Preference Variable named $ErrorActionPreference to override this default value.
Related
I'm just beginning to dip into PowerShell with AD so I apologize if the question seems obvious.
I am trying to check if which of the devices provided in a list are in AD. So far I've used the code from:
Powershell - verify object exists in AD
It works just fine, but the "-ErrorAction SilentlyContinue" does not actually suppress the error messages. I get the below:
Get-ADComputer : Cannot find an object with identity: 'test' under:
'DC=test,DC=dom'.
At C:\Users\testaccount\Desktop\test.ps1:171
char:19
+ if (#(Get-ADComputer $target -ErrorAction SilentlyContinue).Count)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (test:ADComputer) [Get-ADComputer], ADIdentityNotFoundException
+ FullyQualifiedErrorId : ActiveDirectoryCmdlet:Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException,Microsoft.ActiveDirectory.Management.Commands.GetADComputer
The code I am using is as follows:
foreach ($target in $devicelist)
{
if (#(Get-ADComputer $target -ErrorAction SilentlyContinue).Count)
{
$existingdevices += $target
}
else
{
#display error notification
}
}
What I am looking for is suppressing the error message to no longer show in console - for the script to actually silently continue on error.
Any and all help will be appreciated!
So lets talk about whats happening.
There are 2 types of errors Terminating and Non-Terminating.
Terminating stops the execution of a command and throws an Exception. A non-terminating returns a write-out error message.
-ErrorAction takes care of Non-Terminating errors
Try{}Catch{} takes care of Terminating errors.
In your case
foreach ($target in $devicelist)
{
try{
if (#(Get-ADComputer $target -ErrorAction SilentlyContinue).Count)
{
$existingdevices += $target
}
else
{
#display non-terminating error notification
}
}catch{
#display Terminating error notification
}
}
Use output redirection: 2> $Null
Get-ADComputer -Server BlahBlah -Identity ComputerThatDoesntExist 2> $Null
Update 1:
Originally, I posted this with the title: "Scripts ignoring error handling in PowerShell module" as that is the current issue, however it seems more of a module issue, so I have renamed the title.
Update 2:
After a comment that made me question Azure cmdlets, I've tested with the most basic of scripts (added to the module) and the findings are the same, in that the error is not passed to the calling script, however, adding -errorVariable to Get-Service does return something (other than WriteErrorException) that I could probably harness in the handling of the error:
function Test-MyError($Variable)
{
Try
{
Get-Service -Name $variable -ErrorAction Stop -ErrorVariable bar
#Get-AzureRmSubscription -SubscriptionName $variable -ErrorAction Stop
}
Catch
{
Write-Error $error[0]
$bar
}
}
returns:
Test-MyError "Foo"
Test-MyError : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:3 char:1
+ Test-MyError "Foo"
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Test-MyError
The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: Cannot find any service with service name 'foo'.
However, if I run "Test-MyError" in ISE, then call the function, I get:
Test-MyError "Foo"
Test-MyError : Cannot find any service with service name 'Foo'.
At line:3 char:1
+ Test-MyError "Foo"
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Test-MyError
The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: Cannot find any service with service name 'Foo'.
So I am not sure what is happening when running "Test-MyError" in ISE and calling it, against it being dot-sourced in the PSM1 file and then calling it?
Do I now have to use -ErrorVariable and handle on that?
Original Question:
I have two functions in a module: Get-Subscription and Get-AllSubscriptions. Each function sits in its own PS1 file and the PSM1 file dot-sources them. The module seems fine as the module scripts are accessible using intelisense and the module loads without issue. I've used this structure in many modules and I haven't come across this problem before. (Although I wonder if MS have changed the way modules work in PS 5.1 as I have noticed using FunctionsToExport='x','y','Z' and Export-ModuleMember don't seem to behave the same way as they used to.)
Get-AllSubscriptions calls Get-Subscription.
If I am not logged into Azure, Get-Subscription should throw an error which is handled, prompting me to log in. This works as expected, if I call Get-Subscription from the Get-Subscription.ps1.
However, when I call Get-Subscription from the a new PS1 file, Get-AllSubscriptions or from the powershell console, it doesn't work. It iterates all the way through the do..until loop, without "handling" the errors as I would expect. On each iteration, it seems to throw a generic error:
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
However, I do see the last error, Get-Subscription : Unable to find requested subscription after 3 login attempts.
If I execute Get-Subscription in ISE, then call Get-Subscription in a new PS1 file or from Get-AllSubscriptions, it works as expected, however, once I re-import the module (Import-Module AzureVnetTools -Force -Verbose), it goes back to the incorrect behaviour.
If I dot-source Get-Subscription, inside the caller script, it works, but why? This is what should happen with the module's PSM1.
Can anyone help me work out what I am doing wrong here?
(PS 5.1, Windows 7)
Get-Subscription:
function Get-Subscription
{
[cmdletbinding()]
Param
(
[string]$SubscriptionName,
[string]$UserName,
[string]$code
)
$c=1
Write-Verbose "Checking access to '$SubscriptionName' with user '$UserName'..."
Do
{
Write-Verbose "Attempt $c"
Try
{
$oSubscription = Get-AzureRmSubscription -SubscriptionName $SubscriptionName -ErrorAction Stop -WarningAction SilentlyContinue
Write-Verbose "Subscription found: $($oSubscription.SubscriptionName)."
}
Catch
{
if($error[0].Exception.Message -like "*Please verify that the subscription exists in this tenant*")
{
Write-Verbose "Cannot find subscription '$SubscriptionName' with provided credentials."
$account = Login-AzureRmAccount -Credential (Get-Credential -UserName $Username -Message "Subscription '$SubscriptionName' user' password:")
}
elseif($error[0].Exception.Message -like "*Run Login-AzureRmAccount to login*")
{
Write-Verbose "No logged in session found. Please log in."
$account = Login-AzureRmAccount -Credential (Get-Credential -UserName $Username -Message "Subscription '$SubscriptionName' user' password:")
}
else
{
Write-Error $error[0]
}
}
$c++
}
until(($oSubscription) -or ($c -eq 4))
if($c -eq 4)
{
Write-Error "Unable to find requested subscription after $($c-1) login attempts."
break
}
$oSubscription | Add-Member -MemberType NoteProperty -Name Code -Value $code
$oSubscription
}
Get-AllSubscriptions:
function Get-AllSubscriptions
{
[cmdletbinding()]
param
(
[string]$MasterSubscription,
[string]$MasterSubscriptionCode,
[string]$MasterSubscriptionUsername,
[string]$ChildSubscription,
[string]$ChildSubscriptionCode,
[string]$ChildSubscriptionUsername
)
Write-Verbose "Getting all subscriptions..."
$oAllSubscriptions = #()
$oMasterSubscription = Get-Subscription -SubscriptionName $MasterSubscription -UserName $MasterSubscriptionUsername -code $MasterSubscriptionCode -Verbose
$oChildSubscription = Get-Subscription -SubscriptionName $ChildSubscription -UserName $ChildSubscriptionUsername -code $ChildSubscriptionCode -Verbose
$oAllSubscriptions = ($oMasterSubscription,$oChildSubscription)
$oAllSubscriptions
}
Test:
$splat2 = #{
SubscriptionName = "SomeSubscription"
Code = "S02"
Username = "some.user#somewhere.com"
}
#Write-Output "Dot-source:"
#. "D:\Temp\PS.Modules\AzureVnetTools\functions\public\Get-Subscription.ps1"
Get-Subscription #splat2 -verbose
Output:
Get-Subscription #splat2 -verbose
VERBOSE: Checking access to 'SomeSubscription' with user 'some.user#somewhere.com'...
VERBOSE: Attempt 1
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
VERBOSE: Attempt 2
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
VERBOSE: Attempt 3
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
Get-Subscription : Unable to find requested subscription after 3 login attempts.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
AzureVnetTools.psm1
#Get public and private function definition files.
$Public = #( Get-ChildItem -Path $PSScriptRoot\Functions\Public\*.ps1 -ErrorAction SilentlyContinue )
$Private = #( Get-ChildItem -Path $PSScriptRoot\Functions\Private\*.ps1 -ErrorAction SilentlyContinue )
#Dot source the files
Foreach($import in #($Public + $Private))
{
#write-error $import.fullname
Try
{
#Write-Host "Dot-sourcing file: $($import.fullname)."
. $import.fullname
}
Catch
{
Write-Error -Message "Failed to import function $($import.fullname): $_"
}
}
Export-ModuleMember -Function $Public.Basename
AzureVnetTools.psd1 (Relevant section):
FunctionsToExport = '*'
-ErrorAction Stop -WarningAction SilentlyContinue
Is it a warning that's being thrown instead of an error?
So my specific problem was that I was relying on handling the error and doing something based on that. The problem was caused by the way PowerShell's Write-Error works (or not) as I learned from the reply here, given by #Alek.
It simply wasn't passing the actual error back to the calling script. As #Alex suggested, I replaced Write-Error with $PSCmdlet.WriteError(). Although this didn't totally work.
In the Catch{} block, I then changed $error[0] to $_ and the full error was returned to the calling script / function.
I went one further and wrote a reusable function, added to my module:
function Write-PsError
{
[cmdletbinding()]
Param
(
[Exception]$Message,
[Management.Automation.ErrorCategory]$ErrorCategory = "NotSpecified"
)
$arguments = #(
$Message
$null #errorid
[Management.Automation.ErrorCategory]::$ErrorCategory
$null
)
$ErrorRecord = New-Object -TypeName "Management.Automation.ErrorRecord" -ArgumentList $arguments
$PSCmdlet.WriteError($ErrorRecord)
}
Which seems to be working well at the moment. I especially like the way intellisense picks up all the ErrorCategories. Not sure what or how ISE (PS 5.1 / Win 7) does that. I thought I was going to have to add my own dynamic parameter.
HTH.
I have a list of hostnames from which I'd like to extract all AppLocker related eventlogs, especially the ones with level warning and/or error.
I crafted this script:
$ComputersToCheck = Get-Content 'X:\ListWithTheNames.txt'
foreach($OneHost in $ComputersToCheck)
{
try
{
$EventCollection = Get-WinEvent -LogName "Microsoft-Windows-AppLocker/EXE and DLL" -ComputerName $OneHost -Credential $CredentialFromUser
foreach ($SingelEvent in $EventCollection)
{
if($SingelEvent.LevelDisplayName -ne "Information")
{
$pathtosaveto = 'SomeFileName.txt'
$ResultString += $SingelEvent | Select Message,MachineName,UserId | Export-Csv -Path $pathtosaveto -Append
}
}
}
catch
{
//handling exceptions
}
}
This works for a while, but after a certain ammount of time I got an error:
Get-WinEvent : The remote procedure call failed
At X:\FileName.ps1:22 char:28
+ $EventCollection = Get-WinEvent -LogName "Microsoft-Windows-AppLocker/EX ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Get-WinEvent], EventLogException
+ FullyQualifiedErrorId : The remote procedure call failed,Microsoft.PowerShell.Commands.GetWinEventCommand
And right after the script start giving errors like this:
Get-WinEvent : The handle is invalid
At X:\FileName.ps1:22 char:28
+ $EventCollection = Get-WinEvent -LogName "Microsoft-Windows-AppLocker/EX ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Get-WinEvent], EventLogException
+ FullyQualifiedErrorId : The handle is invalid,Microsoft.PowerShell.Commands.GetWinEventCommand
My first thought was that it is related to the host the script try to reach, but the next in the list is the same type (Os, even the same model) as the previous.
I ran the script 3 times, and every time the output size was different (probably because not the same hosts were online with the same amount of logs).
The script should run against more than 700 hosts, to which a special account is needed which I prompt by the Get-Credential, store in a variable and pass it the the Get-WinEvent as a parameter.
To be honest I stuck with this issue, not really sure what cause this and why.
If anyone has an idea please share with me :)
Give this a try to attempt catching references to failed hosts and empty objects. You could write the exception received but I didn't include that in this to make the failedhosts file simple to read. Hope I got it right as I winged it and don't have a true case to test against.
$ComputersToCheck = Get-Content 'X:\ListWithTheNames.txt'
foreach($OneHost in $ComputersToCheck) {
try {
$EventCollection = Get-WinEvent -LogName "Microsoft-Windows-AppLocker/EXE and DLL" -ComputerName $OneHost -Credential $CredentialFromUser -ErrorAction Stop
if($EventCollection) {
foreach ($SingelEvent in $EventCollection) {
if($SingelEvent.LevelDisplayName -ne "Information") {
$pathtosaveto = 'SomeFileName.txt'
$ResultString += $SingelEvent | Select Message,MachineName,UserId | Export-Csv -Path $pathtosaveto -Append
}
}
} else {
Out-File -InputObject $($OneHost + " Empty Event Collection") -FilePath "C:\FailedHosts.txt" -Append -Encoding ascii
}
}
catch {
Out-File -InputObject $($OneHost + " Failed Connection") -FilePath "C:\FailedHosts.txt" -Append -Encoding ascii
}
}
Below is a simple script block, the script block works. However, I would like to suppress any errors that the script block would generate.
$Name = 'TEST'
$SB = { param ($DSNName) ;
$conn = new-object system.data.odbc.odbcconnection
$conn.ConnectionString = ('DSN='+ $DSNName)
$conn.open()
$ConState = $conn.State
$conn.Close()
$ConState
}
$test = Start-job -scriptblock $SB -args $Name -RunAs32 -ErrorAction Stop | wait-job | receive-job
What I am trying to get out of this is a simple test for a 32bit ODBC connection. If the connection fails the connection state will remain closed but I also get an exception error I would like to suppress
Exception calling "Open" with "0" argument(s): "ERROR [IM002] [Microsoft][ODBC Driver Manager] Data source name not found and no default driver specified"
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : OdbcException
+ PSComputerName : localhost
If I pipe to out-null my $test variable is empty. When I use a valid DSN Name everything works as desired.
You could use try..catch:
try {
$test = Start-job -scriptblock $SB -args $Name -RunAs32 -ErrorAction Stop | wait-job | receive-job
catch [System.Management.Automation.MethodInvocationException] {
# Do nothing here if you want to suppress the exception completely.
# Although redirecting it to a log file may be a better idea, e.g.
# $Error[0] | Out-File -FilePath "script.log"
}
I want to output a custom error message to a log, when a GWMI query fails. I can catch the exception, but apparently only the last one, because the output in my error file only has the name of the last computer in the list. I am using a list of computers I know do not have WMI enabled. There should be an entry for each one.
I have a list of domain computers in a text file, each on a single line, no trailing characters. I loop through the file to get network information, using GWMI. Some of the computers do not have WMI enabled, and I want to know which ones. My current script just throws a:
gwmi : The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)
At line:12 char:17
+ $base = gwmi win32_networkadapterconfiguration -computername $comp | whe ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Get-WmiObject], COMException
+ FullyQualifiedErrorId : GetWMICOMException,Microsoft.PowerShell.Commands.GetWmiObjectCommand
whenever it loops through a machine that does not have wmi enabled.
This does not identify the computer that threw the exception. What I would like is for every time the RPC server is unavailable... error is thrown, for a custom error message to be written to a log with the name of the computer that threw the exception.
My script:
$computers = Get-Content -path f:\scripts\docs\computer_list_test.txt
if (F:\scripts\wmi_mac_output.txt){
rm F:\scripts\wmi_mac_output.txt
}
foreach ($comp in $computers) {
try
{
$base = gwmi win32_networkadapterconfiguration -computername $comp -ErrorAction Stop | where {$_.dnsdomain -eq "mydomain.com"}
$machine = $base.DNSHostName
$mac = $base.MACAddress
$ip = $base.IPAddress
"<COMPUTER>`n`tname: $machine`n`tMAC: $mac`n`tIP: $ip`n</COMPUTER>" | Out-File F:\scripts\wmi_mac_output.txt
}
catch [Exception]
{
if ($_.Exception.GetType().Name -eq "COMException")
{
"$comp has no winrm" > f:\scripts\docs\error.txt
}
}
}
Thank you.
I can catch the exception, but apparently only the last one, because the output in my error file only has the name of the last computer in the list.
The issue is that the error.txt is being overwritten instead of appended.
"$comp has no winrm" > f:\scripts\docs\error.txt
Change to:
"$comp has no winrm" >> f:\scripts\docs\error.txt