Public/Start-RpRunspaceJob.ps1

function Start-RpRunspaceJob {
    <#
    .SYNOPSIS
    Starts a PowerShell job in a runspace and tracks it.
 
    .DESCRIPTION
    This function starts a PowerShell job in a runspace and tracks its status.
    By default, it uses the existing session state (environment variables and
    session variables), but you can create a new runspace with specific modules,
    assemblies, and functions loaded.
 
    .NOTES
    - A timer (`$script:RunspaceCleanupTimer`) is used to periodically invoke
    the `Watch-RpRunspaces` function, which is responsible for cleaning up
    completed runspaces, logging output, and updating the UI.
 
    - The runspace cleanup relies on:
    - 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.
 
    .COMPONENT
    Runspaces
 
    .PARAMETER ModulesToLoad
    Modules to load into the runspace environment when not using existing state.
 
    .PARAMETER AssembliesToLoad
    Assemblies to load into the runspace environment when not using existing state.
 
    .PARAMETER FunctionsToImport
    Functions to import into the runspace environment when not using existing state.
 
    .PARAMETER ScriptBlock
    The scriptblock to execute in the runspace.
 
    .PARAMETER ArgumentList
    List of arguments to pass to the scriptblock.
 
    .PARAMETER Argument
    Single argument to pass to the scriptblock.
 
    .PARAMETER UseExistingRunspaceState
    Uses the current session state (variables and environment) in the new runspace.
 
    .PARAMETER UseNewRunspace
    Creates a completely new runspace without using the existing session state.
 
    .PARAMETER Id
    If specified, returns the runspace job ID.
 
    .PARAMETER uiElement
    Optional UI TextBox element to update with runspace status.
 
    .PARAMETER RunspaceJobs
    Collection of runspace jobs for tracking.
 
    .EXAMPLE
    Start-RpRunspaceJob -ScriptBlock { Get-Process } -RunspaceJobs $global:RunspaceJobs
 
    .EXAMPLE
    Start-RpRunspaceJob -ScriptBlock { Get-Process } -ModulesToLoad @('MyModule') `
    -AssembliesToLoad @('MyAssembly') -RunspaceJobs $global:RunspaceJobs
 
    .NOTES
    This function starts a PowerShell job in a runspace and creates identifiers for tracking.
    Relies on "$script:RunspaceJobs = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))"
 
    .LINK
    https://www.remotepro.dev/en-US/Start-RpRunspaceJob
    #>


    [CmdletBinding(DefaultParameterSetName = 'UseExistingRunspace')]
    param (
        [Parameter(ParameterSetName = 'NewRunspace')]
        [string[]]$ModulesToLoad = @(),

        [Parameter(ParameterSetName = 'NewRunspace')]
        [string[]]$AssembliesToLoad = @(),

        [Parameter(ParameterSetName = 'NewRunspace')]
        [string[]]$FunctionsToImport = @(),

        [Parameter(Mandatory = $true)]
        [scriptblock]$ScriptBlock,

        [Parameter()]
        [object[]]$ArgumentList = @(),

        [Parameter()]
        [object]$Argument,

        [Parameter(ParameterSetName = 'UseExistingRunspace')]
        [switch]$UseExistingRunspaceState,  # Default parameter set (use current session state)

        [Parameter(ParameterSetName = 'UseNewRunspace')]
        [switch]$UseNewRunspace,  # Use a completely new runspace

        [Parameter()]
        [switch]$Id,  # Return job ID

        [Parameter()]
        [System.Windows.Controls.TextBox]$uiElement,  # Optional UI element for updates

        [Parameter()]
        [System.Collections.ArrayList]$RunspaceJobs  # Collection of runspace jobs for tracking
    )

    # Create or configure the initial session state
    $initialSessionState = [initialsessionstate]::CreateDefault()

    if ($UseExistingRunspaceState) {
        # Replicate environment variables in the initial session state
        $environmentVariables = Get-ChildItem Env: | ForEach-Object { [PSCustomObject]@{ Name = $_.Name; Value = $_.Value } }
        foreach ($var in $environmentVariables) {
            $envEntry = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry($var.Name, $var.Value, "Environment variable replication")
            $initialSessionState.Variables.Add($envEntry)
        }

        # Replicate all variables from the current session in the initial session state
        $currentVariables = Get-Variable
        foreach ($var in $currentVariables) {
            # Avoid system variables and read-only variables
            if ($var.Options -ne "None" -or $var.Options -ne "ReadOnly") {
                continue
            }
            $varValue = Get-Variable -Name $var.Name -ValueOnly
            $varEntry = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry($var.Name, $varValue, $var.Description)
            $initialSessionState.Variables.Add($varEntry)
        }

        # Create the runspace
        $runspace = [runspacefactory]::CreateRunspacePool($initialSessionState)
        $runspace.ApartmentState = [System.Threading.ApartmentState]::STA
        $runspace.Open()

        # Configure the PowerShell instance to use the new runspace
        $psInstance = [powershell]::Create()
        $psInstance.RunspacePool = $runspace

    } elseif ($UseNewRunspace) {
        # Create a completely new runspace
        $runspace = [runspacefactory]::CreateRunspace()
        $runspace.ApartmentState = [System.Threading.ApartmentState]::STA
        $runspace.Open()

        # Configure the PowerShell instance to use the new runspace
        $psInstance = [powershell]::Create()
        $psInstance.Runspace = $runspace

    } else {
        # Load specified modules, assemblies, and functions into the initial session state
        foreach ($module in $ModulesToLoad) {
            $initialSessionState.ImportPSModule($module)
        }

        foreach ($assembly in $AssembliesToLoad) {
            $assemblyEntry = New-Object System.Management.Automation.Runspaces.SessionStateAssemblyEntry($assembly)
            $initialSessionState.Assemblies.Add($assemblyEntry)
        }

        foreach ($function in $FunctionsToImport) {
            $functionScript = (Get-Item Function:\$function).ScriptBlock
            $initialSessionState.Commands.Add((New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry($function, $functionScript)))
        }

        # Use an existing runspace (potentially passed in)
        # This assumes that the runspace would be managed externally if this switch is on
        $runspace = [runspacefactory]::CreateRunspace()
        $runspace.ApartmentState = [System.Threading.ApartmentState]::STA
        $runspace.Open()

        # Configure the PowerShell instance to use the new runspace
        $psInstance = [powershell]::Create()
        $psInstance.Runspace = $runspace
    }

    # Set the ScriptRoot in the new runspace's environment
    $psInstance.AddScript("`$script:ScriptRoot = '$script:ScriptRoot'").Invoke()
    $psInstance.AddScript($ScriptBlock)

    if ($ArgumentList){
        foreach ($arg in $ArgumentList) {
            $psInstance.AddArgument($arg)
        }
    }

    if ($Argument) {
        $psInstance.AddArgument($Argument)
    }

    $asyncResult = $psInstance.BeginInvoke()
    $runspaceID = $runspace.InstanceId
    # Ensure all components are valid
    if ($null -eq $psInstance -or $null -eq $runspace -or $null -eq $asyncResult) {
        Write-Host "Failed to initialize one or more components of the job."
    } else {
        # Add the job to a global job tracking list
        try {
            [void]$RunspaceJobs.Add([PSCustomObject]@{
                PowerShell = $psInstance
                Runspace = $runspace
                AsyncResult = $asyncResult
                RunspaceID = $runspaceID
            })
            # Generate a log entry for job removal
            $logAddJobText = "Job added successfully."
            $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
            $logAddJobMessage = "$timestamp - INFO - GUID: $runspaceID - $logAddJobText."

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

            Write-Host = $logAddJobMessage

            # GUID of Runspace
            if ($ReturnID) {
                return $runspaceID
            }

        } catch {
            # Generate a log entry for job removal
            $logAddJobErrorText = "Error adding job to global list: $_"
            $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
            $logAddJobMessage = "$timestamp - ERROR - GUID: $runspaceID - $logAddJobErrorText."

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

            Write-Host = $logAddJobErrorMessage
        }
    }
}