Powershell Try catch write errors to an external txt file - powershell

I have a PowerShell script to disable email forwarding in Office 365 using a CSV file. I want to catch all errors to an external Txt file.
Here's my code:
$Groups |
ForEach-Object {
$PrimarySmtpAddress = $_.PrimarySmtpAddress
try {
# disable Mail forwarding
Set-Mailbox -Identity $PrimarySmtpAddress -ForwardingSmtpAddress $Null
}
Catch {
$PrimarySmtpAddress | Out-File $logfile -Append
}
}
but it doesn't catch errors.
Is there any instruction to catch errors to an external file?
Any solution will be helpful.

From Jeff Zeitlin's and TheMadTechnician's comments, changes noted in inline comments:
$Groups | ForEach-Object {
$PrimarySmtpAddress = $_.PrimarySmtpAddress
Try {
# disable Mail forwarding
#Added ErrorAction Stop to cause errors to be terminating
Set-Mailbox -Identity $PrimarySmtpAddress -ForwardingSmtpAddress $Null -ErrorAction Stop
}
Catch {
#Removed Write-Host as Write-Host writes to the host, not down the pipeline, Write-Output would also work
"$PrimarySmtpAddress" | Out-File $logfile -Append
}
}

Try this:
$ErrorMessage = $_.Exception.Message
#or $ErrorMessage= $Error[0].Exception.Message
if($ErrorMessage -ne $null) {
... Your custom code
}

Exchange Online (a.k.a O365) does not honor the thrown error for your catch statement to work. We have had to set
$global:ErrorActionPreference=Stop
to get the error object to return
Note: We implemented this using the following at beginning of function
$saved=$global:ErrorActionPreference
$global:ErrorActionPreference=Stop
and reverted the setting at the end of the function using
$global:ErrorActionPreference=$saved

Related

Get-WinEvent TimeCreated printing out empty

I'm trying to test a powershell script that finds a specific event viewer task that excludes a certain case. For some reason, it's printing the event created time as empty. I think this is why it's falling into a wrong case. Why is this created time empty? This is an example for this website, so Init variable name doesn't quite make sense below, with chromoting.
#Look for crash within 150 hours of boot, and with Init within 7 minutes before that
$today=[system.datetime](Get-Date)
$startTime=$today.AddHours(-150)
$events = Get-WinEvent -FilterHashtable #{LogName='Application';ProviderName='SlotBroker';StartTime=$($startTime);EndTime=$($today);} -ErrorAction SilentlyContinue
if($events -ne $null)
{
foreach ($event in $events)
{
$crashOccurredTime=$event.TimeCreated
$lookForInitStart = $event.TimeCreated.AddMinutes(-7)
$eventInits = {Get-WinEvent -FilterHashtable #{LogName='Application';ProviderName='chromoting';StartTime=$lookForInitStart;EndTime=$crashOccurredTime;} -ErrorAction SilentlyContinue
}
if($eventInits -ne $null)
{
foreach ($eventInit in $eventInits)
{
#check that didn't have Terminate in that timeframe because we don't want this case
#look for exclude case of Terminate between Init and crash
$initTime = $eventInit.TimeCreated #chromoting
Write-Host "initTime $($initTime)" ##this is blank time
$eventInitTerminate = {Get-WinEvent -FilterHashtable #{LogName='Application';ProviderName='AppMgr';StartTime=$initTime;EndTime=$crashOccurredTime;} -ErrorAction SilentlyContinue | Where-Object {(-PipelineVariable Message -Match 'Terminate function called') -or (-PipelineVariable Message -Match 'Terminate function returned')}
}
if($eventInitTerminate -ne $null)
{ #it always falls in here no matter if it should or not.
Write-Host "Found application.exe after Init with Terminate TimeCreatedCrash $($event.TimeCreated) ProviderName $($event.ProviderName) Message $($event.Message) TerminateTime $($eventInitTerminate.TimeCreated)"
}
else #this will print
{
Write-Host "Found application.exe after Init without Terminate TimeCreated $($event.TimeCreated) ProviderName $($event.ProviderName) Message $($event.Message) InitTime $($eventInit.TimeCreated)"
}
} #foreach
}
}
Looking at the event log, I see this:
Error 8/11/2022 9:43 SlotBroker
Information 8/11/2022 9:37 chromoting
Information 8/11/2022 936 AlarmSoundHelper
This is a test case and should be falling into #this will print, but it prints the above case. I think it's because of the time printing blank, so it finds the Terminate elsewhere in the event log. Why is that time not printing out right? I need the time to see if I need to notify me of the event log or not.
The purpose of this script is to avoid events with Terminate between SlotBroker and chromoting. As you can see, it's not in this case, but falls into that if statement like it found events. We don't have PowerShellISE on this computer with the eventLog, so I can't step through.
When I was working on the script in PowerShellISE on my laptop, it seemed like $eventInit might not know what TimeCreated is, but it's not causing an error. I'm not sure how to get that TimeCreated.
Update:
I added this below the $lookForInitStart and it prints ok
Write-Host "lookForInitStart $($lookForInitStart)"
prints
lookForInitStart 8/11/2022 09:36
I'm unsure why the initTime is printing blank.
I got the $initTime to not be empty with this line and used that for all TimeCreated to make sure they were correct.
$initTime = $eventInit | Select-Object -Expand TimeCreated #chromoting
But it's still falling into the $eventInitTerminate block, even though it doesn't have the Terminate events in there.
That would be helpful if someone else knows why it's falling into the "Found application.exe after Init with Terminate..." printout, and I would accept that answer.
I got the $initTime to not be empty with this line and used that for all TimeCreated to make sure they were correct.
$initTime = $eventInit | Select-Object -Expand TimeCreated #chromoting
For the Terminate block, I changed this to fix it (simplified the match for terminate):
$today=[system.datetime](Get-Date)
$startTime=$today.AddHours(-135)
write-host "startTime $($startTime)"
$events = Get-WinEvent -FilterHashtable #{LogName='Application';ProviderName='SlotBroker';StartTime=$($startTime);EndTime=$($today);} -ErrorAction SilentlyContinue
if($events -ne $null)
{
foreach ($event in $events)
{
$crashOccurredTime=$event | Select-Object -Expand TimeCreated
write-host "crashOccurredTime $($crashOccurredTime)"
$lookForInitStart = $event.TimeCreated.AddMinutes(-7)
Write-Host "lookForInitStart $($lookForInitStart)"
$eventInits = Get-WinEvent -FilterHashtable #{LogName='Application';ProviderName='chromoting';StartTime=$lookForInitStart;EndTime=$crashOccurredTime;} -ErrorAction SilentlyContinue
if($eventInits -ne $null)
{
foreach ($eventInit in $eventInits)
{
#check that didn't have Terminate in that timeframe because we don't want this case
#look for exclude case of Terminate between Init and crash
$initTime = $eventInit | Select-Object -Expand TimeCreated #chromoting
Write-Host "initTime $($initTime)"
$eventInitTerminate = Get-WinEvent -FilterHashtable #{LogName='Application';ProviderName='AppMgr';StartTime=$initTime;EndTime=$crashOccurredTime;} -ErrorAction SilentlyContinue | Where-Object -PipelineVariable Message -Match 'Terminate'
if($eventInitTerminate -ne $null)
{
Write-Host "*****Found application.exe after Init with Terminate TimeCreatedCrash $($event.TimeCreated) ProviderName $($event.ProviderName) Message $($event.Message) TerminateTime $($eventInitTerminate.TimeCreated)*****"
}
else #this will print
{
Write-Host "***Found application.exe after Init without Terminate TimeCreated $($event.TimeCreated) ProviderName $($event.ProviderName) Message $($event.Message) InitTime $($eventInit.TimeCreated)***"
}
} #foreach
}
else
{
Write-Host "No application Crash found after Init in 4 hours preceding shell command"
}
}
}#if
else
{
Write-Host "no events found that meet criteria of crash after init"
}

Powershell problem with values comparison in ARS - false positive

I am updating mass info about users. The script is getting data from a file, comparing with the current data in ARS and changing if necessary.
Unfortunately for two parameters - "st" and "postOfficeBox" - it is updating data all the time altho the data is the same in the file and in AD.
first one is empty, the second one is not
I have checked directly -
PS> $user.$parameters.postofficebox -eq $userQuery.$parameters.postofficebox
True
How can I handle this? It is not an error, but it is annoying and not efficient updating the same data all the time.
#Internal Accounts
$Parameters = #("SamAccountName", "co", "company", "department", "departmentNumber","physicalDeliveryOfficeName","streetAddress","l","st","postalCode","employeeType","manager", "division", "title", "edsvaEmployedByCountry", "extensionAttribute4", "EmployeeID", "postOfficeBox")
#import of users
$users = Import-csv -Path C:\ps\krbatch.csv -Delimiter "," -Encoding UTF8
Connect-QADService -Proxy
#Headers compliance
$fileHeaders = $users[0].psobject.Properties | foreach { $_.Name }
$c = Compare-Object -ReferenceObject $fileHeaders -DifferenceObject $Parameters -PassThru
if ($c -ne $null) {Write-Host "headers do not fit"
break}
#Check if account is enabled
foreach ($user in $users) {
$checkEnable = Get-ADUser $user.SamAccountName | select enabled
if (-not $checkEnable.enabled) {
Write-Host $user.SamAccountName -ForegroundColor Red
}
}
#Main loop
$result = #()
foreach ($user in $users) {
$userQuery = Get-QADUser $user.sAMaccountName -IncludedProperties $Parameters | select $Parameters
Write-Host "...updating $($user.samaccountname)..." -ForegroundColor white
foreach ($param in $Parameters) {
if ($user.$param -eq $userQuery.$param) {
Write-Host "$($user.samaccountname) has correct $param" -ForegroundColor Yellow
}
else {
try {
Write-Host "Updating $param for $($user.samaccountname)" -ForegroundColor Green
Set-QADUser -Identity $user.SamAccountName -ObjectAttributes #{$param=$user.$param} -ErrorVariable ProcessError -ErrorAction SilentlyContinue | Out-Null
If ($ProcessError) {
Write-Host "cannot update $param for $($user.samaccountname) $($error[0])" -ForegroundColor Red
$problem = #{}
$problem.samaccountname = $($user.samaccountname)
$problem.param = $param
$problem.value = $($user.$param)
$problem.error = $($error[0])
$result +=[pscustomobject]$problem
}
}
catch { Write-Host "fail, check if the user account is enabled?" -ForegroundColor Red}
}
}
}
$result | Select samaccountname, param, value, error | Export-Csv -Path c:\ps\krfail.csv -NoTypeInformation -Encoding UTF8 -Append
And also any suggestions to my code, where I can make it better will be appreciated.
Similar to what Mathias R. Jessen was suggesting, the way you are testing the comparison doesn't look right. As debugging approaches either add the suggested Write-Host command or a break point such that you can test at run time.
Withstanding the comparison aspect of the question there's a loosely defined advisory request that I'll try to address.
Why are you you using QAD instead of the native AD module. QAD is awesome and still outshines the native tools in a few areas. But, (without a deep investigation) it looks like you can get by with the native tools here.
I'd point out there's an instance capability in AD cmdlets that allows for incremental updates even without comparison... ie you can run the Set-ADUser cmdlet and it will only write the attributes if they different.
Check out the help file for Set-ADUser
It would be inappropriate and time consuming for me to rewrite this. I'd suggest you check out those concepts for a rev 2.0 ... However, I can offer some advice bounded by the current approach.
The way the code is structured it'll run Set-QADUser for each attribute that needs updating rather than setting all the attributes at once on a per/user basis. Instead you could collect all the changes and apply in a single run of Set-QADUser per each user. That would be faster and likely have more compact logging etc...
When you're checking if the account is enabled you aren't doing anything other than Write-Host. If you wanted to skip that user, maybe move that logic into the main loop and add a Continue statement. That would also save you from looping twice.
Avoid using +=, you can use an [ArrayList] instead. Performance & scalability issues with += are well documented, so you can Google for more info. [ArrayList] might look something like:
$result = [Collections.ArrayList]#()
# ...
[Void]$result.Add( [PSCustomObject]$problem )
I'm also not sure how the catch block is supposed to fire if you've set -ErrorAction SilentlyContinue. You can probably remove If($ProcessError)... and and move population of $Result to the Catch{} block.

Powershell extract error from a text file on different computers

I am trying to extract a list of computer having an error in a specific text file. I did the following :
$computernames = Get-Content C:\PC.txt
foreach ($server in $computernames)
{
$filepath = Test-Path "\\$server\c$\"
if ($filepath -eq "True") {
Get-Content "\\$server\c$\file.log" | Select-String "Error" -quiet | Select-Object $server
}
}
That is returning me the first computer with an error, then only some blank lines in the command window.
If I try to get it in a file with out-file or extract or whatever, I have a blank file.
Thanks in advance for any clue on the matter.
Regards.
There are multiple things you can do with this. Let me tell you first what you are looking for:
So lets say you have an error , in order to capture that you should use try/catch.
Replace :
Get-Content "\\$server\c$\file.log" | Select-String "Error" -quiet | Select-Object $server
With This:
try
{
Get-Content "\\$server\c$\file.log" | Select-String "Error" -quiet | Select-Object $server
}
catch
{
$Error >> ErrorFile.txt
}
Second thing, I would suggest is. Lets say you have 10 computers and the lets assume the 8th one is having some error. But still you do not wish to stop the loop and proceed with the remaining computers of the loop. In that case you should use 2 try/catch like this:
$computernames = Get-Content C:\PC.txt
try
{
foreach ($server in $computernames)
{
$filepath = Test-Path "\\$server\c$\"
try{
if ($filepath -eq "True") {
try
{
Get-Content "\\$server\c$\file.log" | Select-String "Error" -quiet | Select-Object $server
}
catch
{
$Error >> ErrorFile.txt
}
}
}
catch
{
$_.Exception.Message >> Errorfile.txt
}
}
}
catch
{
$_.Exception.Message >> Errorfile.txt
}
So the approach is, if there is any error inside the foreach then the internal catch block will catch it and the error message will be parsed to the errorfile.txt keeping the loop running and will proceed with the remaining computers.
If there is some issue with the main $computernames while getting the content of the file, then it will come to the main catch block with the respective error message parsed and appended in the errorfile.txt
Hope it helps you in understanding the effectiveness of try/catch

Exchange Management Shell If Else Variable

I want to enable the Exchange Mailbox from users which are in the organization unit "Test". I created this script:
& 'C:\Program Files\Microsoft\Exchange Server\V15\bin\RemoteExchange.ps1' Connect-ExchangeServer -auto
$timestamp = Get-Date -Format G
$enablemailboxscript = Get-User -OrganizationalUnit "Test" -RecipientTypeDetails User | Enable-Mailbox -Database "Mailbox Database 2_1"
if([string]::IsNullOrEmpty($enablemailboxscript)) {
Write-Output "$timestamp
angelegte Benutzer: $enablemailboxscript" | Out-File D:\Administration\EnableMailboxScript.log -append
} else {
Exit 1
}
My problem is, that the script always create a log record, the variable $enablemailboxscript is empty then. I want that the script only log, when there is a new enabled mailbox.
You are testing the wrong way, you should use
if($enablemailboxscript) { ... }
or
if(![string]::IsNullOrEmpty($enablemailboxscript)) { ... }

(PowerShell) Log Exceptions and Continue (Active Directory Module)

I'm using New-ADUser and Add-ADGroupMember
If the user already exists or is already in the group then the functions throw exceptions (which are expected and not a problem).
How do I log the exceptions to file and keep going?
Redirection is not working - the exceptions always go to the
console.
-ErrorAction is not working - exceptions still go to the console
Try / Catch works, but execution stops and the rest of the commands don't run
I can do a Try / Catch for every single statment, but that seems
ridiculous
You can combine -ErrorAction SilentlyContinue with -ErrorVariable:
$e = $null
New-ADUser iExist -ErrorAction SilentlyContinue -ErrorVariable e
$e # contains the error
You can also use the built in $Error variable, which is a circular buffer holding all the errors.
$ErrorPreference = SilentlyContinue # I don't like this personally
New-ADUser iExist
Add-ADGroupMember iExist iForgotTheParameters
$Error[0] # The Add-ADGroupMember error
$Error[1] # The New-ADUser error
So you could set your $ErrorPreference, do a bunch of commands, and at the end of it all, do something like $Error | Out-File -Path errors.txt.
Have a look at PowerShell Error Handling and Why You Should Care for more ideas.
The simplest way to accomplish this is probably using the trap construct:
function Test-Trap{
trap {
$_ | Out-String | Out-File C:\path\to\errors.txt -Append
}
Get-ADUser -NoSuchParam "argument"
Write-Host "Show must go on"
nonexistingcommand
Write-Host "Still executing"
}
When you call Test-Trap, you'll see that after the error has been written to the console, the trap is executed, and the rest of the execution flow is resumed:
And the error record output as it would normally appear on screen (courtesy of Out-String) has been saved to the file:
You could add cool features like timestamps and stack traces to your trap:
function Test-Trap{
trap {
$LogPath = "C:\path\to\errors.txt"
$ErrorCount = $ErrorCount + 1
$("[Error {0} trapped {1}]:" -f $ErrorCount,(Get-Date -Format "dd/MM/yyyy HH:mm:ss.fff")) | Out-File $LogPath -Append
$_ | Out-String | Out-File $LogPath -Append
if(($st = $_.Exception.Stacktrace)){ $st |Out-File $LogPath -Append }
$("[Error {0} logged]" -f $ErrorCount)| Out-File $LogPath -Append
}
Provoke-Error -NoSuchParam muhahaha
}