Private/Tasks.ps1

function Test-PodeTasksExist {
    return (($null -ne $PodeContext.Tasks) -and (($PodeContext.Tasks.Enabled) -or ($PodeContext.Tasks.Items.Count -gt 0)))
}

function Start-PodeTaskHousekeeper {
    if (!(Test-PodeTasksExist)) {
        return
    }

    Add-PodeTimer -Name '__pode_task_housekeeper__' -Interval 30 -ScriptBlock {
        try {
            if ($PodeContext.Tasks.Processes.Count -eq 0) {
                return
            }

            $now = [datetime]::UtcNow

            foreach ($key in $PodeContext.Tasks.Processes.Keys.Clone()) {
                try {
                    $process = $PodeContext.Tasks.Processes[$key]

                    # has it completed or expire? then dispose and remove
                    if ((($null -ne $process.CompletedTime) -and ($process.CompletedTime.AddMinutes(1) -lt $now)) -or ($process.ExpireTime -lt $now)) {
                        Close-PodeTaskInternal -Process $process
                        continue
                    }

                    # if completed, and no completed time, set it
                    if ($process.Runspace.Handler.IsCompleted -and ($null -eq $process.CompletedTime)) {
                        $process.CompletedTime = $now
                    }
                }
                catch {
                    $_ | Write-PodeErrorLog
                }
            }

            $process = $null
        }
        catch {
            $_ | Write-PodeErrorLog
        }
    }
}

function Close-PodeTaskInternal {
    param(
        [Parameter()]
        [hashtable]
        $Process
    )

    if ($null -eq $Process) {
        return
    }

    Close-PodeDisposable -Disposable $Process.Runspace.Pipeline
    Close-PodeDisposable -Disposable $Process.Result
    $null = $PodeContext.Tasks.Processes.Remove($Process.ID)
}

function Invoke-PodeInternalTask {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Task,

        [Parameter()]
        [hashtable]
        $ArgumentList = $null,

        [Parameter()]
        [int]
        $Timeout = -1,

        [Parameter()]
        [ValidateSet('Default', 'Create', 'Start')]
        [string]
        $TimeoutFrom = 'Default'
    )

    try {
        # generate processId for task
        $processId = New-PodeGuid

        # setup event param
        $parameters = @{
            ProcessId    = $processId
            ArgumentList = $ArgumentList
        }

        # what's the timeout values to use?
        if ($TimeoutFrom -eq 'Default') {
            $TimeoutFrom = $Task.Timeout.From
        }

        if ($Timeout -eq -1) {
            $Timeout = $Task.Timeout.Value
        }

        # what is the expire time if using "create" timeout?
        $expireTime = [datetime]::MaxValue
        $createTime = [datetime]::UtcNow

        if (($TimeoutFrom -ieq 'Create') -and ($Timeout -ge 0)) {
            $expireTime = $createTime.AddSeconds($Timeout)
        }

        # add task process
        $result = [System.Management.Automation.PSDataCollection[psobject]]::new()
        $PodeContext.Tasks.Processes[$processId] = @{
            ID            = $processId
            Task          = $Task.Name
            Runspace      = $null
            Result        = $result
            CreateTime    = $createTime
            StartTime     = $null
            CompletedTime = $null
            ExpireTime    = $expireTime
            Timeout       = @{
                Value = $Timeout
                From  = $TimeoutFrom
            }
            State         = 'Pending'
        }

        # start the task runspace
        $scriptblock = Get-PodeTaskScriptBlock
        $runspace = Add-PodeRunspace -Type Tasks -Name $Task.Name -ScriptBlock $scriptblock -Parameters $parameters -OutputStream $result -PassThru

        # add runspace to process
        $PodeContext.Tasks.Processes[$processId].Runspace = $runspace

        # return the task process
        return $PodeContext.Tasks.Processes[$processId]
    }
    catch {
        $_ | Write-PodeErrorLog
    }
}

function Get-PodeTaskScriptBlock {
    return {
        param($ProcessId, $ArgumentList)

        try {
            $process = $PodeContext.Tasks.Processes[$ProcessId]
            if ($null -eq $process) {
                # Task process does not exist: $ProcessId
                throw ($PodeLocale.taskProcessDoesNotExistExceptionMessage -f $ProcessId)
            }

            # set the start time and state
            $process.StartTime = [datetime]::UtcNow
            $process.State = 'Running'

            # set the expire time of timeout based on "start" time
            if (($process.Timeout.From -ieq 'Start') -and ($process.Timeout.Value -ge 0)) {
                $process.ExpireTime = $process.StartTime.AddSeconds($process.Timeout.Value)
            }

            # get the task, error if not found
            $task = $PodeContext.Tasks.Items[$process.Task]
            if ($null -eq $task) {
                # Task does not exist
                throw ($PodeLocale.taskDoesNotExistExceptionMessage -f $process.Task)
            }

            # build the script arguments
            $TaskEvent = @{
                Lockable  = $PodeContext.Threading.Lockables.Global
                Sender    = $task
                Timestamp = [DateTime]::UtcNow
                Metadata  = @{}
            }

            $_args = @{ Event = $TaskEvent }

            if ($null -ne $task.Arguments) {
                foreach ($key in $task.Arguments.Keys) {
                    $_args[$key] = $task.Arguments[$key]
                }
            }

            if ($null -ne $ArgumentList) {
                foreach ($key in $ArgumentList.Keys) {
                    $_args[$key] = $ArgumentList[$key]
                }
            }

            # add any using variables
            if ($null -ne $task.UsingVariables) {
                foreach ($usingVar in $task.UsingVariables) {
                    $_args[$usingVar.NewName] = $usingVar.Value
                }
            }

            # invoke the script from the task
            Invoke-PodeScriptBlock -ScriptBlock $task.Script -Arguments $_args -Scoped -Splat -Return

            # set the state to completed
            $process.State = 'Completed'
        }
        catch {
            # update the state
            if ($null -ne $process) {
                $process.State = 'Failed'
            }

            # log the error
            $_ | Write-PodeErrorLog
        }
        finally {
            Invoke-PodeGC
        }
    }
}

function Wait-PodeNetTaskInternal {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [System.Threading.Tasks.Task]
        $Task,

        [Parameter()]
        [int]
        $Timeout = -1
    )

    # do we need a timeout?
    $timeoutTask = $null
    if ($Timeout -gt 0) {
        $timeoutTask = [System.Threading.Tasks.Task]::Delay($Timeout)
    }

    # set the check task
    if ($null -eq $timeoutTask) {
        $checkTask = $Task
    }
    else {
        $checkTask = [System.Threading.Tasks.Task]::WhenAny($Task, $timeoutTask)
    }

    # is there a cancel token to supply?
    if (($null -eq $PodeContext) -or ($null -eq $PodeContext.Tokens.Cancellation.Token)) {
        $checkTask.Wait()
    }
    else {
        $checkTask.Wait($PodeContext.Tokens.Cancellation.Token)
    }

    # if the main task isnt complete, it timed out
    if (($null -ne $timeoutTask) -and (!$Task.IsCompleted)) {
        # "Task has timed out after $($Timeout)ms")
        throw [System.TimeoutException]::new($PodeLocale.taskTimedOutExceptionMessage -f $Timeout)
    }

    # only return a value if the result has one
    if ($null -ne $Task.Result) {
        return $Task.Result
    }
}

function Wait-PodeTaskInternal {
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]
        $Task,

        [Parameter()]
        [int]
        $Timeout = -1
    )

    # timeout needs to be in milliseconds
    if ($Timeout -gt 0) {
        $Timeout *= 1000
    }

    # wait for the pipeline to finish processing
    $null = $Task.Runspace.Handler.AsyncWaitHandle.WaitOne($Timeout)

    # get the current result
    $result = $Task.Result.ReadAll()

    # close the task
    Close-PodeTask -Task $Task

    # only return a value if the result has one
    if (($null -ne $result) -and ($result.Count -gt 0)) {
        return $result
    }
}