I've got this running but I changed the PortOpen
because if systems were down, this block would take too long to run. However, after I made the change, I thought I was good until I had come to test it again and again and somehow, I have different results for systems. Sometimes the same system comes up twice without the other. And sometimes doubles of both. I had hoped I can set the timeout faster this way. The PortOpen
line does work, but I'm not sure how to get it to work as a whole within this job. Basically, I'm not sure how to structure this to be able to leverage that piece.
$ProgressPreference = 'Ignore'
$maxThreads = 32
$pool = [runspacefactory]::CreateRunspacePool(1, $maxThreads,
[initialsessionstate]::CreateDefault2(),
$Host)
$pool.Open()
$jobs = [System.Collections.Generic.List[hashtable]]::new()
$servers = ( Get-ADComputer -filter * -searchbase "OU=Sales, DC=example,DC=com" | Select-Object -expand Name )
$servers | ForEach-Object {
$instance = [powershell]::Create().AddScript({
param($computer)
[pscustomobject]@{
Computer = $computer
Port = 5985
#PortOpen = Test-NetConnection $computer -Port 5985 -InformationLevel Quiet
PortOpen = [System.Net.Sockets.TcpClient]::new().ConnectAsync($computer, 5985).Wait(100)
}
}).AddParameters(@{ computer = $_ })
$instance.RunspacePool = $pool
$jobs.Add(@{
Instance = $instance
Async = $instance.BeginInvoke()
})
}
$result = while($jobs) {
$job = $jobs[[System.Threading.WaitHandle]::WaitAny($jobs.Async.AsyncWaitHandle)]
$job.Instance.EndInvoke($job.Async)
$job.Instance.Dispose()
$null = $jobs.Remove($job)
}
$pool.Dispose()
$online = @()
$online += $result | Where-Object PortOpen | ForEach-Object Computer
Write-Output $online
As I've stated in comments, I don't really see something wrong with your code that could be causing output being duplicated or missing output. I do see though that 100ms to await the task is too little time, I would recommend at least 1 second.
Another point is that WaitHandle.WaitAny
supports top 63 wait handles in STA Mode (PowerShell default), from the Remarks section of the documentation:
The maximum number of the wait handles is 64, and 63 if the current thread is in STA state.
Ideally, you would need to consume the tasks if that number is reached in the first loop before continuing with the rest (this requires additional logic that checks if $jobs.Count -eq 63
).
Lastly, I would recommend error handling in your code, this is to avoid aggregate exceptions caused by the .Wait
method.
Now, since you're already aware of and currently have installed the PSParallelPipeline
Module, this is how I would approach the code:
$maxThreads = 32
$timespan = [timespan]::FromSeconds(5)
$result = Get-ADComputer -Filter * -SearchBase 'OU=Sales, DC=example,DC=com' | Invoke-Parallel {
$outObject = [pscustomobject]@{
Computer = $_.Name
Port = 5985
PortOpen = $false # <= Assume Port Closed Here
}
try {
$tcp = [System.Net.Sockets.TcpClient]::new()
# if the connection was successful, this property gets updated
$outObject.PortOpen = $tcp.ConnectAsync($_.Name, 5985).Wait($using:timespan)
}
catch {
# ignore any errors here, this avoids the exception:
# System.Net.Sockets.SocketException (10060):
# A connection attempt failed because the connected party did not properly respond after a period of time,
# or established connection failed because connected host has failed to respond.
}
finally {
# dispose the TCP instance
if ($tcp) {
$tcp.Dispose()
}
}
# output the object
$outObject
} -ThrottleLimit $maxThreads
# get all computers with the port open
$computersWithPortOpen = $result | Where-Object PortOpen | ForEach-Object Computer
# do other stuff with `$computersWithPortOpen`
Invoke-Command -ComputerName $computersWithPortOpen .....