Private/Server.ps1

<#
.SYNOPSIS
    Starts the internal Pode server, initializing configurations, middleware, routes, and runspaces.
 
.DESCRIPTION
    This function sets up and starts the internal Pode server. It initializes the server's configurations, routes, middleware, runspace pools, logging, and schedules. It also handles different server modes, such as normal, service, or serverless (Azure Functions, AWS Lambda). The function ensures all necessary components are ready and operational before triggering the server's start.
 
.PARAMETER Request
    Provides request data for serverless execution scenarios.
 
.PARAMETER Browse
    A switch to enable browsing capabilities for HTTP servers.
 
.EXAMPLE
    Start-PodeInternalServer
        Starts the Pode server in the normal mode with all necessary components initialized.
 
.EXAMPLE
    Start-PodeInternalServer -Request $RequestData
        Starts the Pode server in serverless mode, passing the required request data.
 
.EXAMPLE
    Start-PodeInternalServer -Browse
        Starts the Pode HTTP server with browsing capabilities enabled.
 
.NOTES
    - This function is used to start the Pode server, either initially or after a restart.
    - Handles specific setup for serverless types like Azure Functions and AWS Lambda.
    - This is an internal function used within the Pode framework and is subject to change in future releases.
#>

function Start-PodeInternalServer {
    param(
        [Parameter()]
        $Request,

        [switch]
        $Browse
    )

    try {
        $null = Test-PodeVersionPwshEOL -ReportUntested

        #Show starting console
        Show-PodeConsoleInfo -ShowTopSeparator

        # run start event hooks
        Invoke-PodeEvent -Type Starting

        # setup temp drives for internal dirs
        Add-PodePSInbuiltDrive

        # setup inbuilt scoped vars
        Add-PodeScopedVariablesInbuilt

        # create the shared runspace state
        New-PodeRunspaceState

        # if iis, setup global middleware to validate token
        Initialize-PodeIISMiddleware

        # load any secret vaults
        Import-PodeSecretVaultsIntoRegistry

        # get the server's script and invoke it - to set up routes, timers, middleware, etc
        $_script = $PodeContext.Server.Logic
        if (Test-PodePath -Path $PodeContext.Server.LogicPath -NoStatus) {
            $_script = Convert-PodeFileToScriptBlock -FilePath $PodeContext.Server.LogicPath
        }

        $_script = Convert-PodeScopedVariables -ScriptBlock $_script -Exclude Session, Using
        $null = Invoke-PodeScriptBlock -ScriptBlock $_script -NoNewClosure -Splat

        #Validate OpenAPI definitions
        Test-PodeOADefinitionInternal

        # load any modules/snapins
        Import-PodeSnapinsIntoRunspaceState
        Import-PodeModulesIntoRunspaceState

        # load any functions
        Import-PodeFunctionsIntoRunspaceState -ScriptBlock $_script

        # run starting event hooks
        Invoke-PodeEvent -Type Start

        # start timer for task housekeeping
        Start-PodeTaskHousekeeper

        # start the cache housekeeper
        Start-PodeCacheHousekeeper

        # create timer/schedules for auto-restarting
        New-PodeAutoRestartServer

        # start the runspace pools for web, schedules, etc
        New-PodeRunspacePool
        Open-PodeRunspacePool

        if (!$PodeContext.Server.IsServerless) {
            # start runspace for loggers
            Start-PodeLoggingRunspace

            # start runspace for schedules
            Start-PodeScheduleRunspace

            # start runspace for timers
            Start-PodeTimerRunspace

            # start runspace for gui
            Start-PodeGuiRunspace

            # start runspace for websockets
            Start-PodeWebSocketRunspace

            # start runspace for file watchers
            Start-PodeFileWatcherRunspace
        }

        # start the appropriate server
        $PodeContext.Server.EndpointsInfo = @()

        # - service
        if ($PodeContext.Server.IsService) {
            Start-PodeServiceServer
        }

        # - serverless
        elseif ($PodeContext.Server.IsServerless) {
            switch ($PodeContext.Server.ServerlessType.ToUpperInvariant()) {
                'AZUREFUNCTIONS' {
                    Start-PodeAzFuncServer -Data $Request
                }

                'AWSLAMBDA' {
                    Start-PodeAwsLambdaServer -Data $Request
                }
            }
        }

        # - normal
        else {
            # start each server type
            foreach ($_type in $PodeContext.Server.Types) {
                switch ($_type.ToUpperInvariant()) {
                    'SMTP' {
                        $PodeContext.Server.EndpointsInfo += (Start-PodeSmtpServer)
                    }

                    'TCP' {
                        $PodeContext.Server.EndpointsInfo += (Start-PodeTcpServer)
                    }

                    'HTTP' {
                        $PodeContext.Server.EndpointsInfo += (Start-PodeWebServer -Browse:$Browse)
                    }
                }
            }

            if ($PodeContext.Server.EndpointsInfo) {
                # Re-order the endpoints
                $PodeContext.Server.EndpointsInfo = Get-PodeSortedEndpointsInfo -EndpointsInfo $PodeContext.Server.EndpointsInfo

                # now go back through, and wait for each server type's runspace pool to be ready
                foreach ($pool in ($PodeContext.Server.EndpointsInfo.Pool | Sort-Object -Unique)) {
                    $start = [datetime]::Now
                    Write-Verbose "Waiting for the $($pool) RunspacePool to be Ready"

                    # wait
                    while ($PodeContext.RunspacePools[$pool].State -ieq 'Waiting') {
                        Start-Sleep -Milliseconds 100
                    }

                    Write-Verbose "$($pool) RunspacePool $($PodeContext.RunspacePools[$pool].State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]"

                    # errored?
                    if ($PodeContext.RunspacePools[$pool].State -ieq 'error') {
                        throw ($PodeLocale.runspacePoolFailedToLoadExceptionMessage -f $pool) #"$($pool) RunspacePool failed to load"
                    }
                }
            }
            else {
                Write-Verbose 'No Endpoints defined.'
            }
        }


        # set the start time of the server (start and after restart)
        $PodeContext.Metrics.Server.StartTime = [datetime]::UtcNow

        # Trigger the start
        Close-PodeCancellationTokenRequest -Type Start

        Show-PodeConsoleInfo

        # run running event hooks
        Invoke-PodeEvent -Type Running


    }
    catch {
        throw
    }
}

<#
.SYNOPSIS
    Restarts the internal Pode server by clearing all configurations, contexts, and states, and reinitializing the server.
 
.DESCRIPTION
    This function performs a comprehensive restart of the internal Pode server. It resets all contexts, clears caches, schedules, timers, middleware, and security configurations, and reinitializes the server state. It also reloads the server configuration if enabled and increments the server restart count.
 
.EXAMPLE
    Restart-PodeInternalServer
        Restarts the Pode server, clearing all configurations and states before starting it again.
.NOTES
    - This function is called internally to restart the Pode server gracefully.
    - Handles cancellation tokens, clean-up processes, and reinitialization.
    - This is an internal function used within the Pode framework and is subject to change in future releases.
#>

function Restart-PodeInternalServer {

    if (!$PodeContext.Tokens.Restart.IsCancellationRequested) {
        return
    }

    try {
        Reset-PodeCancellationToken -Type Start
        # inform restart
        # Restarting server...
        Show-PodeConsoleInfo

        # run restarting event hooks
        Invoke-PodeEvent -Type Restarting

        # cancel the session token
        Close-PodeCancellationTokenRequest -Type Cancellation, Terminate

        # close all current runspaces
        Close-PodeRunspace -ClosePool

        # remove all of the pode temp drives
        Remove-PodePSDrive

        # clear-up modules
        $PodeContext.Server.Modules.Clear()

        # clear up timers, schedules and loggers
        Clear-PodeHashtableInnerKey -InputObject $PodeContext.Server.Routes
        Clear-PodeHashtableInnerKey -InputObject $PodeContext.Server.Handlers
        Clear-PodeHashtableInnerKey -InputObject $PodeContext.Server.Events

        if ($null -ne $PodeContext.Server.Verbs) {
            $PodeContext.Server.Verbs.Clear()
        }

        $PodeContext.Server.Views.Clear()
        $PodeContext.Timers.Items.Clear()
        $PodeContext.Server.Logging.Types.Clear()

        # clear schedules
        $PodeContext.Schedules.Items.Clear()
        $PodeContext.Schedules.Processes.Clear()

        # clear tasks
        $PodeContext.Tasks.Items.Clear()
        $PodeContext.Tasks.Processes.Clear()

        # clear file watchers
        $PodeContext.Fim.Items.Clear()

        # auto-importers
        Reset-PodeAutoImportConfiguration

        # clear middle/endware
        $PodeContext.Server.Middleware = @()
        $PodeContext.Server.Endware = @()

        # clear body parsers
        $PodeContext.Server.BodyParsers.Clear()

        # clear security headers
        $PodeContext.Server.Security.Headers.Clear()
        Clear-PodeHashtableInnerKey -InputObject $PodeContext.Server.Security.Cache

        # clear endpoints
        $PodeContext.Server.Endpoints.Clear()
        $PodeContext.Server.EndpointsMap.Clear()

        # clear openapi
        $PodeContext.Server.OpenAPI = Initialize-PodeOpenApiTable -DefaultDefinitionTag $PodeContext.Server.Configuration.Web.OpenApi.DefaultDefinitionTag
        # clear the sockets
        $PodeContext.Server.Signals.Enabled = $false
        $PodeContext.Server.Signals.Listener = $null
        $PodeContext.Server.Http.Listener = $null
        $PodeContext.Listeners = @()
        $PodeContext.Receivers = @()
        $PodeContext.Watchers = @()

        # set view engine back to default
        $PodeContext.Server.ViewEngine = @{
            Type           = 'html'
            Extension      = 'html'
            ScriptBlock    = $null
            UsingVariables = $null
            IsDynamic      = $false
        }

        # clear up cookie sessions
        $PodeContext.Server.Sessions.Clear()

        # clear up authentication methods
        $PodeContext.Server.Authentications.Methods.Clear()
        $PodeContext.Server.Authorisations.Methods.Clear()

        # clear up shared state
        $PodeContext.Server.State.Clear()

        # clear scoped variables
        $PodeContext.Server.ScopedVariables.Clear()

        # clear cache
        $PodeContext.Server.Cache.Items.Clear()
        $PodeContext.Server.Cache.Storage.Clear()

        # clear up secret vaults/cache
        Unregister-PodeSecretVaultsInternal -ThrowError
        $PodeContext.Server.Secrets.Vaults.Clear()
        $PodeContext.Server.Secrets.Keys.Clear()

        # dispose mutex/semaphores
        Clear-PodeLockables
        Clear-PodeMutexes
        Clear-PodeSemaphores

        # clear up output
        $PodeContext.Server.Output.Variables.Clear()

        # reset type if smtp/tcp
        $PodeContext.Server.Types = @()

        # recreate the session tokens
        Reset-PodeCancellationToken -Type Cancellation, Restart, Suspend, Resume, Terminate, Disable

        # if the configuration is enable reload it
        if ( $PodeContext.Server.Configuration.Enabled) {
            # reload the configuration
            $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext -ConfigFile $PodeContext.Server.Configuration.ConfigFile
        }

        # restart the server
        $PodeContext.Metrics.Server.RestartCount++

        Start-PodeInternalServer

        # run restarting event hooks
        Invoke-PodeEvent -Type Restart
    }
    catch {
        $_ | Write-PodeErrorLog
        throw $_.Exception
    }
}

<#
.SYNOPSIS
    Determines whether the Pode server should remain open based on its configuration and active components.
 
.DESCRIPTION
    The `Test-PodeServerKeepOpen` function evaluates the current server state and configuration
    to decide whether to keep the Pode server running. It considers the existence of timers,
    schedules, file watchers, service mode, and server types to make this determination.
 
    - If any timers, schedules, or file watchers are active, the server remains open.
    - If the server is not running as a service and is either serverless or has no types defined,
      the server will close.
    - In other cases, the server will stay open.
 
 .NOTES
    This is an internal function used within the Pode framework and is subject to change in future releases.
#>

function Test-PodeServerKeepOpen {
    # if we have any timers/schedules/fim - keep open
    if ((Test-PodeTimersExist) -or (Test-PodeSchedulesExist) -or (Test-PodeFileWatchersExist)) {
        return $true
    }

    # if not a service, and not any type/serverless - close server
    if (!$PodeContext.Server.IsService -and (($PodeContext.Server.Types.Length -eq 0) -or $PodeContext.Server.IsServerless)) {
        return $false
    }

    # keep server open
    return $true
}

<#
.SYNOPSIS
    Suspends the Pode server and its associated runspaces.
 
.DESCRIPTION
    This function suspends the Pode server by pausing all associated runspaces and ensuring they enter a debug state.
    It triggers the 'Suspend' event, updates the server's suspension status, and provides progress and feedback during the suspension process.
    This is primarily used internally by the Pode framework to handle server suspension.
 
.PARAMETER Timeout
    The maximum time, in seconds, to wait for each runspace to be suspended before timing out.
    The default timeout is 30 seconds.
 
.EXAMPLE
    Suspend-PodeServerInternal -Timeout 60
    # Suspends the Pode server with a timeout of 60 seconds.
 
.NOTES
    This is an internal function used within the Pode framework and is subject to change in future releases.
#>

function Suspend-PodeServerInternal {
    param(
        [int]
        $Timeout = 30
    )

    # Exit early if no suspension request is pending or if the server is already suspended.
    if (!(Test-PodeCancellationTokenRequest -Type Suspend) -or (Test-PodeServerState -State Suspended)) {
        return
    }

    try {
        # Display suspension initiation message in the console.
        Show-PodeConsoleInfo

        # Trigger the 'Suspending' event for the server.
        Invoke-PodeEvent -Type Suspending

        # Retrieve all Pode-related runspaces for tasks and schedules.
        $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_Tasks_*' -or $_.Name -like 'Pode_Schedules_*' }

        # Iterate over each runspace to initiate suspension.
        $runspaces | Foreach-Object {
            $originalName = $_.Name
            $startTime = [DateTime]::UtcNow
            $elapsedTime = 0

            # Activate debug mode on the runspace to suspend it.
            Enable-RunspaceDebug -BreakAll -Runspace $_

            while (! $_.Debugger.InBreakpoint) {
                # Calculate elapsed suspension time.
                $elapsedTime = ([DateTime]::UtcNow - $startTime).TotalSeconds

                # Exit loop if the runspace is already completed.
                if ($_.Name.StartsWith('_')) {
                    Write-Verbose "$originalName runspace has been completed."
                    break
                }

                # Handle timeout scenario and raise an error if exceeded.
                if ($elapsedTime -ge $Timeout) {
                    $errorMsg = "$($_.Name) failed to suspend (Timeout reached after $Timeout seconds)."
                    Write-PodeHost $errorMsg -ForegroundColor Red
                    throw $errorMsg
                }

                # Pause briefly before rechecking the runspace state.
                Start-Sleep -Milliseconds 200
            }
        }
    }
    catch {
        # Log any errors encountered during suspension.
        $_ | Write-PodeErrorLog

        # Force a resume action to ensure server continuity.
        Set-PodeResumeToken
    }
    finally {
        # Reset cancellation token if a cancellation request was made.
        if ($PodeContext.Tokens.Cancellation.IsCancellationRequested) {
            Reset-PodeCancellationToken -Type Cancellation
        }

        # Trigger the 'Suspend' event for the server.
        Invoke-PodeEvent -Type Suspend

        # Brief pause before refreshing console output.
        Start-Sleep -Seconds 1

        # Refresh the console and display updated information.
        Show-PodeConsoleInfo
    }
}

<#
.SYNOPSIS
    Resumes the Pode server from a suspended state.
 
.DESCRIPTION
    This function resumes the Pode server, ensuring all associated runspaces are restored to their normal execution state.
    It triggers the 'Resume' event, updates the server's status, and clears the console for a refreshed view.
    The function also provides timeout handling and progress feedback during the resumption process.
 
.PARAMETER Timeout
    The maximum time, in seconds, to wait for each runspace to exit its suspended state before timing out.
    The default timeout is 30 seconds.
 
.EXAMPLE
    Resume-PodeServerInternal
    # Resumes the Pode server after being suspended.
 
.NOTES
    This is an internal function used within the Pode framework and may change in future releases.
#>

function Resume-PodeServerInternal {

    param(
        [int]
        $Timeout = 30
    )

    # Exit early if no resumption request is pending.
    if (!(Test-PodeCancellationTokenRequest -Type Resume)) {
        return
    }

    try {
        # Display resumption initiation message in the console.
        Show-PodeConsoleInfo

        # Trigger the 'Resuming' event for the server.
        Invoke-PodeEvent -Type Resuming

        # Pause briefly to allow processes to stabilize.
        Start-Sleep -Seconds 1

        # Retrieve all runspaces currently in a suspended (debug) state.
        $runspaces = Get-Runspace | Where-Object { ($_.Name -like 'Pode_Tasks_*' -or $_.Name -like 'Pode_Schedules_*') -and $_.Debugger.InBreakpoint }

        # Iterate over each suspended runspace to restore normal execution.
        $runspaces | ForEach-Object {
            # Track the start time for timeout calculations.
            $startTime = [DateTime]::UtcNow
            $elapsedTime = 0

            # Disable debug mode on the runspace to resume it.
            Disable-RunspaceDebug -Runspace $_

            while ($_.Debugger.InBreakpoint) {
                # Calculate the elapsed time since resumption started.
                $elapsedTime = ([DateTime]::UtcNow - $startTime).TotalSeconds

                # Handle timeout scenario and raise an error if exceeded.
                if ($elapsedTime -ge $Timeout) {
                    $errorMsg = "$($_.Name) failed to resume (Timeout reached after $Timeout seconds)."
                    Write-PodeHost $errorMsg -ForegroundColor Red
                    throw $errorMsg
                }

                # Pause briefly before rechecking the runspace state.
                Start-Sleep -Milliseconds 200
            }
        }

        # Pause briefly before refreshing the console view.
        Start-Sleep -Seconds 1
    }
    catch {
        # Log any errors encountered during the resumption process.
        $_ | Write-PodeErrorLog

        # Force a restart action to recover the server.
        Close-PodeCancellationTokenRequest -Type Restart
    }
    finally {
        # Reset the resume cancellation token for future suspension/resumption cycles.
        Reset-PodeCancellationToken -Type Resume

        # Trigger the 'Resume' event for the server.
        Invoke-PodeEvent -Type Resume

        # Clear the console and display refreshed header information.
        Show-PodeConsoleInfo
    }
}

<#
.SYNOPSIS
    Enables new requests by removing the access limit rule that blocks requests when the Pode Watchdog service is active.
 
.DESCRIPTION
    This function checks if the access limit rule associated with the Pode Watchdog client is present, and if so, it removes it to allow new requests.
    This effectively re-enables access to the service by removing the request blocking.
 
.NOTES
    This function is used internally to manage Watchdog monitoring and may change in future releases of Pode.
#>

function Enable-PodeServerInternal {

    # Check if the Watchdog middleware exists and remove it if found to allow new requests
    if (!(Test-PodeServerState -State Running) -or (Test-PodeServerIsEnabled) ) {
        return
    }

    # Trigger the 'Enable' event for the server.
    Invoke-PodeEvent -Type Enable

    # remove the access limit rule
    Remove-PodeLimitRateRule -Name $PodeContext.Server.AllowedActions.DisableSettings.LimitRuleName
}

<#
.SYNOPSIS
    Disables new requests by adding an access limit rule that blocks incoming requests when the Pode Watchdog service is active.
 
.DESCRIPTION
    This function adds an access limit rule to the Pode server to block new incoming requests while the Pode Watchdog client is active.
    It responds to all new requests with a 503 Service Unavailable status and sets a 'Retry-After' header, indicating when the service will be available again.
 
.NOTES
    This function is used internally to manage Watchdog monitoring and may change in future releases of Pode.
#>

function Disable-PodeServerInternal {
    if (!(Test-PodeServerState -State Running) -or (!( Test-PodeServerIsEnabled)) ) {
        return
    }

    # Trigger the 'Enable' event for the server.
    Invoke-PodeEvent -Type Disable

    # add a rate limit rule to block new requests, returning a 503 Service Unavailable status
    $limitName = $PodeContext.Server.AllowedActions.DisableSettings.LimitRuleName
    $duration = $PodeContext.Server.AllowedActions.DisableSettings.RetryAfter * 1000

    Add-PodeLimitRateRule -Name $limitName -Limit 0 -Duration $duration -StatusCode 503 -Priority ([int]::MaxValue) -Component @(
        New-PodeLimitIPComponent -Group
    )
}

function Test-PodeServerIsEnabled {
    return !(Test-PodeLimitRateRule -Name $PodeContext.Server.AllowedActions.DisableSettings.LimitRuleName)
}