Powershell appears to run statements asynchronously? [duplicate] - powershell

This question already has answers here:
PowerShell output is crossing between functions
(1 answer)
weird delay of the output of an object when followed by start-sleep (or until script end)
(3 answers)
Closed 2 years ago.
I wrote a powershell cmdlet that works great on Server 2012 R2 (powershell 4.0), however on Windows 10 or Server 2016 (powershell 5.1) the commands do not appear to wait for each other to finish, but rather execute asynchronously (?). This is certainly not desired behavior and is causing the cmdlet to not function as intended.
The core of the script starts a transcript, runs Get-ADPrincipalGroupMembership followed by Get-ADUser and then Get-Date, and finally closes the transcript.
try {
Start-Transcript -Path $transactionFilename
Write-Host "GROUP MEMBERSHIP FOR $($targetUsername)"
Get-ADPrincipalGroupMembership -Credential $credential -Identity $Username -Server $domainServer | select name,distinguishedName | format-table
Write-Host "ACCOUNT PROPERTIES FOR $($targetUsername)"
Get-ADUser -Credential $credential -Identity $Username -Server $domainServer -Properties *
Write-Host "CURRENT TIME"
(Get-Date).DateTime
} catch {
} finally {
Stop-Transcript
write-host "Transcript is available at"
write-host $transactionFilename
$Host.UI.RawUI.BufferSize = New-Object Management.Automation.Host.Size ($originalHostWidth, $hostHeight)
}
When run on PS 4.0 each statement is executed in order - each one waiting for the previous to finish.
👍
When run on PS 5.1 the Get-ADPrincipalGroupMembership finishes, then the Write-Host "ACCOUNT PROPERTIES" runs then Write-Host "CURRENT TIME" runs, then everything in the finally block runs then the Get-ADUser and Get-Date commands run.
👎
As you can imagine, having Stop-Transcript run in the middle of the script is a show-stopper!
I've Googled for stopping a cmdlet from executing asynchronously, but all the articles are about how to make it execute async - not how to stop it. I'm not sure where to look for help now.
How can I adjust powershell 5.1 to run the statements synchronously? Backwards compatibility with 4.0 is not strictly necessary, but would be a bonus.

Per comments from #Lee_Dailey and #js2010 I was able to modify the script to function as desired by piping the output from format-table and Get-ADUser to Out-Host:
try {
Start-Transcript -Path $transactionFilename
Write-Host "GROUP MEMBERSHIP FOR $($targetUsername)"
Get-ADPrincipalGroupMembership -Credential $credential -Identity $Username -Server $domainServer | select name,distinguishedName | format-table | out-host
Write-Host "ACCOUNT PROPERTIES FOR $($targetUsername)"
Get-ADUser -Credential $credential -Identity $Username -Server $domainServer -Properties * | out-host
Write-Host "CURRENT TIME"
(Get-Date).DateTime
} catch {
} finally {
Stop-Transcript
write-host "Transcript is available at"
write-host $transactionFilename
$Host.UI.RawUI.BufferSize = New-Object Management.Automation.Host.Size ($originalHostWidth, $hostHeight)
}

Related

PowerShell Invoke-Command Speed (win32_product)

I wrote a short script to uninstall a program on multiple computers (from a text doc of hostnames). The script is working as expected but is taking about 2-3 minutes per host. Is there a way to perform this on all the machines simultaneously?
Here's my script.
$computers = Get-Content C:\Computernames.txt
foreach($Computer in $computers){
Invoke-Command -ComputerName $Computer -ScriptBlock{
$application = Get-WmiObject -Class Win32_Product -Filter "Name LIKE '%Appname%'"
#uninstall the app if it exists
if($application){
$application.Uninstall()
Write-Output "Application uninstalled successfully.."
}
else{
Write-Output "Application not found.."
}
}
}
Can I do Invoke-Command -ComputerName $Computers and do all machines simultaneously to avoid looping through?
As suggested, using $Computers worked successfully. I was able to get rid of my loop and speed the script up tremendously.
Here's the updated script - thanks for letting me know it supports arrays.
#list of computers
$computers = Get-Content C:\Computernames.txt
Invoke-Command -ComputerName $computers -ScriptBlock{
$application = Get-WmiObject -Class Win32_Product -Filter "Name LIKE '%appname%'"
if($application){
$application.Uninstall()
Write-Output "Successful uninstall on $env:COMPUTERNAME "
}
else{
Write-Output "Application not found on $env:COMPUTERNAME"
}
}
The win32_product class is notoriously slow, because it verifies every msi whenever it's used. I assume appname is replaced with a specific name. You can use the little known get-package and uninstall-package in powershell 5.1, but only with msi providers:
get-package appname | uninstall-package

Skip part of script on error

I've been banging my head against the wall for awhile on this one. No amount of Googling has yielded me any successful results thus far. Wondering if someone can give me a hand?
# Load the PowerShell module for Active Directory
Import-Module ActiveDirectory
# For each computer in AD that is a member of the target group
Get-ADGroupMember -Identity "CN=RenameComputer,CN=Users,DC=int,DC=example,DC=com" | ForEach {
# Define the new name as the old one, minus one letter at the end
$NewComputerName = $_.Name -replace ".$"
# Perform the rename operation
Rename-Computer -ComputerName $_.Name -NewName $NewComputerName -Force -PassThru -Restart -WhatIf
# Remove the computer from the target group
# THIS SHOULD NOT OCCUR IF THE RENAME ABOVE FAILED!!!
Remove-ADGroupMember -Identity "CN=RenameComputer,CN=Users,DC=int,DC=example,DC=com" -Members $NewComputerName -WhatIf
}
TL;DR: This script finds a bunch of computers in a designated group, renames them, and then removes them from the group. I need help telling the script to NOT remove them from the group in the event that the rename fails (machine is offline, etc.) or throws some error.
Thoughts?
Thanks in advance!
There are a few ways you can check the result of a cmdlet in Powershell.
Run the cmdlet as an if statement condition
You can use the returned object of a cmdlet as an if statement condition, as long as it returns an object on success (which is why we need -PassThru for Rename-Computer, as normally it doesn't return an object). So to use your code as an example (and removing the -WhatIf):
# If Rename-Computer succeeds...
if( Rename-Computer -ComputerName $_.Name `
-NewName $NewComputerName -Force -PassThru -Restart ) {
# then remove the Computer from group
Remove-ADGroupMember -Identity "CN=RenameComputer,CN=Users,DC=int,DC=example,DC=com" `
-Members $NewComputerName
}
This works because objects are truthy in Powershell. Most defined objects and any number other than 0 (zero) are considered $True, but values like $null, "" (an empty string), and 0 (zero) are evaluated as $False. So in the case of your code, if it succeeds an object will be returned ($True), if an error occurs there will be no object returned ($False).
Explicitly check cmdlet result
Alternatively, you could run Rename-Computer first, then check its success:
Rename-Computer -ComputerName $_.Name -NewName $NewComputerName -Force
# Check success of last cmdlet
if( $? ) {
Remove-ADGroupMember -Identity "CN=RenameComputer,CN=Users,DC=int,DC=example,DC=com" `
-Members $NewComputerName
}
$? evaluates the success of the last cmdlet run (don't confuse this with $LASTEXITCODE, which is for checking the last run program result, not cmdlet result). If the cmdlet succeeded, it returns $True, if not, $False.
Catch Errors in a Try/Catch Block
A third way to do it would be to use a try/catch block, though you have to make sure that any error is terminating (will stop the script if uncaught) to catch an exception. You can use the -ErrorAction Stop paremeter for this:
try {
Rename-Computer -ComputerName $_.Name -NewName $NewComputerName -Force `
-PassThru -Restart -ErrorAction Stop
Remove-ADGroupMember -Identity "CN=RenameComputer,CN=Users,DC=int,DC=example,DC=com" `
-Members $NewComputerName
} catch {
# Do something on failure
Write-Warning "An error occurred: $($_.Exception.Message)"
}
If Rename-Computer throws an error within the try block, which it will on failure, the execution jumps to the catch block and will skip over Remove-ADGroupMember. If using a try/catch is your preferred method of error handling, you may consider setting $ErrorActionPreference = 'Stop' in your scripts, which sets the default -ErrorAction behavior to Stop, and you don't have to specify it on every cmdlet.
Additional Information
Here are some official docs about try/catch/finally and if statements in Powershell. I did not cover finally above but this is an optional part of try/catch statements:
About If: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_if?view=powershell-6
About Try/Catch/Finally: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_try_catch_finally?view=powershell-6
Here is also some advanced information about exceptions, errors, and handling them in Powershell: https://kevinmarquette.github.io/2017-04-10-Powershell-exceptions-everything-you-ever-wanted-to-know/

Log each Powershell process to text file through

Firstly, I'm by no means a PS expert, total newbie - admission done. I have scoured the internet for what I need in order to get the script to do what I want, but I've reached a point where I'm struggling and in need of help.
Basically, I've created a script using ISE that grabs the users in an AD OU, processes them by disabling the accounts, renaming them, stripping out the groups and moving them to another folder. In order to automate the deactivation process for users. But I now need to create a log file every time this runs, to show a) if it found any Users in the original OU (ToBeProcessed) and b) what processes were run and if they were successful. Here is the code.
$OUToBeProcessed = "OU=ToBeProcessed,OU=Users,OU=World,DC=local"
$OURetired = "OU=RetiredUsers,OU=Users,OU=World,DC=local"
$Users = Get-ADUser -SearchBase $OUToBeProcessed -Filter 'name -Like "*"' -Properties MemberOf
ForEach($User in $Users){
$SAN = $User.SamAccountName
#Disable user account
Disable-ADAccount -Identity $SAN
#Remove membership from groups for user
$User.Memberof | Remove-ADGroupMember -Member $User -Confirm:$False
$NewDN = "zzz_" + $User.Name
#Change display name
set-aduser $User -Displayname $newDN -ErrorAction SilentlyContinue
#Change distinguished name
Get-ADUser $SAN | Rename-ADObject -Newname $NewDN
Write-Host "$SAN may already exist."
#Move account to RetiredUsers
Get-Aduser $SAN | Move-ADObject -TargetPath $OURetired
}
I'm assuming I'll need to either use a Write-Output or Log-File cmdlet, though someone had also suggested Transcript, but I don't think that's what I need.
I've tried a number of ways to incorporate the Write-Output into the script, it runs without errors, but no text file is produced. But I'm placing it within the loop which may be the issue. I've placed it outside the loop but I think because it's not being passed anything it's creating the file with nothing in it. Would really appreciate some help as to where the Write-Output might need to go if that is the right cmdlet.
Personally I tend to add a Log function to my scripts. Something like this (where I output to the host and file):
Function Log {
Param (
[Parameter(Mandatory=$true)] [string] $String,
[Parameter(Mandatory=$true)] [string] $LogFilePath,
[Parameter(Mandatory=$false)][ValidateSet("ERROR","WARN","INFO","DEBUG")] [string] $Level = "INFO"
)
$LogString = ((Get-Date -Format "s") +" $Level $env:USERNAME $String")
Write-Host $LogString
Out-File -Append -FilePath $LogFilePath -InputObject $LogString
}
Then you could do logging:
Log "Something wrong!" "c:\mylog.log" "WARN"
Log "Updated stuff" "c:\mylog.log"
Or search the http://www.powershellgallery.com/ for logging modules.
Example (haven't tried this one myself):
https://www.powershellgallery.com/packages/PSLogging/2.5.2

Powershell script Memory usage

I'm running a simple script to get all inactive users (enabled state) not logged in for 60 days as below. The script seems to work fine on my workstation with ARS 6.7 & WinXP . But if i run the same script on another workstation with ARS 6.9 & Win7 the RAM usage of the script goes on increasing over time & it finally throws OutofMemory exception (after reaching 1.5+ GB) & aborts. The same script when run on WinXP workstation (ARS v6.7) does not consume more than 50 MB of RAM throughout. The domain i'm scanning is pretty big with over 550000 accounts. I'm totally confused about the issue here...Pls help!
[datetime]$TodayDate = Get-Date
[datetime]$InActivityDate = $TodayDate.AddDays(-62).Date
try
{
Get-QADUser -SearchRoot $SearchOU -Service $Service -SizeLimit $SizeLimit -PageSize 1000 -Enabled -DontUseDefaultIncludedProperties -IncludedProperties SamAccountName,`
Name,ParentContainer,DN,LastLogon,WhenCreated,PasswordLastSet,employeeID,`
employeeNumber,Manager,AccountIsDisabled,co,scriptPath |`
#Filter out inactive accounts
Where-Object {$_.LastLogonTimeStamp -lt $InActivityDate} |`
Select-Object SamAccountName,Name,ParentContainer,DN,LastLogon,WhenCreated,`
PasswordLastSet,employeeID,employeeNumber,Manager,`
AccountIsDisabled,co,scriptPath | Export-Csv $OutputFile -NoTypeInformation
}
catch
{
$ErrorMessage = $_.Exception.Message
$ErrTime = Get-Date
Write-Host "Error occured:`n$ErrorMessage" -ForegroundColor Red
Write-Output "[$ErrTime] Error occured:`n$ErrorMessage" | Out-File $OutputFile -Append
}
You should delegate filtering to AD side, quoting from Get-ADUser help: Get-ADUser -filter { lastLogon -le $logonDate } In your case, Get-QADUser cannot delegate filtering, so swap to Microsoft's AD module: Get-ADUser -filter { lastLogon -le $InactivityDate }. To get the module, install RSAT for oyur Windows version, then (if not enabled by default) go "Add/remove Windows components - RSAT - Role administration services - AD/LDAP - Windows Powershell module" and enable it.
Launching the Quest Shell in MTA mode has solved this problem. This can simply be done by adding "-mta" parameter to existing shortcut.
eg.
C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe
-psconsolefile "C:\Program Files\Quest Software\Management Shell for AD\ConsoleSettings.psc1" -noexit -Mta -command ". 'C:\Program
Files\Quest Software\Management Shell for AD\qsft.ps1'"

powershell variable won't be used in second part of script

I'm learning power shell and I have problem with this code. when I parse it, it all works, but not together. what could be the problem? Thank you for answer.
$hotfix = read-host "Enter hotfixID"
Start-Process firefox.exe (get-hotfix |
Where-Object -filter {$_.hotfixID -eq $hotfix} |
Select-Object -ExpandProperty Caption)
Your script works correctly here. Note that I don't have Firefox installed, but it works fine with iexplore. What issue are you experiencing?
Also, as #Colyn1337 stated, you do not need to use Where-Object; you can simplify this script as follows:
$Hotfix = Read-Host "Enter Hotfix ID"
Start-Process firefox.exe
(
Get-HotFix -Id "$Hotfix" |
Select-Object -ExpandProperty Caption
)
EDIT: As discussed in the below comments, the issue was that the arguments do not work when called via powershell.exe -command scriptname. The solution would be to pass the arguments implicitly via ArgumentList:
$Hotfix = Read-Host "Enter Hotfix ID"
Start-Process firefox.exe -ArgumentList `
(
Get-HotFix -Id "$Hotfix" |
Select-Object -ExpandProperty Caption
)
To get a specific hotfix, you'd want to try something lik this:
$hotFix = Read-Host "Enter hotfixID"
Get-Hotfix -Id $hotFix
If I understand what you're trying to do, creating a browser process is not necessary.