multithreadingpowershellparallel-processingrunspace

set timeout across domain for Test-NetConnection or 5985 in Powershell


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

Solution

  • 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 .....