Adapting a script to do multiple functions, starting with test-connection to gather data, will be hitting 6000+ machines so I am using RunspacePools adapted from the below site;
http://learn-powershell.net/2013/04/19/sharing-variables-and-live-objects-between-powershell-runspaces/
The data comes out as below, I would like to get it sorted into an array (I think that's the terminology), so I can sort the data via results. This will be adapted to multiple other functions pulling anything from Serial Numbers to IAVM data.
Is there any way I can use the comma delimited data and have it spit the Values below into columns? IE
Name IPAddress ResponseTime Subnet
x qwe qweeqwe qweqwe
The added values aren't so important at the moment, just the ability to add the values and pull them.
Name Value
—- —–
x-410ZWG \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410ZWG",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-47045Q \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-47045Q",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-440J26 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-440J26",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-410Y45 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410Y45",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DJKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DJKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
nonexistant
x-DDMVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DDMVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-470481 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-470481",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DHKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DHKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-430XXF \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-430XXF",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-DLKVV1 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-DLKVV1",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-410S86 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-410S86",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-SCH004 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-SCH004",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
x-431KMS
x-440J22 \\x-DHMVV1\root\cimv2:Win32_PingStatus.Address="x-440J22",BufferSize=32,NoFragmentation=false,RecordRoute=0,…
Thank for any help!
Code currently
Function Get-RunspaceData {
[cmdletbinding()]
param(
[switch]$Wait
)
Do {
$more = $false
Foreach($runspace in $runspaces) {
If ($runspace.Runspace.isCompleted) {
$runspace.powershell.EndInvoke($runspace.Runspace)
$runspace.powershell.dispose()
$runspace.Runspace = $null
$runspace.powershell = $null
} ElseIf ($runspace.Runspace -ne $null) {
$more = $true
}
}
If ($more -AND $PSBoundParameters['Wait']) {
Start-Sleep -Milliseconds 100
}
#Clean out unused runspace jobs
$temphash = $runspaces.clone()
$temphash | Where {
$_.runspace -eq $Null
} | ForEach {
Write-Verbose ("Removing {0}" -f $_.computer)
$Runspaces.remove($_)
}
Write-Host ("Remaining Runspace Jobs: {0}" -f ((#($runspaces | Where {$_.Runspace -ne $Null}).Count)))
} while ($more -AND $PSBoundParameters['Wait'])
}
#Begin
#What each runspace will do
$ScriptBlock = {
Param ($computer,$hash)
$Ping = test-connection $computer -count 1 -ea 0
$hash[$Computer]= $Ping
}
#Setup the runspace
$Script:runspaces = New-Object System.Collections.ArrayList
# Data table for all of the runspaces
$hash = [hashtable]::Synchronized(#{})
$sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
$runspacepool = [runspacefactory]::CreateRunspacePool(1, 100, $sessionstate, $Host)
$runspacepool.Open()
#Process
ForEach ($Computer in $Computername) {
#Create the powershell instance and supply the scriptblock with the other parameters
$powershell = [powershell]::Create().AddScript($scriptBlock).AddArgument($computer).AddArgument($hash)
#Add the runspace into the powershell instance
$powershell.RunspacePool = $runspacepool
#Create a temporary collection for each runspace
$temp = "" | Select-Object PowerShell,Runspace,Computer
$Temp.Computer = $Computer
$temp.PowerShell = $powershell
#Save the handle output when calling BeginInvoke() that will be used later to end the runspace
$temp.Runspace = $powershell.BeginInvoke()
Write-Verbose ("Adding {0} collection" -f $temp.Computer)
$runspaces.Add($temp) | Out-Null
}
# Wait for all runspaces to finish
#End
Get-RunspaceData -Wait
$stoptimer = Get-Date
#Display info, and display in GridView
Write-Host
Write-Host "Availability check complete!" -ForegroundColor Cyan
"Execution Time: {0} Minutes" -f [math]::round(($stoptimer – $starttimer).TotalMinutes , 2)
$hash | ogv
When you use runspaces, you write the scriptblock for the runspace pretty much the same way you would for a function. You write whatever you want the return to be to the pipeline, and then either assign it to a variable, pipe it to another cmdlet or function, or just let it output to the console. The difference is that while the function returns it's results automatically, with the runspace they collect in the runspace output buffer and aren't returned until you do the .EndInvoke() on the runspace handle.
As a general rule, the objective of a Powershell script is (or should be) to create objects, and the objective of using the runspaces is to speed up the process by multi-threading. You could return string data from the runspaces back to the main script and then use that to create objects there, but that's going to be a single threaded process. Do your object creation in the runspace, so that it's also multi-threaded.
Here's a sample script that uses a runspace pool to do a pingsweep of a class C subnet:
Param (
[int]$timeout = 200
)
$scriptPath = (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent)
While (
($network -notmatch "\d{1,3}\.\d{1,3}\.\d{1,3}\.0") -and -not
($network -as [ipaddress])
)
{ $network = read-host 'Enter network to scan (ex. 10.106.31.0)' }
$scriptblock =
{
Param (
[string]$network,
[int]$LastOctet,
[int]$timeout
)
$options = new-object system.net.networkinformation.pingoptions
$options.TTL = 128
$options.DontFragment = $false
$buffer=([system.text.encoding]::ASCII).getbytes('a'*32)
$Address = $($network.trim("0")) + $LastOctet
$ping = new-object system.net.networkinformation.ping
$reply = $ping.Send($Address,$timeout,$buffer,$options)
Try { $hostname = ([System.Net.Dns]::GetHostEntry($Address)).hostname }
Catch { $hostname = 'No RDNS' }
if ( $reply.status -eq 'Success' )
{ $ping_result = 'Yes' }
else { $ping_result = 'No' }
[PSCustomObject]#{
Address = $Address
Ping = $ping_result
DNS = $hostname
}
}
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(100,100)
$RunspacePool.Open()
$Jobs =
foreach ( $LastOctet in 1..254 )
{
$Job = [powershell]::Create().
AddScript($ScriptBlock).
AddArgument($Network).
AddArgument($LastOctet).
AddArgument($Timeout)
$Job.RunspacePool = $RunspacePool
[PSCustomObject]#{
Pipe = $Job
Result = $Job.BeginInvoke()
}
}
Write-Host 'Working..' -NoNewline
Do {
Write-Host '.' -NoNewline
Start-Sleep -Seconds 1
} While ( $Jobs.Result.IsCompleted -contains $false)
Write-Host ' Done! Writing output file.'
Write-host "Output file is $scriptPath\$network.Ping.csv"
$(ForEach ($Job in $Jobs)
{ $Job.Pipe.EndInvoke($Job.Result) }) |
Export-Csv $scriptPath\$network.ping.csv -NoTypeInformation
$RunspacePool.Close()
$RunspacePool.Dispose()
The runspace script does a ping on each address, and if it gets successful ping attempts to resolve the host name from DNS. Then it builds a custom object from that data, which is output to the pipeline. At the end, those objects are returned when the .EndInvoke() is done on the runspace jobs and piped directly into Export-CSV, but it could just as easily be output to the console, or saved into a variable.
Related
In the original script, I was attempting to search for a string in a text file in a running log. It worked fine however, since there is a -wait parameter in the loop, it wasn't easy to find a solution that would allow for the same script to search over multiple text files. Since then the following script was introduced to me that incorporates Runspaces:
using namespace System.Management.Automation.Runspaces
using namespace System.Threading
# get the log files here
$LogGroup = ('C:\log 0.txt', 'C:\Log 1.txt', 'C:\Log 2.txt')
# this help us write to the main log file in a thread safe manner
$lock = [SemaphoreSlim]::new(1, 1)
# define the logic used for each thread, this is very similar to the
# initial script except for the use of the SemaphoreSlim
$action = {
param($path)
$PSDefaultParameterValues = #{ "Get-Date:format" = "yyyy-MM-dd HH:mm:ss" }
Get-Content $path -Tail 1 -Wait | ForEach-Object {
if($_ -match 'down') {
# can I write to this file?
$lock.Wait()
try {
Write-Host "Down: $_ - $path" -ForegroundColor Green
Add-Content "path\to\mainLog.txt" -Value "$(Get-Date) Down: $_ - $path"
}
finally {
# release the lock so other threads can write to the file
$null = $lock.Release()
}
}
}
}
try {
$iss = [initialsessionstate]::CreateDefault2()
$iss.Variables.Add([SessionStateVariableEntry]::new('lock', $lock, $null))
$rspool = [runspacefactory]::CreateRunspacePool(1, $LogGroup.Count, $iss, $Host)
$rspool.ApartmentState = [ApartmentState]::STA
$rspool.ThreadOptions = [PSThreadOptions]::UseNewThread
$rspool.Open()
$res = foreach($path in $LogGroup) {
$ps = [powershell]::Create($iss).AddScript($action).AddArgument($path)
$ps.RunspacePool = $rspool
#{
Instance = $ps
AsyncResult = $ps.BeginInvoke()
}
}
# block the main thread
do {
$id = [WaitHandle]::WaitAny($res.AsyncResult.AsyncWaitHandle, 200)
}
while($id -eq [WaitHandle]::WaitTimeout)
}
finally {
# clean all the runspaces
$res.Instance.ForEach('Dispose')
$rspool.ForEach('Dispose')
}
The Runspaces allow for additional threads allowing for multitasking but I am not very skilled and I need help adding an elseif clause after an if statement. But my attempts were rewarded with the following error:
cmdlet ForEach-Object at command pipeline position 2
Supply values for the following parameters:
Process[0]:
Here’s the best I could come up with so far:
$action = {
param($path)
$PSDefaultParameterValues = #{ "Get-Date:format" = "yyyy-MM-dd HH:mm:ss" }
$lock.Wait()
# can I write to this file?
try {
Get-Content $path -Tail 1 -Wait | ForEach-Object
if($_ -match 'down) {
Write-Host "Down: $_ - $path" -ForegroundColor Red
Add-Content "C:\ log_down.txt" -Value "$(Get-Date) Down: $_ - $path"
}
elseif($_ -match 'up') {
Write-Host "Down: $_ - $path" -ForegroundColor Green
Add-Content "C:\ log_up.txt" -Value "$(Get-Date) up: $_ - $path"
}
$lock.Wait()
}
finally {
# release the lock so other threads can write to the file
$null = $lock.Release()
}
}
Thanks in advance for any help!
Here is the only change you need to do, below code only addresses the $action Script Block. Rest of the code should remain the same.
Make sure you're using the Full Paths of the logs.
$action = {
param($path)
$PSDefaultParameterValues = #{ "Get-Date:format" = "yyyy-MM-dd HH:mm:ss" }
Get-Content $path -Tail 1 -Wait | ForEach-Object {
# wait to enter the SemaphoreSlim
$lock.Wait()
try {
if($_ -match 'down') {
Write-Host "Down: $_ - $path" -ForegroundColor Red
Add-Content "C:\log_down.txt" -Value "$(Get-Date) Down: $_ - $path"
}
elseif($_ -match 'up') {
Write-Host "Up: $_ - $path" -ForegroundColor Green
Add-Content "C:\log_up.txt" -Value "$(Get-Date) up: $_ - $path"
}
# more conditions can go here
}
finally {
# release the lock so other threads can write to the file
$null = $lock.Release()
}
}
}
After closer look at your attempt, it seems almost right:
Missing an opening { after ForEach-Object.
Missing a closing } before the finally block.
.Wait() should be inside the loop instead of outside.
I am trying to search for a string in multiple text files to trigger an event. The log file is being actively added to by a program. The following script successfully achieves that goal, but it only works for one text file at a time:
$PSDefaultParameterValues = #{"Get-Date:format"="yyyy-MM-dd HH:mm:ss"}
Get-Content -path "C:\Log 0.txt" -Tail 1 -Wait | ForEach-Object { If ($_ -match 'keyword') {
Write-Host "Down : $_" -ForegroundColor Green
Add-Content "C:\log.txt" "$(get-date) down"
Unfortunately it means I have to run 3 instances of this script to search the 3 log files (C:\log 0.txt, C:\log 1.txt and C:'log 2.txt).
What I want to do is run one powershell script to search for that string across all three text files and not three.
I tried using a wildcard in the path ("C:\log*.txt)
I also tried adding a foreach loop:
$PSDefaultParameterValues = #{"Get-Date:format"="yyyy-MM-dd HH:mm:ss"}
$LogGroup = ('C:\log 0.txt', 'C:\Log 1.txt', 'C:\Log 2.txt')
ForEach ($log in $LogGroup) {
Get-Content $log -Tail 1 -Wait | ForEach-Object { If ($_ -match 'keyword') {
Write-Host "Down: $_" -ForegroundColor Green
Add-Content -path "C:\log.txt" "$(get-date) down"
Add-Content -path "C:\log.txt" "$(get-date) down"
}
}
}
This got me no errors but it also didn't work.
I saw others use Get-ChildItem instead of Get-Content but since this worked with one file... shouldn't it work with multiple? I assume it's my lack of scripting ability. Any help would be appreciated. Thanks.
This is how you can apply the same logic you already have for one file but for multiple logs at the same time, the concept is to spawn as many PowerShell instances as log paths there are in the $LogGroup array. Each instance is assigned and will be monitoring 1 log path and when the keyword is matched it will append to the main log file.
The instances are assigned the same RunspacePool, this help us initialize all with a SemaphoreSlim instance which help us ensure thread safety (only 1 thread can write to the main log at a time).
using namespace System.Management.Automation.Runspaces
using namespace System.Threading
# get the log files here
$LogGroup = ('C:\log 0.txt', 'C:\Log 1.txt', 'C:\Log 2.txt')
# this help us write to the main log file in a thread safe manner
$lock = [SemaphoreSlim]::new(1, 1)
# define the logic used for each thread, this is very similar to the
# initial script except for the use of the SemaphoreSlim
$action = {
param($path)
$PSDefaultParameterValues = #{ "Get-Date:format" = "yyyy-MM-dd HH:mm:ss" }
Get-Content $path -Tail 1 -Wait | ForEach-Object {
if($_ -match 'down') {
# can I write to this file?
$lock.Wait()
try {
Write-Host "Down: $_ - $path" -ForegroundColor Green
Add-Content "path\to\mainLog.txt" -Value "$(Get-Date) Down: $_ - $path"
}
finally {
# release the lock so other threads can write to the file
$null = $lock.Release()
}
}
}
}
try {
$iss = [initialsessionstate]::CreateDefault2()
$iss.Variables.Add([SessionStateVariableEntry]::new('lock', $lock, $null))
$rspool = [runspacefactory]::CreateRunspacePool(1, $LogGroup.Count, $iss, $Host)
$rspool.ApartmentState = [ApartmentState]::STA
$rspool.ThreadOptions = [PSThreadOptions]::UseNewThread
$rspool.Open()
$res = foreach($path in $LogGroup) {
$ps = [powershell]::Create($iss).AddScript($action).AddArgument($path)
$ps.RunspacePool = $rspool
#{
Instance = $ps
AsyncResult = $ps.BeginInvoke()
}
}
# block the main thread
do {
$id = [WaitHandle]::WaitAny($res.AsyncResult.AsyncWaitHandle, 200)
}
while($id -eq [WaitHandle]::WaitTimeout)
}
finally {
# clean all the runspaces
$res.Instance.ForEach('Dispose')
$rspool.ForEach('Dispose')
}
I am trying to work on an existing script that I had some assistance from in another thread. With some member assistance I was able to get my script to run using the "ThreadJob" module, however I was hoping I can also make use of runspacepools in conjunction with the ThreadJob to make it run faster.
In my code I post below, I am printing out a line to notify me that the function get's called. And I can see it is getting called. So it makes me think that line 56 and line 59 are incorrectly being called and I can't figure out how to call them.
if I run the "$rootPath\UpdateContacts\UpdateContacts.ps1" file manually through powershell ISE, it runs fine (obviously outside of the runspace), but I'd like to try and get it to work within the runspacepool.
Here is what I'm working with. I think I am pretty close.
begin
{
CLS
[switch]$MultiThread=$true
$rootPath = $(Split-path $MyInvocation.MyCommand.path -Parent)
$userEmail = "user#domain.com"
$SessionState = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, 10,$Sessionstate, $Host)
$RunspacePool.Open()
$Jobs = #()
}
process
{
function ContactUpdater()
{
##################### Start Comparing Data #####################
#Our Array of values we will be comparing
[array]$CompareValues = "FirstName","MiddleName","LastName","DisplayName","Email","Mobile","TelephoneNumber","Title","Dept","Company"
for($i=0; $i -lt $CompareValues.Count; $i++)
{
#First let's create 2 variables that will hold the info we want
$A = ($Users).($CompareValues[$i])
$B = ($Contacts).($CompareValues[$i])
##################### Update Contacts #####################
#Only Run if there are contacts; otherwise there is nothing for us to compare
if(($NULL -ne $B))
{
#Displays all differences
#$Differences = [string[]]([Linq.Enumerable]::Except([object[]]$a, [object[]]$b) + [Linq.Enumerable]::Except([object[]]$b, [object[]]$a))
#Displays what accounts we need to import
$NeedsToBeAdded = [string[]]([Linq.Enumerable]::Except([object[]]$a, [object[]]$b))
#Displays what accounts we need to delete because they no longer exist
$NeedsToBeDeleted = [string[]]([Linq.Enumerable]::Except([object[]]$b, [object[]]$a))
}
}
##################### Import All Contacts #####################
if($NULL -eq $Contacts)
{
Write-Host "I am in the import"
# Load UpdateContacts function in memory
. "$rootPath\UpdateContacts\UpdateContacts\UpdateContacts.ps1"
#Write-host "Importing Contacts. This could take several minutes."
& "$rootPath\UpdateContacts\UpdateContacts.ps1"
}
}
if($MultiThread)
{
foreach($userEmail in $EmailAddress)
{
try
{
##################### Create Contact Folder #####################
if($NULL -eq $folderId)
{
$start = [datetime]::UtcNow
Write-Host "Creating Contacts Folder"
Try
{
while($NULL = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$UPN/contactFolders/$folderId" -Headers $headers -Method get))
{
$NewContactFolder = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$UPN/contactFolders" -Body $ContactsFolderBody -Headers $headers -Method post -ContentType 'application/json'
sleep -Milliseconds 1
$folderId = $($NewContactFolder.id)
}
}
Catch
{
Out-Null
}
Add-Content "$rootPath\progress.txt" "`t`tCreated Contacts Folder in: $('{0:N2}' -f ([datetime]::UtcNow - $start).TotalSeconds) seconds"
Add-Content "$rootPath\progress.txt" ""
}
##################### Getting All User Info #####################
$start = [datetime]::UtcNow
$Users = & $rootPath\GetUserInfo\GetUserInfo.ps1
Add-Content "$rootPath\progress.txt" "`t`tFinished Getting all User Info in: $('{0:N2}' -f ([datetime]::UtcNow - $start).TotalSeconds) seconds"
Add-Content "$rootPath\progress.txt" ""
##################### Getting Contact Info #####################
if($NULL -ne $folderId)
{
$start = [datetime]::UtcNow
$Contacts = & $rootPath\GetContactInfo\GetContactInfo.ps1
Add-Content "$rootPath\progress.txt" "`t`tFinished Getting all Contact Info in: $('{0:N2}' -f ([datetime]::UtcNow - $start).TotalSeconds) seconds"
Add-Content "$rootPath\progress.txt" ""
}
##################### Import Contacts #####################
$start = [datetime]::UtcNow
CLS
if($NULL -eq $ImportMsg)
{
Write-host "Importing Contacts. This could take several minutes."
$ImportMsg = "Ran"
}
$ContactImporter = ContactUpdater
Add-Content "$rootPath\progress.txt" "`t`tFinished Importing Contact Info in: $('{0:N2}' -f ([datetime]::UtcNow - $start).TotalSeconds) seconds"
}
catch
{
$LogFile = "$rootPath\log.txt"
$errcond = $_.Exception.Message
$timestamp = (get-date).DateTime
"Time of exception: $timestamp" | Out-File $LogFile -Append
"User: $userEmail" | out-file $LogFile -Append
$errcond | out-file -FilePath $LogFile -append
}
1..10 | Foreach-Object {
$PowershellThread = [powershell]::Create()
$PowershellThread.RunspacePool = $RunspacePool
$PowershellThread.AddScript($ContactImporter).AddArgument($userEmail)
$Jobs += $PowershellThread.BeginInvoke()
}
}
}
}
end
{
if($MultiThread)
{
while ($Jobs.IsCompleted -contains $false)
{
Start-Sleep -Milliseconds 100
}
$RunspacePool.Close() | Out-Null
$RunspacePool.Dispose() | Out-Null
}
}
The part in the "Import all Contacts" section within the ContactUpdater() Function, should call the script:
& "$rootPath\UpdateContacts\UpdateContacts.ps1"
That script looks like this:
# Save the function in a scriptBlock, we need this
# so we can pass this function in the scope of the ThreadJobs
$updateContacts = "function UpdateContacts { $function:updateContacts }"
# Define the Number of Threads we are going to use
# (Get-CimInstance win32_processor).NumberOfLogicalProcessors
# Can give you a good perspective as to how many Threads is safe to use.
$numberOfThreads = 10
# $users is the array we want to process with
# the UpdateContacts function.
# Grouping the users in chunks so each running Job can process
# a chunk of users. Each chunk will contain around 50 users to process.
$groupSize = [math]::Ceiling($users.Count / $numberOfThreads)
$counter = [pscustomobject]#{ Value = 0 }
$chunks = $users | Group-Object -Property {
[math]::Floor($counter.Value++ / $groupSize)
}
foreach($chunk in $chunks)
{
# Capture this chunk of users in a variable
$thisGroup = $chunk.Group
# This is what we are running inside the scope
# of our threadJob
$scriptBlock = {
# Pass our variables to this scope
$UPN = $using:UPN
$folderID = $using:folderId
$headers = $using:headers
$contactsBody = $using:contactsBody
$ImportMsg = $using:ImportMsg
# First we need to define the function inside this scope
. ([scriptBlock]::Create($using:updateContacts))
# Loop through each user
foreach($user in $using:thisGroup)
{
UpdateContacts -User $user
}
}
# ThrottleLimit is the number of Jobs that can run at the same time.
# Be aware, a higher number of Jobs running does NOT mean that the
# task will perform faster. This always depends on your CPU & Memory.
# And, this case in particular, the number of requests your URI is able to handle
Start-ThreadJob -ScriptBlock $scriptBlock -ThrottleLimit $numberOfThreads
}
# Now we should have 10 Jobs running at the same time, each Job
# is processing a chunk of 50 users aprox. (500 users / 10)
# the output of all Jobs:
$result = Get-Job | Receive-Job -Wait
# Free up memory:
Get-Job | Remove-Job
That code above, starts a threadjob and launches another function in "$rootPath\UpdateContacts\UpdateContacts\UpdateContacts.ps1"
And that script looks like this:
Function UpdateContacts($User)
{
#FirstName, MiddleName, LastName, DisplayName, SamAccountName, Email, Mobile, TelephoneNumber, Title, Dept, Company, Photo, ExtensionAttribute2
$ContactsBody = #"
{
"givenName" : "$($User.FirstName)",
"middleName" : "$($User.MiddleName)",
"surname" : "$($User.LastName)",
"fileAs" : "$($User.LastName)",
"displayName" : "$($User.DisplayName)",
"jobTitle" : "$($User.Title)",
"companyName" : "$($User.Company)",
"department" : "$($User.Dept)",
"mobilePhone" : "$($User.Mobile)",
"homePhones" : ["$($User.TelephoneNumber)"],
"emailAddresses":
[
{
"address": "$($User.Email)",
"name": "$($User.DisplayName)"
}
]
}
"#
Try
{
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$UPN/contactFolders/$folderId/contacts" -Headers $headers -Body $ContactsBody -Method Post -ContentType 'application/json' | Out-Null
#After each user clear the info
$User = $NULL
}
Catch
{
if($error)
{
$User
$error
pause
}
$_.Exception.Message
Write-Host "--------------------------------------------------------------------------------------"
$_.Exception.ItemName
}
}
I have a PowerShell script that is running a web request as part of a job. With the script block that is run by the job I am logging the Endpoint Uri and the response time. and then after all the jobs are finished but before I remove the jobs I am trying to export the result into excel.
PS Verion:
Major Minor Build Revision
----- ----- ----- --------
5 1 17763 503
Code:
$Code = {
Param(
[array]$Domain,
[array]$Services,
[array]$TestData,
[string]$Path
)
$Log = #()
## Get Random School ##
$Random = Get-Random -InputObject $TestData -Count 1
## Get Random Service ##
$RandService = Get-Random -InputObject $Services
## Get Random Endpoint ##
$ServiceEndpoints = Get-Content "$Path\Service.Endpoints.json" | Out-String | ConvertFrom-Json
$GatewayEndpoints = $ServiceEndpoints.Services.$RandService.Gateway
$RandomEndpoint = Get-Random -InputObject $GatewayEndpoints
$Headers = #{
"Authorization" = "Bearer" + ' ' + $Random.Token
}
$Uri = 'https://' + $Domain + $RandomEndpoint
Try {
$TimeTaken = Measure-Command -Expression {
$JsonResponse = Invoke-WebRequest -Uri $Uri -Headers $Headers -ContentType 'application/json' -Method Get -UseBasicParsing
}
}
Catch {
}
$ResponseTime = [Math]::Round($TimeTaken.TotalMilliseconds, 1)
$LogItem = New-Object PSObject
$LogItem | Add-Member -type NoteProperty -Name 'Endpoint' -Value $Uri
$LogItem | Add-Member -type NoteProperty -Name 'Time' -Value $ResponseTime
$Log += $LogItem
Write-Host $Log
}
#Remove all jobs
Get-Job | Remove-Job
#Start the jobs. Max 4 jobs running simultaneously.
foreach($Row in $TestData){
While ($(Get-Job -state running).count -ge $MaxThreads){
Start-Sleep -Milliseconds 3
}
Start-Job -Scriptblock $Code -ArgumentList $Domain, $Services, $TestData, $Path
}
#Wait for all jobs to finish.
While ($(Get-Job -State Running).count -gt 0) {
start-sleep 1
}
$Log | Export-XLSX -Path .\Test.Results\Performance\Performance.Test.Log.xlsx -ClearSheet
#Get information from each job.
foreach($Job in Get-Job) {
$Info = Receive-Job -Id ($Job.Id)
}
#Remove all jobs created.
Get-Job | Remove-Job
I cannot seem to get the endpoint uri and the response time out of the script block. When I try to export the $Log, all that happens is it creates an empty excel file.
Write-Host $Log
#{Endpoint=https://domain/customer/v1/years/2019/marks; Time=1233.3}
#{Endpoint=https://domain/customer/v1/years/2019/marks; Time=2131.7}
You've to return $Log in your script block, since $Log lives in another scope. You can return $Log in your $Code script block, and finally, fetch it via Receive-Job.
Change your code to:
$Code = {
...
$Log += $LogItem
Write-Host $Log
$Log # return via pipeline to the caller
}
As Niraj Gajjar's answer suggests you can use Export-Csv cmdlet to create an Excel file:
#Get information from each job.
foreach($Job in Get-Job) {
Receive-Job -Id ($Job.Id) | Export-CSV -Path .\Test.Results\Performance\Performance.Test.Log.xlsx -Append -NoTypeInformation
}
Global variables inside of jobs aren't visible in the main script since job is running in a new session with its own global space.
You can use Export-Csv to open file in excel.
sample code with multiple jobs to CSV format :
$jobs = #() # INITILIZING ARRAY
$jobs += Start-Job { appwiz.cpl } # START JOB 1 AND ADDING TO ARRAY
$jobs += Start-Job { compmgmt.msc } # START JOB 2 AND ADDING TO ARRAY
$jobs += Start-Job { notepad.exe } # START JOB 3 AND ADDING TO ARRAY
foreach ( $job in $jobs) # INTERATION OF JOBS
{
Export-Csv -InputObject $job "C:\result.csv" -Append # SAVING TO FILE
}
Note : There are some internal properties of job which are not converted by CSV because depth is 1 but some basic properties are available.
I'm at a bit of a loss with the script I am trying to pull.
In short: I want to scan my domain-computers for WinRM connectivity - and I can do that just fine. The problem is, that it takes up to 5 minutes to finish - thats why I want to multithread the task.
Working NON MULTITHREAD code:
# this is normaly a textfile with lots of machine hostnames
$computers = "PC100","PC106","PC124","PC115","PC21"
function checkMachine($computers){
$ErrorActionPreference = "Stop"
foreach ($item in $computers){
#the function contest only performs a ping and returne $true or $false
$connection = ConTest($item)
if($connection){
try{
$winRM = test-wsman -ComputerName $item
if($winRM){
write-host "winRM"
[void] $objListboxLeft.Items.Add($item)
}
}catch{
write-host "NO winRM"
[void] $objListboxCenter.Items.Add($item)
}
}else{
write-host "offline"
[void] $objListboxRight.Items.Add($item)
}
}
}
this is basically just a small portion of what my skript does/will do but it's the part that takes ages.
My failing runspace test - I basically fail to get ANY results at all. Nothing in textboxes, no output on my commandline and I basically have no idea what I am doing wrong.
Multithread code:
function MulticheckMachine($computers){
$ErrorActionPreference = "Stop"
$runspaceCollection = #()
$runspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$runspacePool.open()
$scriptBlock = {
Param($item)
$connection = ConTest($item)
if($connection){
try{
test-wsman -ComputerName $item
$winRM = test-wsman -ComputerName $item
if($winRM){
write-host "winRM"
[void] $objListboxLeft.Items.Add($item)
}
}catch{
write-host "NO winRM"
[void] $objListboxCenter.Items.Add($item)
}
}else{
write-host "offline"
[void] $objListboxRight.Items.Add($item)
}
}
Foreach($item in $computers){
$powershell = [PowerShell]::Create().AddScript($scriptBlock).AddArgument($item)
$powershell.runspacePool = $runspacePool
[Collections.Arraylist]$runspaceCollection += New-Object -TypeName PSObject -Property #{
Runspace = $powershell.BeginInvoke()
PowerShell = $powershell
}
$runspaceCollection
}
While($runspaceCollection){
Foreach($runspace in $runspaceCollection.ToArray()){
If($runspace.Runspace.IsCompleted){
$runspace.PowerShell.EndInvoke($runspace.Runspace)
$runspace.PowerShell.Dispose()
$runspaceCollection.Remove($runspace)
}
}
}
}
the runspace code comes from a mix of these guides:
http://blogs.technet.com/b/heyscriptingguy/archive/2013/09/29/weekend-scripter-max-out-powershell-in-a-little-bit-of-time-part-2.aspx
http://newsqlblog.com/2012/05/22/concurrency-in-powershell-multi-threading-with-runspaces/
I hope someone can help me out and tell me where/why I fail. Thanks!
Well, thanks for the hints but the problem was far more basic.
I was trying to get my data at the wrong position. Also, I simplified my script a bit. I don't call functions in functions anymore.
Note1: I did not realize I can/need to work with return values within my scriptblock for the runspace.
Note2: I am now collecting my data and inserting it into my listboxes (or where-ever else I wanted to) at the end of my function within the while loop - where I basically build-back my runspaces.
Note3: All "GUI parts" I reference to are located in a different file and do exist!
I got the duration down to roughly 20 seconds (from almost 5 minutes before)
The number of threads I use is a bit random, it's one of the combinations that works fastest.
Code:
function multiCheckMachine($computers){
$ErrorActionPreference = "Stop"
$runspaceCollection = #()
$runspacePool = [RunspaceFactory]::CreateRunspacePool(1,50)
$runspacePool.open()
$scriptBlock = {
Param($item)
$FQDNitem = "$item.domain.com"
$address = nslookup $FQDNitem
if($address -like "addresses*"){
$address = $address[5] -replace ".* ",""
}else{
$address = $address[4] -replace ".* ",""
}
$con = ping -n 1 $address
if($con[2] -like "*Bytes*"){
$winRM = test-wsman -ComputerName $item
if($winRM){
return "$item.winRM"
}else{
return "$item.NOremote"
}
}else{
return "$item.offline"
}
}
Foreach($item in $computers){
$powershell = [PowerShell]::Create().AddScript($scriptBlock).AddArgument($item)
$powershell.runspacePool = $runspacePool
[Collections.Arraylist]$runspaceCollection += New-Object -TypeName PSObject -Property #{
Runspace = $powershell.BeginInvoke()
PowerShell = $powershell
}
}
While($runspaceCollection){
Foreach($runspace in $runspaceCollection.ToArray()){
If($runspace.Runspace.IsCompleted){
if($runspace.PowerShell.EndInvoke($runspace.Runspace) -like "*winrm"){
[void] $objListboxOnline.Items.Add($runspace.PowerShell.EndInvoke($runspace.Runspace).split(".")[0])
}elseif($runspace.PowerShell.EndInvoke($runspace.Runspace) -like "*NOremote"){
[void] $objListboxNoWinRM.Items.Add($runspace.PowerShell.EndInvoke($runspace.Runspace).split(".")[0])
}elseif($runspace.PowerShell.EndInvoke($runspace.Runspace) -like "*offline"){
[void] $objListboxOffline.Items.Add($runspace.PowerShell.EndInvoke($runspace.Runspace).split(".")[0])
}
$runspace.PowerShell.Dispose()
$runspaceCollection.Remove($runspace)
}
}
}
}