Public/Watch-RpRunspaces.ps1

function Watch-RpRunspaces {
    <#
    .SYNOPSIS
    Monitors and manages runspaces running in the background.
 
    .DESCRIPTION
    This function iterates through the collection of runspace jobs and checks if
    they have completed. For completed jobs, it collects the results, logs the
    output, updates the UI (if provided), and safely removes the job from the
    global collection.
 
    .COMPONENT
    Runspaces
 
    .PARAMETER LogPath
    The path to the log file where job statuses and results are logged. This is
    mandatory.
 
    .PARAMETER uiElement
    The UI TextBox element that displays job statuses and messages. This is
    optional.
 
    .PARAMETER RunspaceJobs
    A synchronized ArrayList that tracks runspaces currently running in the
    background. This is mandatory.
 
    .PARAMETER RunspaceResults
    A synchronized ArrayList that stores the results from completed runspaces.
    This is mandatory.
 
    .PARAMETER OpenRunspaces
    A PSObject that holds static information about open runspaces and their jobs.
    This is mandatory.
 
    .EXAMPLE
    Watch-RpRunspaces -LogPath "C:\Logs\RunspaceLog.txt" -uiElement $textBoxElement
    -RunspaceJobs $script:RunspaceJobs -RunspaceResults $script:RunspaceResults
    -OpenRunspaces $script:openRunspaces
 
    This example monitors the runspaces and logs the output to "C:\Logs\RunspaceLog.txt".
    It also updates the UI element with the job statuses and messages.
 
    .NOTES
    - The function relies on the following module variables and collections:
    - Initialize-RpRunspaceJobs $script:RunspaceJobs: Tracks each runspace
        dispatched.
    - Initialize-RpRunspaceResults $script:RunspaceResults: Collects results from
        runspaces.
    - Initialize-RpOpenRunspaces $script:openRunspaces: Static collection of active
        runspaces.
    - The log path is determined using the Get-RpLogPath cmdlet, which provides the
        path to the RemotePro AppData location.
    - UI updates rely on a TextBox control ($uiElement), where job and status
        messages are displayed. This UI element must be bound to "Runspace_Mutex_Log"
        for proper updates.
    - Runspace job collection and maintenance of runspaces running in the
        background.
    - Log relies on .psm1 module manifest definition variable ...
    - The cmdlet Get-RpLogPath for providing location to the RemotePro AppData
        location.
    - UI relies on...
    - [System.Windows.Controls.TextBox]$uiElement to be set to "Runspace_Mutex_Log"
        to update the UI
    - Jobs indexing relies on...
    - $script:RunspaceJobs = [System.Collections.ArrayList]::Synchronized((New-Object
        System.Collections.ArrayList))
    - Results relies on...
    - "$script:RunspaceResults = [System.Collections.ArrayList]::Synchronized((New-Object
        System.Collections.ArrayList))"
    - If implemented, static runspaces from "$script:openRunspaces" which relies on...
    - "$script:openRunspaces = New-Object PSObject -Property @{ Jobs = New-Object
        System.Collections.ObjectModel.ObservableCollection[object]}"
 
    .LINK
    https://www.remotepro.dev/en-US/Watch-RpRunspaces
    #>


    [CmdletBinding()]
    param (
        [Parameter()]
        [string]$LogPath,

        [Parameter()]
        [System.Windows.Controls.TextBox]$uiElement,

        [Parameter()]
        [System.Collections.ArrayList]$RunspaceJobs,

        [Parameter()]
        [System.Collections.ArrayList]$RunspaceResults,

        [Parameter()]
        [psobject]$OpenRunspaces
    )

    Foreach ($job in $RunspaceJobs.ToArray()) {  # Use ToArray() to avoid collection modification issues
        # Get an array of all RunspaceIds from the jobs stored in $script:openRunspaces.Jobs
        $storedRunspaceIds = $script:RpOpenRunspaces.Jobs | ForEach-Object { $_.RunspaceId }

        $OpenRunspaces.Jobs
        # Check if the job is completed and its runspaceId is NOT in the stored RunspaceIds
        if ($job.AsyncResult.IsCompleted -eq $True -and -not ($storedRunspaceIds -contains $job.runspaceId)) {
            try {
                Write-Host "$($_)"
                # replace GUID with runspace GUID
                #$rsGUID = Get-Runspace -InstanceId $job.RunspaceID
                # Get the output of the PowerShell job
                #$RSOutput = $job.PowerShell.EndInvoke($job.AsyncResult)
                $RunspaceResults.Add($job.PowerShell.EndInvoke($job.AsyncResult))

                # Get the index of the current output
                $currentIndex = $RunspaceResults.Count - 1

                # Display the current result in Out-GridView
                #$RunspaceResults[$currentIndex] | Out-GridView -Title "Runspace Result $currentIndex"

                # Convert the output to a string for logging
                $runspaceOutputString = $RunspaceResults[$currentIndex] | Out-String

                # Dispose of the PowerShell instance and runspace
                $job.PowerShell.Dispose()
                $job.Runspace.Dispose()


                # Generate a unique log identifier
                #$LogGuid = [System.Guid]::NewGuid().ToString()
                $LogGuid = $job.RunspaceID
                $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
                $script:logMessage = "$timestamp - INFO - GUID: $LogGuid - Runspace output: $runspaceOutputString"
                $jobSuccessful = $true
            } catch {
                $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
                $LogGuid = $job.RunspaceID
                $logMessage = "$timestamp - ERROR - GUID: $LogGuid - Error managing runspace job: $_"
                $jobSuccessful = $false
            }

            # UI and Log message update
            Set-RpMutexLogAndUI -logPath $LogPath -message $logMessage -uiElement $uiElement

            # Critical section to safely remove the job from the global list
            [System.Threading.Monitor]::Enter($RunspaceJobs.SyncRoot)
            try {
                $RunspaceJobs.Remove($job)

                # Remove the job from OpenRunspaces
                $script:RpOpenRunspaces.Jobs.Remove($job)

                # Generate a log entry for job removal
                $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
                $removalStatus = if ($jobSuccessful) { "Job removed successfully." } else { "Job removed with error." }
                if ($jobSuccessful) {
                    $logRemovalMessage = "$timestamp - INFO - GUID: $LogGuid - $removalStatus."
                } else {
                    $logRemovalMessage = "$timestamp - ERROR - GUID: $LogGuid - $removalStatus."
                }

                # UI and Log message update
                Set-RpMutexLogAndUI -logPath $LogPath -message $logRemovalMessage -uiElement $uiElement
            } finally {
                [System.Threading.Monitor]::Exit($RunspaceJobs.SyncRoot)
            }

            # Invoke garbage collection manually (ToDo: make this optional)
            [GC]::Collect()
            [GC]::WaitForPendingFinalizers()
        }
    }
}