#my script is a excerpt of https://www.codeproject.com/Tips/895840/Multi-Threaded-PowerShell-Cookbook
#the first example
#the issue is where ".AddScript($secb)" is. All jobs are finished sequentially , Could anyone explain ???
#why in my script , .AddScript($sb) is concurrent ??
$numThreads = 5
# Create session state
$myString = "this is session state!"
$sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$sessionstate.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList "myString" ,$myString, "example string"))
# Create runspace pool consisting of $numThreads runspaces
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, 5, $sessionState, $Host)
$RunspacePool.Open()
$Jobs = #()
$sb={
param ($data)
$r=Get-Random
Write-Host "before $r"
Start-Sleep -Seconds 3
Write-Host "after $r"
}
$secb={
param ($block)
Invoke-Command -ScriptBlock $block
}
1..5 | % {
#below line is not concurrent , i don't know why
$Job = [powershell]::Create().AddScript($secb) # $Job = [powershell]::Create().AddScript($sb) could do multi-thread
$Job.AddArgument($sb)
$Job.RunspacePool = $RunspacePool
$Jobs += New-Object PSObject -Property #{
RunNum = $_
Job = $Job
Result = $Job.BeginInvoke()
}
}
Write-Host "Waiting.." -NoNewline
Do {
Write-Host "." -NoNewline
Start-Sleep -Seconds 1
} While ( $Jobs.Result.IsCompleted -contains $false) #Jobs.Result is a collection
I'm by far not an expert on RunSpaces, usually whenever I need multithreading I use the ThreadJob module. I'm not sure what you are doing wrong in your code but I can show you a working example for what you're trying to do. I took this example from this answer (credits to Mathias) which is excellent and modified it a bit.
Code:
cls
$elapsedTime = [System.Diagnostics.Stopwatch]::StartNew()
$numThreads = 5
# Create session state
$myString = "this is session state!"
$sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$sessionstate.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList "myString" ,$myString, "example string"))
# Create runspace pool consisting of $numThreads runspaces
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, 5, $sessionState, $Host)
$RunspacePool.Open()
$runspaces = foreach($i in 1..5)
{
$PSInstance = [powershell]::Create().AddScript({
param ($TestNumber)
Write-Output "Test Number: $TestNumber"
$r={Get-Random}
Write-Output "before $(& $r)"
Start-Sleep -Seconds 3
Write-Output "after $(& $r)"
}).AddParameter('TestNumber',$i)
$PSInstance.RunspacePool = $RunspacePool
[pscustomobject]#{
Instance = $PSInstance
Result = $PSInstance.BeginInvoke()
}
}
while($runspaces|Where-Object{-not $_.Result.IsCompleted})
{
Start-Sleep -Milliseconds 500
}
$resultRunspace = [collections.generic.list[string]]::new()
$Runspaces|ForEach-Object {
$resultRunspace.Add($_.Instance.EndInvoke($_.Result))
}
$elapsedTime.Stop()
"Elapsed Time: {0}" -f $elapsedTime.Elapsed.TotalSeconds
""
$resultRunspace
Result:
Elapsed Time: 3.1271587
Test Number: 1 before 474010429 after 2055432874
Test Number: 2 before 1639634857 after 1049683678
Test Number: 3 before 72786850 after 2046654322
Test Number: 4 before 1958738687 after 1832326064
Test Number: 5 before 1944958392 after 1652518661
Now, again, if you are able to install modules I would recommend ThreadJob as it is a lot easier to use and performs equally as fast as RunSpaces.
I wrote a script some time ago which would loop through all the directories in $HOME and get the number of files on each one, the script was meant to compare Linear Loops vs ThreadJob vs RunSpace here are the results and why I would always recommend ThreadJob
Related
I'm using the below code to display the results of PowerShell Jobs with a timeout of 120 seconds. I would like to enhance this code by incorporating Write-Progress (based on number of jobs completed). I tried using this example as a reference, however, when I try to incorporate that code, the progress bar displays briefly after all the jobs are all done already.
$Jobs = #()
$ForceStoppedIds = #{}
$Jobs += Get-Job
$Jobs | Wait-Job -Timeout 120 | Out-Null
$Jobs | ?{$_.State -eq 'Running'} | Stop-Job -PassThru | %{$ForceStoppedIds[$_.Id] = $true}
foreach ($Job in $Jobs) {
$Name = $Job.Name
$Output = (Get-Job -Name $Name | Receive-Job)
if ($ForceStoppedIds.Contains($Job.Id)) {
Write-Output "$($Name) - Device unable to process request within 2 minutes"
} else {
Write-Output $Output
}
}
Wait-Job -Timeout 120 will block the thread until the specified timeout or all jobs have completed, hence, is not possible to display progress and wait for them at the same time.
There are 2 alternatives that I can think of, the first one would be to create a proxy command / proxy function around this cmdlet to extend it's functionality.
These blogs demonstrate how to do it:
https://devblogs.microsoft.com/scripting/proxy-functions-spice-up-your-powershell-core-cmdlets/
https://devblogs.microsoft.com/powershell/extending-andor-modifing-commands-with-proxies/
proxy function
You can also follow the indications from this helpful answer.
The other alternative is to define your own function that does a similar work as Wait-Job but, instead of blocking the thread, you can add a loop that will run based on 2 conditions:
That the elapsed time is lower than or equal to the Timeout we passed as argument to the function (we can use Diagnostics.Stopwatch for this).
And, that the jobs are still Running (the $jobs List<T> is still populated).
Note, the function below should work in most cases however is purely for demonstration purposes only and should not be relied upon.
First we define a new function that can be used to display progress as well as wait for our jobs based on a timeout:
using namespace System.Collections.Generic
using namespace System.Diagnostics
using namespace System.Threading
using namespace System.Management.Automation
function Wait-JobWithProgress {
[cmdletbinding()]
param(
[parameter(Mandatory, ValueFromPipeline)]
[object[]] $InputObject,
[parameter()]
[double] $TimeOut
)
begin {
$jobs = [List[object]]::new()
}
process {
foreach($job in $InputObject) {
$jobs.Add($job)
}
}
end {
$timer = [Stopwatch]::StartNew()
$total = $jobs.Count
$completed = 0.1
$expression = { $true }
if($PSBoundParameters.ContainsKey('TimeOut')) {
$expression = { $timer.Elapsed.TotalSeconds -le $TimeOut }
}
while((& $expression) -and $jobs) {
$remaining = $total - $completed
$average = $timer.Elapsed.TotalSeconds / $completed
$estimate = [math]::Round($remaining * $average)
$status = 'Completed Jobs: {0:0} of {1} - ETC: {2}s' -f $completed, $total, $estimate
$progress = #{
Activity = 'Waiting for Jobs'
PercentComplete = $completed / $total * 100
Status = $status
}
Write-Progress #progress
$id = [WaitHandle]::WaitAny($jobs.Finished, 200)
if($id -eq [WaitHandle]::WaitTimeout) {
continue
}
# output this job
$jobs[$id]
# remove this job
$jobs.RemoveAt($id)
$completed++
}
# Stop the jobs not yet Completed and remove them
$jobs | Stop-Job -PassThru | ForEach-Object {
Remove-Job -Job $_
"Job [#{0} - {1}] did not complete on time and was removed." -f $_.Id, $_.Name
} | Write-Warning
Write-Progress #progress -Completed
}
}
Then for testing it, we can create a few jobs with a random timer:
0..10 | ForEach-Object {
Start-Job {
Start-Sleep (Get-Random -Minimum 5 -Maximum 15)
[pscustomobject]#{
Job = $using:_
Result = 'Hello from [Job #{0:D2}]' -f $using:_
}
}
} | Wait-JobWithProgress -TimeOut 10 |
Receive-Job -AutoRemoveJob -Wait | Format-Table -AutoSize
$vcconnect has 50 machines and I need to run this job on all 50 machines, but when I just run it, it crashes the shell.
I would like to limit the parallel execution to 10 at a point of time.
I tried a do-while but I was missing something as it executed on all 50 at same time and crashed my shell.
foreach($vci in $vcconnect){
[array]$jobstart += Start-Job -Name top2 -ArgumentList #($vci, $cred, $from, $to) -ScriptBlock $importcode
}
If you want to run scripts in parallel, and control the maximum number of concurrently running instances, use a RunspacePool:
# Create a RunspacePool, of maximum 10 concurrently running runspaces
$RSPool = [runspacefactory]::CreateRunspacePool(1,10)
$RSPool.Open()
# Start a new "job" for each server
$Jobs = foreach($vci in $vconnect){
$PS = [PowerShell]::Create().AddScript($importcode)
$PS.RunspacePool = $RSPool
$vci, $cred, $from, $to |ForEach-Object {
[void]$PS.AddArgument($_)
}
New-Object psobject -Property #{
Shell = $PS
ComputerName = $vci
ResultHandle = $PS.BeginInvoke()
}
}
# Wait for the "jobs" to finish
do{
Start-Sleep -Milliseconds 500
} while ($Jobs |Where-Object IsCompleted -eq $false)
# Collect results, suppress (but warn on) errors
$Results = foreach($Job in $Jobs){
$Job.Shell.EndInvoke($Job.ResultHandle)
if($Job.Shell.HadErrors){
Write-Warning "$($Job.ComputerName) had $($Job.Shell.Streams.Error.Count) errors:"
$Job.Shell.Streams.Error |Write-Warning
}
}
I am trying to create a thread that will use a shared variable (between main session and the thread)
plus the give the thread ability to use outside function from the main code
I have managed to pass the function for the thread to use
and i have managed to pass read only variable.
my problem is that if i am changing the value of the variable inside the thread and then i try to read it from the main session - i can not see the change in the value, therefore its not shared.
How do i do that?
My goal is to have one thread in the end.
this is my code:
$x = [Hashtable]::Synchronized(#{})
$global:yo = "START"
Function ConvertTo-Hex {
#Write-Output "Function Ran"
write-host "hi"
$x.host.ui.WriteVerboseLine("===========")
write-host $yo
$yo="TEST"
write-host $yo
}
#endregion
$rs = [RunspaceFactory]::CreateRunspace()
$rs.ApartmentState,$rs.ThreadOptions = "STA","ReUseThread"
$rs.Open()
$rs.SessionStateProxy.SetVariable("x",$x)
ls
# create an array and add it to session state
$arrayList = New-Object System.Collections.ArrayList
$arrayList.AddRange(('a','b','c','d','e'))
$x.host = $host
$sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
$sessionstate.Variables.Add((New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry('arrayList', $arrayList, $null)))
$sessionstate.Variables.Add((New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry('x', $x, $null)))
#$sessionstate.Variables.Add((New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry('yo', $yo, $true)))
$sessionstate.Commands.Add((New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList 'ConvertTo-Hex', (Get-Content Function:\ConvertTo-Hex -ErrorAction Stop)))
$runspacepool = [runspacefactory]::CreateRunspacePool(1, 2, $sessionstate, $Host)
$runspacepool.Open()
$ps1 = [powershell]::Create()
$ps1.RunspacePool = $runspacepool
$ps1.AddScript({
for ($i = 1; $i -le 15; $i++)
{
$letter = Get-Random -InputObject (97..122) | % {[char]$_} # a random lowercase letter
$null = $arrayList.Add($letter)
start-sleep -s 1
}
})
$ps1.AddParameter(#($yo))
# on the first thread start a process that adds values to $arrayList every second
$handle1 = $ps1.BeginInvoke()
# now on the second thread, output the value of $arrayList every 1.5 seconds
$ps2 = [powershell]::Create()
$ps2.RunspacePool = $runspacepool
$ps2.AddScript({
Write-Host "ArrayList contents is "
foreach ($i in $arrayList)
{
Write-Host $i -NoNewline
Write-Host " " -NoNewline
}
Write-Host ""
$yo = "BAH"
ConvertTo-Hex
})
$ps2.AddParameter(#($yo))
1..10 | % {
$handle2 = $ps2.BeginInvoke()
if ($handle2.AsyncWaitHandle.WaitOne())
{
$ps2.EndInvoke($handle2)
}
start-sleep -s 1.5
write-host "====================" + $yo
}
Long story short, we are experiencing issues with some of our servers that cause crippling effects on them and I am looking for a way to monitor them, now I have a script that will check the RDP port to make sure that it is open and I am thinking that I want to use get-service and then I will return if it pulled any data or not.
Here is the issue I don't know how to limit the time it will wait for a response before returning false.
[bool](Get-process -ComputerName MYSERVER)
Although I like Ansgars answer with a time-limited job, I think a separate Runspace and async invocation fits this task better.
The major difference here being that a Runspace reuses the in-process thread pool, whereas the PSJob method launches a new process, with the overhead that that entails, such as OS/kernel resources spawning and managing a child process, serializing and deserializing data etc.
Something like this:
function Timeout-Statement {
param(
[scriptblock[]]$ScriptBlock,
[object[]]$ArgumentList,
[int]$Timeout
)
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.Open()
$PS = [powershell]::Create()
$PS.Runspace = $Runspace
$PS = $PS.AddScript($ScriptBlock)
foreach($Arg in $ArgumentList){
$PS = $PS.AddArgument($Arg)
}
$IAR = $PS.BeginInvoke()
if($IAR.AsyncWaitHandle.WaitOne($Timeout)){
$PS.EndInvoke($IAR)
}
return $false
}
Then use that to do:
$ScriptBlock = {
param($ComputerName)
Get-Process #PSBoundParameters
}
$Timeout = 2500 # 2 and a half seconds (2500 milliseconds)
Timeout-Statement $ScriptBlock -ArgumentList "mycomputer.fqdn" -Timeout $Timeout
You could run your check as a background job:
$sb = { Get-Process -ComputerName $args[0] }
$end = (Get-Date).AddSeconds(5)
$job = Start-Job -ScriptBlock $sb -ArgumentList 'MYSERVER'
do {
Start-Sleep 100
$finished = (Get-Job -Id $job.Id).State -eq 'Completed'
} until ($finished -or (Get-Date) -gt $end)
if (-not $finished) {
Stop-Job -Id $job.Id
}
Receive-Job $job.Id
Remove-Job $job.Id
This is a known issue: https://connect.microsoft.com/PowerShell/feedback/details/645165/add-timeout-parameter-to-get-wmiobject
There is a workaround provided Here : https://connect.microsoft.com/PowerShell/feedback/details/645165/add-timeout-parameter-to-get-wmiobject
Function Get-WmiCustom([string]$computername,[string]$namespace,[string]$class,[int]$timeout=15)
{
$ConnectionOptions = new-object System.Management.ConnectionOptions
$EnumerationOptions = new-object System.Management.EnumerationOptions
$timeoutseconds = new-timespan -seconds $timeout
$EnumerationOptions.set_timeout($timeoutseconds)
$assembledpath = "\\" + $computername + "\" + $namespace
#write-host $assembledpath -foregroundcolor yellow
$Scope = new-object System.Management.ManagementScope $assembledpath, $ConnectionOptions
$Scope.Connect()
$querystring = "SELECT * FROM " + $class
#write-host $querystring
$query = new-object System.Management.ObjectQuery $querystring
$searcher = new-object System.Management.ManagementObjectSearcher
$searcher.set_options($EnumerationOptions)
$searcher.Query = $querystring
$searcher.Scope = $Scope
trap { $_ } $result = $searcher.get()
return $result
}
You can call the function like this:
get-wmicustom -class Win32_Process -namespace "root\cimv2" -computername MYSERVER –timeout 1
I have created a Powershell script to do a timed restart on my server. This is a snippet of my code and more specifically the startServers Function.
I have added the global variables into the function in an effort to have an example.
Basically each server name is the name of a shortcut that starts an exe in a separate directory. The names are exactly that and have no .lnk after (I have tried both ways).
The aff array is for processor affinity.
This script should:
For Each Server
Create a New Sysem Diag Process with start info
Start the process
Set affinity
This is not the case: I get an error revolving around the "arg0" and it not being able to start the process. I suppose if anyone has some further explanation of what is going on with args0 and what it is would be helpful.
Code Snippet:
function startServers
{
#Added For Example Purposes
$servers = #( "public", "private" )
$aff = #( 196, 56 )
#End
$i = 0
foreach($s in $servers)
{
$app_name = "$s"
$a = $aff[$i]
$app_arguments = "arg0"
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.Arguments = $app_arguments
$pinfo.FileName = $app_name
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start()
$p.ProcessorAffinity=$a
$i = $i + 1
#Start "$s"
Write-Output "Starting $s"
}
}
function startServers {
$i = 0
$global:pids = #( )
foreach($s in $servers)
{
$a = $aff[$i]
$Starter = Start-Process -FilePath $s -WindowStyle Minimized -PassThru
$Process = Get-Process -Id $Starter.ID
$global:pids += $Process.ID
$Process.ProcessorAffinity = $a
$CurrentTime = updateTime
Write-Output "Starting $s at $CurrentTime"
$i = $i + 1
Start-Sleep -s 10
}
return
}
This is how I ended up solving the problem. I switched over to tracking the process by PID.
I used -PassThru on my Start-Process and I made it a variable.
I then made a variable that would hold the PID after it was started called Process.
Then you do the basic $foo.ProcessorAffinity = foo and you get your result.