Multithread PowerShell script - powershell

I have a PowerShell script that checks if a file is present in a folder. The problem is: This script works as it should, but it's very slowly. I must check 10K Pcs for a statistic / day.
I want to use Invoke-Command, but I can't use it because not all clients have enabled WinRM.
Is it possible to make this script multithread without WinRM?
Here is my code:
function Show-Menu {
param (
[string]$Title = 'Debug'
)
cls
Write-Host "================ $Title ================"
Write-Host "Ziel Clients werden in der Datei C:\temp\srv.txt angegeben!" -ForegroundColor Red
Write-Host "1: Detailansicht alle Clients" -ForegroundColor Green
Write-Host "2: Today Crash" -ForegroundColor Green
Write-Host "3: Detailansich einzelner Client" -ForegroundColor Green
Write-Host "Q: 'Q' zum beenden."
Write-Host "Script wird ausgefuehrt als:"
whoami
}
do {
Show-Menu
$input = Read-Host "Nummer angeben"
switch ($input) {
'1' {
cls
Write-Host "Detailansicht alle Clients"
$computers = Get-Content C:\temp\srv.txt
foreach ($computer in $computers) {
Write-Host -foregroundcolor "green" "Verarbeite $computer..."
if ( ! (Test-Connection $computer -Count 1 -Quiet)) {
Write-Host -foregroundcolor "red" "$computer ist offline"
continue
}
$path = Test-Path "\\$computer\c$\Program Files\Oracle\Runtime\BIN\ifrun60_*" -Include *dump*
Get-Item "\\$computer\c$\Program Files\Oracle\Runtime\BIN\ifrun60_*"
If ($path -eq $true ) { Write-Host $computer 'Dumps are present' }
Else { Write-Host $computer 'Dumps are not present' }
pause
}
}
'2' {
cls
Write-Host "Today Crash"
$computers = Get-Content C:\temp\srv.txt
foreach ($computer in $computers) {
Write-Host -foregroundcolor "green" "Verarbeite $computer..."
if ( ! (Test-Connection $computer -Count 1 -Quiet)) {
Write-Host -foregroundcolor "red" "$computer ist offline"
continue
}
$result = Get-ChildItem -Path "\\$computer\c$\Program Files\Oracle\Runtime\BIN\ifrun60_*" | Where-Object { $_.LastWriteTime -ge (Get-Date).Date }
}
$result | Out-GridView
}
'3' {
cls
Write-Host "Detailansich einzelner Client"
$computer = Read-Host -Prompt 'Client angeben'
$result = Get-ChildItem -Path "\\$computer\c$\Program Files\Oracle\Runtime\BIN\ifrun60_*"
$result | Out-GridView
}
}
}
until ($input -eq 'q')

While background jobs - started via Start-Job - do permit running in parallel, they run in child processes, and are therefore both resource-intensive and slow; furthermore, you cannot throttle their use, i.e. you cannot (directly) control how many child process at most are permitted to run simultaneously
Thread jobs, by contrast, run as threads in-process and therefore require fewer resources and are much faster than child-process-based background jobs; furthermore, throttling (limiting the number of threads permitted to run simultaneously) is supported.
Thread jobs are started via the Start-ThreadJob cmdlet, which comes with PowerShell [Core] v6+ and in Windows PowerShell can be installed on demand with, e.g., Install-Module ThreadJob -Scope CurrentUser.
You simply call Start-ThreadJob instead of Start-Job, and use the standard *-Job cmdlets to manage such thread jobs - the same way you'd manage a Start-Job-launched background job.
In the simplest case, you can simply wait for all threads to complete:
# PowerShell [Core] 6+
# Windows PowerShell with the ThreadJob module installed.
Get-Content C:\temp\srv.txt | ForEach-Object {
$computer = $_
Start-ThreadJob { # create a thread job and output an object representing it
Write-Host -ForegroundColor Green "Processing $using:computer..."
# ...
Get-Item "\\$using:computer\c$\Program Files\Oracle\Runtime\BIN\ifrun60_*"
# ...
}
} | Receive-Job -Wait -AutoRemoveJob
Note the use of the $using: scope specifier, which is needed to access the caller's $computer value; this requirement applies equally to background jobs, thread jobs, and remoting.
By default, up to 5 threads are allowed to run simultaneously; you can use the -ThrottleLimit parameter to modify this value, but note that increasing this value beyond what your hardware can support can actually slow things down.
Output sequencing isn't guaranteed; that is, the outputs aren't guaranteed to correspond to the order in which the computer names were specified.
In PowerShell 7+, there's an even easier way to run threads in parallel, via ForEach-Object's
-Parallel parameter:
# PowerShell 7+
Get-Content C:\temp\srv.txt | ForEach-Object -Parallel {
Write-Host -foregroundcolor Green "Processing $_..."
# ...
Get-Item "\\$_\c$\Program Files\Oracle\Runtime\BIN\ifrun60_*"
# ...
}
The comments above regarding output sequencing and throttling apply equally here, but note that the computer names are now provided as input via the pipeline, so the usual automatic $_ variable can be used inside the thread script block.

Related

How to return a list from ForEach-Object in powershell 7.2.1

I have a library of Powershell 5.1 scripts and I want to rewrite some of them in Powershell 7.2.1. Mainly because of the new parallel execution of ForEach-Object.
Here is simplified example of script written in Powershell 5.1 that Test-Connection ForEach-Object in $computers list and add pc either to $OnlinePc list or $OfflinePc list.
$color = "Gray"
$Major = ($PSVersionTable.PSVersion).Major
$Minor = ($PSVersionTable.PSVersion).Minor
Write-Host "My Powershell version: " -NoNewline -ForegroundColor $color
Write-Host "$Major.$Minor"
Write-Host
$computers = #(
'172.30.79.31',
'172.30.79.32',
'172.30.79.33',
'172.30.79.34',
'172.30.79.35',
'172.30.79.36',
'172.30.79.37'
)
Write-Host "List of all computers:" -ForegroundColor $color
$computers
foreach ($pc in $computers) {
if (Test-Connection -Count 1 $pc -ErrorAction SilentlyContinue) {
$OnlinePc+=$pc
}
else {
$OfflinePc+=$pc
}
}
Write-Host
Write-Host "List of online computers:" -ForegroundColor $color
$OnlinePc
Write-Host
Write-Host "List of offline computers:" -ForegroundColor $color
$OfflinePc
Write-Host
pause
And here is same script rewtitten in Powershell 7.2.1
$color = "Gray"
$Major = ($PSVersionTable.PSVersion).Major
$Minor = ($PSVersionTable.PSVersion).Minor
$Patch = ($PSVersionTable.PSVersion).Patch
Write-Host "My Powershell version: " -NoNewline -ForegroundColor $color
Write-Host "$Major.$Minor.$Patch"
Write-Host
$computers = #(
'172.30.79.31',
'172.30.79.32',
'172.30.79.33',
'172.30.79.34',
'172.30.79.35',
'172.30.79.36',
'172.30.79.37'
)
Write-Host "List of all computers:" -ForegroundColor $color
$computers
$computers | ForEach-Object -Parallel {
if (Test-Connection -Count 1 $_ -ErrorAction SilentlyContinue) {
$OnlinePc+=$_
}
else {
$OfflinePc+=$_
}
}
Write-Host
Write-Host "List of online computers:" -ForegroundColor $color
$OnlinePc
Write-Host
Write-Host "List of offline computers:" -ForegroundColor $color
$OfflinePc
Write-Host
pause
Here is picture of output from both scripts.
Outputs
I tried to edit the ForEach-Object syntax in many ways, but I can't get it to work the same way it worked in 5.1, any tips would be apperacited.
PS: Computers 172.30.79.32 and 172.30.79.33 are offline, the others are online.
As commenters noted, script blocks in a ForEach-Object -Parallel don't have direct access to surrounding variables as they run in isolated runspaces.
While you could use the $using keyword to work around this situation (as show in this QA), a more idiomatic approach is to capture the output of ForEach-Object in a variable. This automatically produces an array if more than one objects are output. By storing the online state in a property, we can later split that array to get two separate lists for online and offline PCs.
$computerState = $computers | ForEach-Object -Parallel {
[pscustomobject]#{
Host = $_
Online = Test-Connection -Count 1 $_ -ErrorAction SilentlyContinue -Quiet
}
}
Write-Host
Write-Host "List of online computers:" -ForegroundColor $color
$computerState.Where{ $_.Online -eq $true }.Host
Write-Host
Write-Host "List of offline computers:" -ForegroundColor $color
$computerState.Where{ $_.Online -eq $false }.Host
Notes:
[pscustomobject]#{…} dynamically creates an object and implicitly outputs it, which gets captured in $computerState, automatically creating an array if necessary. This is more efficient than using the array += operator, which has to reallocate and copy the array elements for each new element to be added, because arrays are actually of fixed size (more info).
Parameter -Quiet is used so that Test-Connection outputs a [bool] value to be stored in the Online property.
.Where{…} is an intrinsic method that PowerShell provides for all objects. Similarly to Where-Object it acts as a filter, but is faster and the syntax is more succinct.
Finally by writing .Host we make use of PowerShell's convenient member access enumeration feature, which creates an array from the Host property of the filtered $computerState items.
When using ForEach-Object -Parallel each object is processed in a different runspace and thread (as mentioned by mclayton in a comment). Variables are not accessible across runspaces in most cases. Below are 2 possible alternative ways to use -Parallel
The following code does not assign or use any variables inside the Foreach-Object -Parallel block. Instead online computers are assigned to a variable outside.
$computers = #(
'localhost',
'www.google.com',
'notarealcomputer',
'www.bing.com',
'www.notarealwebsitereally.com'
)
# Assign the output of foreach-object command directly into a variable rather than assigning variables inside
$onlinePc = $computers | ForEach-Object -Parallel {
$_ | Where-Object { Test-Connection -Count 1 -Quiet $_ -ErrorAction SilentlyContinue }
}
# determine offline computers by checking which are not in $onlinePc
$offlinePc = $computers | Where-Object { $_ -notin $onlinePc }
Write-Host '--- Online ---' -ForegroundColor Green
$onlinePc
Write-Host
Write-Host '--- Offline ---' -ForegroundColor Red
$offlinePc
This next method uses a synchronized hashtable and the $using statement to collect the data inside the -Parallel block
$computers = #(
'localhost',
'www.google.com',
'notarealcomputer',
'www.bing.com',
'www.notarealwebsitereally.com'
)
# Create a hashtable containing empty online and offline lists
$results = #{
'online' = [System.Collections.Generic.List[string]]::new()
'offline' = [System.Collections.Generic.List[string]]::new()
}
# thread-safe wrapped hashtable
$syncResults = [hashtable]::Synchronized($results)
$computers | ForEach-Object -Parallel {
if ($_ | Test-Connection -Quiet -Count 2 -ErrorAction SilentlyContinue) {
($using:syncResults).online.add($_)
}
else {
($using:syncResults).offline.add($_)
}
}
$results
Output
Name Value
---- -----
online {localhost, www.bing.com, www.google.com}
offline {www.notarealwebsitereally.com, notarealcomputer}
I do not claim to be an expert. There are likely better ways to do this. Just thought I'd share a couple that I know.

Deactivating all "ready/running Tasks" on a bunch of servers in Specific Folders with the ability to activate those tasks again

i am looking for a Powershell script that can disable all tasks that are "ready" or "running" in a specific folder in the Task Scheduler on 3 or more servers.
After we updated the software i should be able to activate all the tasks that were disabled by the script again, not just activate all disabled scripts, but specificly the ones that were disabled by the script.
I know this should be possible, but i am not capable of assembling the single parts. Everything thats more than a single command is to much for my logic capacitys.
<#getScheduledTasksinfo-FreeholdReboots.ps1
.Synopsis
PowerShell script to list all Scheduled Tasks, the User ID and the State
.DESCRIPTION
This script scans the content of the c:\Windows\System32\tasks and searches the UserID XML value.
The output of the script is a comma-separated log file containing the Computername, Task name, UserID, Status.
.Author
UNKNOWN
.Tweaker
Patrick Burwell
#>
Remove-Item -Force "D:\batch\Logs\Maintenance\$day-SchedTasks-Reboots.csv"
$logfilepath = "D:\batch\Logs\Maintenance\$day-SchedTasks-Reboots.csv"
$ErrorActionPreference = "SilentlyContinue"
$serverlist = gc D:\batch\input\servers.txt
foreach($server in $serverlist){
if($server -like '#*')
{
continue
}
Write-Host $server
$path = "\\" + $server + "\c$\Windows\System32\Tasks"
$tasks = Get-ChildItem -Path $path -File
if ($tasks)
{
Write-Verbose -Message "I found $($tasks.count) tasks for $server"
}
foreach ($item in $tasks)
{
if($item -like 'Optimize Start Menu*'){continue}
if ($item -like "User_Feed_Synchronization*"){continue}
if($item -like 'ComputeSensorWatchDog*'){continue}
if ($item -like "Google*"){continue}
$AbsolutePath = $path + "\" + $item.Name
$task = [xml] (Get-Content $AbsolutePath)
$states = (Get-ScheduledTask -Verbose -TaskPath '\' -TaskName "reboot")
[STRING]$check = $task.Task.Principals.Principal.UserId
[STRING]$state = $states.State
if ($task.Task.Principals.Principal.UserId)
{
if($item -ilike "*reboot*"){
Write-Verbose -Message "Writing the log file with values for $server"
Add-content -path $logfilepath -Value "$server,$item,$check,$state"
}
else {continue}
}
}
}

Detect and uninstall antivirus

I have been trying to make a powershell script to detect what antivirus software is installed, and then uninstall it.
I have been able to detect what antivirus is installed using WMI.
I cant find a way to uninstall antivirus software via powershell however.
Is there a way to do this?
Hope you guys can help.
The script i use to detect antivirus:
function Get-AntivirusName {
[cmdletBinding()]
param (
[string]$ComputerName = "$env:computername" ,
$Credential
)
BEGIN
{
$wmiQuery = "SELECT * FROM AntiVirusProduct"
}
PROCESS
{
$AntivirusProduct = Get-WmiObject -Namespace "root\SecurityCenter2" -Query $wmiQuery #psboundparameters
[array]$AntivirusNames = $AntivirusProduct.displayName
Switch($AntivirusNames) {
{$AntivirusNames.Count -eq 0}{"No Antivirus installed";Continue}
{$AntivirusNames.Count -eq 1 -and $_ -eq "Windows Defender"} {"Only Windows Defender is installed!";Continue}
{$_ -ne "Windows Defender"} {"Antivirus installed ($_)."}
}
}
END {
}
}
$av = Get-AntivirusName
Add-Type -AssemblyName PresentationFramework
[System.Windows.MessageBox]::Show($av,'Antivirus')
You could try the following, from https://community.spiceworks.com/scripts/show/3161-detect-and-remove-software-powershell:
################################################
# Powershell Detect and Remove software script #
# #
# V1.0 - Gav #
################################################
# - Edit the Variables below and launch the #
# script as an account that can access the #
# machines. #
# - Script will check that logs exist and #
# create them if needed. #
################################################
cls
#VARIABLES - EDIT BELOW
$software = "INSERT SOFTWARE HERE" # - Enter the name as it appears from WMIC query. WMIC PRODUCT NAME
$textfile = "C:\path\pclist.txt"
$Successlogfile = "C:\path\Done_Machines.txt"
$Errorlogfile = "C:\path\Failed_Machines.txt"
#Date Calculation for Logs
$today = Get-Date
$today = $today.ToString("dddd (dd-MMMM-yyyy)")
#Load PC's From Text File
$computers = Get-Content "$textfile"
#Check if Log Files Exist
If (Test-Path $Successlogfile) {
Write-Host -ForegroundColor Green "Success Log File Exists, Results will be appended"
}
else
{
Write-Host -ForegroundColor Red "Success Log File does not exist, creating log file"
New-Item -path $Successlogfile -ItemType file
}
If (Test-Path $Errorlogfile) {
Write-Host -ForegroundColor Green "Error Log File Exists, Results will be appended"
}
else
{
Write-Host -ForegroundColor Red "Error Log File does not exist, creating log file"
New-Item -path $Successlogfile -ItemType file
}
#Run Ping Test and Uninstall if turned on
foreach ($computer in $computers) {
If (Test-Connection -quiet -ErrorAction SilentlyContinue -computername $computer -count 2)
{
Write-Host -ForegroundColor Green "$Computer is responding, Attempting Uninstall of $Software"
Add-Content $Successlogfile "`n$today`n$Computer`n"
Get-WmiObject -class Win32_Product -ComputerName $computer | Where-Object {$_.Name -match $software} | ForEach-Object { $_.Uninstall()}
}
else
{
Write-Host -ForegroundColor Red "$Computer is not responding"
Add-Content $Errorlogfile "`n$today`n$Computer`n"
}
}

Using Powershell To Distribute Script Level Jobs On Remote Servers

More of a theory question...
I have a powershell script that exists on three servers. In this example the three servers are:
server1
server2
server3
I am using another machine, server4, to call script C:\ExampleScript.ps1 remotely using Invoke-Command while specifying the remote machine via the ComputerName parameter. The ultimate goal of the script is to detect whether powershell is running, if it is not, then the computer is "not busy" and can open up the script being called remotely. If the computer is "busy", move onto the next server and continue on through the three machines until all the parameter values have been exhausted. If all machines are busy, it would be ideal if there was a way to periodically check the processes and see if they are still open. In this way, execution of the script can be balanced across the various machines, in an albeit primitive fashion.
Consider the following code:
$servers = "server1","server2","server3"
$data = "param1", "param2", "param3", "param4", "param5", "param6"
#somehow loop through the different servers/data using the above arrays
$job = Invoke-Command $servers[0] {
$ProcessActive = Get-Process powershell -ErrorAction SilentlyContinue
if($ProcessActive -eq $null)
{
"Running"
Invoke-Command -ComputerName $env:computername -FilePath C:\ExampleScript.ps1 -ArgumentList $data[0]
}
else
{
"Busy go to next machine"
}
} -AsJob
Wait-Job $job
$r = Receive-Job $job
$r
The expected result trying to be achieved is attempting to load balance the script across the machines based on whether there is an active powershell process, if not move onto the next machine and perform the same test and subsequent possible execution. The script should go through all the values as specified in the $data array (or whatever).
I found this question interesting, so I wanted to give it a try.
$servers = "server1","server2","server3"
$data = New-Object System.Collections.ArrayList
$data.AddRange(#("param1", "param2", "param3", "param4", "param5", "param6"))
$jobs = New-Object System.Collections.ArrayList
do
{
Write-Host "Checking job states." -ForegroundColor Yellow
$toremove = #()
foreach ($job in $jobs)
{
if ($job.State -ne "Running")
{
$result = Receive-Job $job
if ($result -ne "ScriptRan")
{
Write-Host " Adding data back to que >> $($job.InData)" -ForegroundColor Green
$data.Add($job.InData) | Out-Null
}
$toremove += $job
}
}
Write-Host "Removing completed/failed jobs" -ForegroundColor Yellow
foreach ($job in $toremove)
{
Write-Host " Removing job >> $($job.Location)" -ForegroundColor Green
$jobs.Remove($job) | Out-Null
}
# Check if there is room to start another job
if ($jobs.Count -lt $servers.Count -and $data.Count -gt 0)
{
Write-Host "Checking servers if they can start a new job." -ForegroundColor Yellow
foreach ($server in $servers)
{
$job = $jobs | ? Location -eq $server
if ($job -eq $null)
{
Write-Host " Adding job for $server >> $($data[0])" -ForegroundColor Green
# No active job was found for the server, so add new job
$job = Invoke-Command $server -ScriptBlock {
param($data, $hostname)
$ProcessActive = Get-Process powershell -ErrorAction SilentlyContinue
if($ProcessActive -eq $null)
{
# This will block the thread on the server, so the JobState will not change till it's done or fails.
Invoke-Command -ComputerName $hostname -FilePath C:\ExampleScript.ps1 -ArgumentList $data
Write-Output "ScriptRan"
}
} -ArgumentList $data[0], $env:computername -AsJob
$job | Add-Member -MemberType NoteProperty -Name InData -Value $data[0]
$jobs.Add($job) | Out-Null
$data.Remove($data[0])
}
}
}
# Just a manual check of $jobs
Write-Output $jobs
# Wait a bit before checking again
Start-Sleep -Seconds 10
} while ($data.Count -gt 0)
Basically I create an array, and keep it constantly populated with one job for each server.
Data is removed from the list when a new job starts, and is added back if a job fails. This is to avoid servers running the script with the same data/params.
I lack a proper environment to test this properly at the moment, but will give it a whirl at work tomorrow and update my answer with any changes if needed.

Powershell command for waiting one process to get completed

I have power shell script which will perform multiple tasks like uninstalling,removing and again installing wsp solutions.I am facing an issue like the first process is taking too long time for uninstalling solution so that other process has to wait till the first action has to get completed fully.Now i am giving sleep time but it has some problem while different machines speed gets varies.. I also aware about calling notepad like an external function but i dont want that has to happen here.Apart from that any solutions are availabe like I need to wait for first process to get complete before starting the second process.
$InstallDIR = "F:\new\source\UpdatedWSPFiles"
$Dir = get-childitem $InstallDIR -Recurse
$WSPList = $Dir | where {$_.Name -like "*.wsp*"}
Foreach ($wsp in $WSPList )
{
$WSPFullFileName = $wsp.FullName
$WSPFileName = $wsp.Name
try
{
Write-Host -ForegroundColor White -BackgroundColor Blue "Working on $WSPFileName"
Write-Host -ForegroundColor Green "Retracting Solution"
Uninstall-SPSolution -AllWebApplications -Identity "$WSPFileName" -Confirm:$false
sleep 100
Write-Host -ForegroundColor Green "Removing Solution from farm"
Remove-SPSolution -Identity "$WSPFileName" -Confirm:$false -Force
sleep 60
Write-Host -ForegroundColor Green "Adding solution to farm"
Add-SPSolution "$WSPFullFileName" -Confirm:$false
sleep 60
}
you can try start-process with the -wait switch :
PS> $p="C:\windows\System32\WindowsPowerShell\v1.0\powershell.exe"
PS> $params="-command &{ if ((Get-PSSnapin `"Microsoft.SharePoint.PowerShell`" -ErrorAction SilentlyContinue) -eq $null)
{
Add-PSSnapin `"Microsoft.SharePoint.PowerShell`"
}
Uninstall-SPSolution -AllWebApplications -Identity `"$WSPFileName`" -Confirm:$false}"
PS> Start-Process $p $params -Wait
Guess you could try
Start-Job -Name "jobname" -ScriptBlock { Uninstall-SPSolution -AllWebApplications -Identity "$WSPFileName" -Confirm:$false }
and
Wait-Job -Name "jobname" -Timeout "maximum wait time"