Private/Runspaces.ps1

<#
.SYNOPSIS
    Adds a new runspace to Pode with the specified type and script block.
 
.DESCRIPTION
    The `Add-PodeRunspace` function creates a new PowerShell runspace within Pode
    based on the provided type and script block. This function allows for additional
    customization through parameters, output streaming, and runspace management options.
 
.PARAMETER Type
    The type of runspace to create. Accepted values are:
    'Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks',
    'WebSockets', 'Files', 'Timers'.
 
.PARAMETER ScriptBlock
    The script block to execute within the runspace. This script block will be
    added to the runspace's pipeline.
 
.PARAMETER Parameters
    Optional parameters to pass to the script block.
 
.PARAMETER OutputStream
    A PSDataCollection object to handle output streaming for the runspace.
 
.PARAMETER Forget
    If specified, the pipeline's output will not be stored or remembered.
 
.PARAMETER NoProfile
    If specified, the runspace will not load any modules or profiles.
 
.PARAMETER PassThru
    If specified, returns the pipeline and handler for custom processing.
 
.EXAMPLE
    Add-PodeRunspace -Type 'Tasks' -ScriptBlock {
        # Your script code here
    }
#>


function Add-PodeRunspace {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files', 'Timers')]
        [string]
        $Type,

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

        [Parameter()]
        $Parameters,

        [Parameter()]
        [System.Management.Automation.PSDataCollection[psobject]]
        $OutputStream = $null,

        [switch]
        $Forget,

        [switch]
        $NoProfile,

        [switch]
        $PassThru,

        [string]
        $Name,

        [string]
        $Id = '1'
    )

    try {
        # Define the script block to open the runspace and set its state.
        $openRunspaceScript = {
            param($Type, $Name, $NoProfile)
            try {
                # Set the runspace name.
                Set-PodeCurrentRunspaceName -Name $Name

                if (!$NoProfile) {
                    # Import necessary internal Pode modules for the runspace.
                    Import-PodeModulesInternal

                    # Add required PowerShell drives.
                    Add-PodePSDrivesInternal
                }

                # Mark the runspace as 'Ready' to process requests.
                $PodeContext.RunspacePools[$Type].State = 'Ready'
            }
            catch {
                # Handle errors, setting the runspace state to 'Error' if applicable.
                if ($PodeContext.RunspacePools[$Type].State -ieq 'waiting') {
                    $PodeContext.RunspacePools[$Type].State = 'Error'
                }

                # Output the error details to the default stream and rethrow.
                $_ | Out-Default
                $_.ScriptStackTrace | Out-Default
                throw
            }
        }

        # Create a PowerShell pipeline.
        $ps = [powershell]::Create()
        $ps.RunspacePool = $PodeContext.RunspacePools[$Type].Pool

        # Add the script block and parameters to the pipeline.
        $null = $ps.AddScript($openRunspaceScript)
        $null = $ps.AddParameters(
            @{
                'Type'      = $Type
                'Name'      = "Pode_$($Type)_$($Name)_$($Id)"
                'NoProfile' = $NoProfile.IsPresent
            }
        )

        # Add the main script block to the pipeline.
        $null = $ps.AddScript($ScriptBlock)

        # Add any provided parameters to the script block.
        if (!(Test-PodeIsEmpty $Parameters)) {
            $Parameters.Keys | ForEach-Object {
                $null = $ps.AddParameter($_, $Parameters[$_])
            }
        }

        # Begin invoking the pipeline, with or without output streaming.
        if ($null -eq $OutputStream) {
            $pipeline = $ps.BeginInvoke()
        }
        else {
            $pipeline = $ps.BeginInvoke($OutputStream, $OutputStream)
        }

        # Handle forgetting, returning, or storing the pipeline.
        if ($Forget) {
            $null = $pipeline
        }
        elseif ($PassThru) {
            return @{
                Pipeline = $ps
                Handler  = $pipeline
            }
        }
        else {
            $PodeContext.Runspaces += @{
                Pool     = $Type
                Pipeline = $ps
                Handler  = $pipeline
                Stopped  = $false
            }
        }
    }
    catch {
        # Log and throw any exceptions encountered during execution.
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
}

<#
.SYNOPSIS
    Closes and disposes of the Pode runspaces, listeners, receivers, watchers, and optionally runspace pools.
 
.DESCRIPTION
    This function checks and waits for all Listeners, Receivers, and Watchers to be disposed of
    before proceeding to close and dispose of the runspaces and optionally the runspace pools.
    It ensures a clean shutdown by managing the disposal of resources in a specified order.
    The function handles serverless and regular server environments differently, skipping
    disposal actions in serverless contexts.
 
.PARAMETER ClosePool
    Specifies whether to close and dispose of the runspace pools along with the runspaces.
    This is optional and should be specified if the pools need to be explicitly closed.
 
.EXAMPLE
    Close-PodeRunspace -ClosePool
    This example closes all runspaces and their associated pools, ensuring that all resources are properly disposed of.
 
.OUTPUTS
    None
    Outputs from this function are primarily internal state changes and verbose logging.
#>

function Close-PodeRunspace {
    param(
        [switch]
        $ClosePool
    )

    # Early return if server is serverless, as disposal is not required.
    if ($PodeContext.Server.IsServerless) {
        return
    }

    try {
        # Only proceed if there are runspaces to dispose of.
        if (!(Test-PodeIsEmpty $PodeContext.Runspaces)) {
            Write-Verbose 'Waiting until all Listeners are disposed'

            $count = 0
            $continue = $false
            # Attempts to dispose of resources for up to 10 seconds.
            while ($count -le 10) {
                Start-Sleep -Seconds 1
                $count++

                $continue = $false
                # Check each listener, receiver, and watcher; if any are not disposed, continue waiting.
                foreach ($listener in $PodeContext.Listeners) {
                    if (!$listener.IsDisposed) {
                        $continue = $true
                        break
                    }
                }

                foreach ($receiver in $PodeContext.Receivers) {
                    if (!$receiver.IsDisposed) {
                        $continue = $true
                        break
                    }
                }

                foreach ($watcher in $PodeContext.Watchers) {
                    if (!$watcher.IsDisposed) {
                        $continue = $true
                        break
                    }
                }
                # If undisposed resources exist, continue waiting.
                if ($continue) {
                    continue
                }

                break
            }

            Write-Verbose 'All Listeners disposed'

            # now dispose runspaces
            Write-Verbose 'Disposing Runspaces'
            $runspaceErrors = @(foreach ($item in $PodeContext.Runspaces) {
                    if ($item.Stopped) {
                        continue
                    }

                    try {
                        # only do this, if the pool is in error
                        if ($PodeContext.RunspacePools[$item.Pool].State -ieq 'error') {
                            $item.Pipeline.EndInvoke($item.Handler)
                        }
                    }
                    catch {
                        "$($item.Pool) runspace failed to load: $($_.Exception.InnerException.Message)"
                    }

                    Close-PodeDisposable -Disposable $item.Pipeline
                    $item.Stopped = $true
                })

            # dispose of schedule runspaces
            if ($PodeContext.Schedules.Processes.Count -gt 0) {
                foreach ($key in $PodeContext.Schedules.Processes.Keys.Clone()) {
                    Close-PodeScheduleInternal -Process $PodeContext.Schedules.Processes[$key]
                }
            }

            # dispose of task runspaces
            if ($PodeContext.Tasks.Processes.Count -gt 0) {
                foreach ($key in $PodeContext.Tasks.Processes.Keys.Clone()) {
                    Close-PodeTaskInternal -Process $PodeContext.Tasks.Processes[$key]
                }
            }

            $PodeContext.Runspaces = @()
            Write-Verbose 'Runspaces disposed'
        }

        # close/dispose the runspace pools
        if ($ClosePool) {
            Close-PodeRunspacePool
        }

        # Check for and throw runspace errors if any occurred during disposal.
        if (($null -ne $runspaceErrors) -and ($runspaceErrors.Length -gt 0)) {
            foreach ($err in $runspaceErrors) {
                if ($null -eq $err) {
                    continue
                }

                throw $err
            }
        }

        # garbage collect
        Invoke-PodeGC
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
}