Public/Schedules.ps1

<#
.SYNOPSIS
Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions.
 
.DESCRIPTION
Adds a new Schedule with logic to periodically invoke, defined using Cron Expressions.
 
.PARAMETER Name
The Name of the Schedule.
 
.PARAMETER Cron
One, or an Array, of Cron Expressions to define when the Schedule should trigger.
 
.PARAMETER ScriptBlock
The script defining the Schedule's logic.
 
.PARAMETER Limit
The number of times the Schedule should trigger before being removed.
 
.PARAMETER StartTime
A DateTime for when the Schedule should start triggering.
 
.PARAMETER EndTime
A DateTime for when the Schedule should stop triggering, and be removed.
 
.PARAMETER ArgumentList
A hashtable of arguments to supply to the Schedule's ScriptBlock.
 
.PARAMETER FilePath
A literal, or relative, path to a file containing a ScriptBlock for the Schedule's logic.
 
.PARAMETER OnStart
If supplied, the schedule will trigger when the server starts, regardless if the cron-expression matches the current time.
 
.EXAMPLE
Add-PodeSchedule -Name 'RunEveryMinute' -Cron '@minutely' -ScriptBlock { /* logic */ }
 
.EXAMPLE
Add-PodeSchedule -Name 'RunEveryTuesday' -Cron '0 0 * * TUE' -ScriptBlock { /* logic */ }
 
.EXAMPLE
Add-PodeSchedule -Name 'StartAfter2days' -Cron '@hourly' -StartTime [DateTime]::Now.AddDays(2) -ScriptBlock { /* logic */ }
 
.EXAMPLE
Add-PodeSchedule -Name 'Args' -Cron '@minutely' -ScriptBlock { /* logic */ } -ArgumentList @{ Arg1 = 'value' }
#>

function Add-PodeSchedule
{
    [CmdletBinding(DefaultParameterSetName='Script')]
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [string[]]
        $Cron,

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

        [Parameter()]
        [int]
        $Limit = 0,

        [Parameter()]
        [DateTime]
        $StartTime,

        [Parameter()]
        [DateTime]
        $EndTime,

        [Parameter(Mandatory=$true, ParameterSetName='File')]
        [string]
        $FilePath,

        [Parameter()]
        [hashtable]
        $ArgumentList,

        [switch]
        $OnStart
    )

    # error if serverless
    Test-PodeIsServerless -FunctionName 'Add-PodeSchedule' -ThrowError

    # ensure the schedule doesn't already exist
    if ($PodeContext.Schedules.ContainsKey($Name)) {
        throw "[Schedule] $($Name): Schedule already defined"
    }

    # ensure the limit is valid
    if ($Limit -lt 0) {
        throw "[Schedule] $($Name): Cannot have a negative limit"
    }

    # ensure the start/end dates are valid
    if (($null -ne $EndTime) -and ($EndTime -lt [DateTime]::Now)) {
        throw "[Schedule] $($Name): The EndTime value must be in the future"
    }

    if (($null -ne $StartTime) -and ($null -ne $EndTime) -and ($EndTime -le $StartTime)) {
        throw "[Schedule] $($Name): Cannot have a StartTime after the EndTime"
    }

    # if we have a file path supplied, load that path as a scriptblock
    if ($PSCmdlet.ParameterSetName -ieq 'file') {
        $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath
    }

    # check if the scriptblock has any using vars
    $ScriptBlock, $usingVars = Invoke-PodeUsingScriptConversion -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # check for state/session vars
    $ScriptBlock = Invoke-PodeStateScriptConversion -ScriptBlock $ScriptBlock
    $ScriptBlock = Invoke-PodeSessionScriptConversion -ScriptBlock $ScriptBlock

    # add the schedule
    $parsedCrons = ConvertFrom-PodeCronExpressions -Expressions @($Cron)
    $nextTrigger = Get-PodeCronNextEarliestTrigger -Expressions $parsedCrons -StartTime $StartTime -EndTime $EndTime

    $PodeContext.Schedules[$Name] = @{
        Name = $Name
        StartTime = $StartTime
        EndTime = $EndTime
        Crons = $parsedCrons
        CronsRaw = @($Cron)
        Limit = $Limit
        Count = 0
        NextTriggerTime = $nextTrigger
        LastTriggerTime = $null
        Script = $ScriptBlock
        UsingVariables = $usingVars
        Arguments = (Protect-PodeValue -Value $ArgumentList -Default @{})
        OnStart = $OnStart
        Completed = ($null -eq $nextTrigger)
    }
}

<#
.SYNOPSIS
Set the maximum number of concurrent schedules.
 
.DESCRIPTION
Set the maximum number of concurrent schedules.
 
.PARAMETER Maximum
The Maximum number of schdules to run.
 
.EXAMPLE
Set-PodeScheduleConcurrency -Maximum 25
#>

function Set-PodeScheduleConcurrency
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [int]
        $Maximum
    )

    # error if <=0
    if ($Maximum -le 0) {
        throw "Maximum concurrent schedules must be >=1 but got: $($Maximum)"
    }

    # ensure max > min
    $_min = 1
    if ($null -ne $PodeContext.RunspacePools.Schedules) {
        $_min = $PodeContext.RunspacePools.Schedules.GetMinRunspaces()
    }

    if ($_min -gt $Maximum) {
        throw "Maximum concurrent schedules cannot be less than the minimum of $($_min) but got: $($Maximum)"
    }

    # set the max schedules
    $PodeContext.Threads.Schedules = $Maximum
    if ($null -ne $PodeContext.RunspacePools.Schedules) {
        $PodeContext.RunspacePools.Schedules.SetMaxRunspaces($Maximum)
    }
}

<#
.SYNOPSIS
Adhoc invoke a Schedule's logic.
 
.DESCRIPTION
Adhoc invoke a Schedule's logic outside of its defined cron-expression. This invocation doesn't count towards the Schedule's limit.
 
.PARAMETER Name
The Name of the Schedule.
 
.EXAMPLE
Invoke-PodeSchedule -Name 'schedule-name'
#>

function Invoke-PodeSchedule
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Name
    )

    # ensure the schedule exists
    if (!$PodeContext.Schedules.ContainsKey($Name)) {
        throw "Schedule '$($Name)' does not exist"
    }

    # run schedule logic
    Invoke-PodeInternalScheduleLogic -Schedule ($PodeContext.Schedules[$Name])
}

<#
.SYNOPSIS
Removes a specific Schedule.
 
.DESCRIPTION
Removes a specific Schedule.
 
.PARAMETER Name
The Name of the Schedule to be removed.
 
.EXAMPLE
Remove-PodeSchedule -Name 'RenewToken'
#>

function Remove-PodeSchedule
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Name
    )

    $null = $PodeContext.Schedules.Remove($Name)
}

<#
.SYNOPSIS
Removes all Schedules.
 
.DESCRIPTION
Removes all Schedules.
 
.EXAMPLE
Clear-PodeSchedules
#>

function Clear-PodeSchedules
{
    [CmdletBinding()]
    param()

    $PodeContext.Schedules.Clear()
}

<#
.SYNOPSIS
Edits an existing Schedule.
 
.DESCRIPTION
Edits an existing Schedule's properties, such an cron expressions or scriptblock.
 
.PARAMETER Name
The Name of the Schedule.
 
.PARAMETER Cron
Any new Cron Expressions for the Schedule.
 
.PARAMETER ScriptBlock
The new ScriptBlock for the Schedule.
 
.PARAMETER ArgumentList
Any new Arguments for the Schedule.
 
.EXAMPLE
Edit-PodeSchedule -Name 'Hello' -Cron '@minutely'
 
.EXAMPLE
Edit-PodeSchedule -Name 'Hello' -Cron @('@hourly', '0 0 * * TUE')
#>

function Edit-PodeSchedule
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Name,

        [Parameter()]
        [string[]]
        $Cron,

        [Parameter()]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [hashtable]
        $ArgumentList
    )

    # ensure the schedule exists
    if (!$PodeContext.Schedules.ContainsKey($Name)) {
        throw "Schedule '$($Name)' does not exist"
    }

    $_schedule = $PodeContext.Schedules[$Name]

    # edit cron if supplied
    if (!(Test-PodeIsEmpty $Cron)) {
        $_schedule.Crons = (ConvertFrom-PodeCronExpressions -Expressions @($Cron))
        $_schedule.CronsRaw = $Cron
        $_schedule.NextTriggerTime = Get-PodeCronNextEarliestTrigger -Expressions $_schedule.Crons -StartTime $_schedule.StartTime -EndTime $_schedule.EndTime
    }

    # edit scriptblock if supplied
    if (!(Test-PodeIsEmpty $ScriptBlock)) {
        $ScriptBlock, $usingVars = Invoke-PodeUsingScriptConversion -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $ScriptBlock = Invoke-PodeStateScriptConversion -ScriptBlock $ScriptBlock
        $ScriptBlock = Invoke-PodeSessionScriptConversion -ScriptBlock $ScriptBlock
        $_schedule.Script = $ScriptBlock
        $_schedule.UsingVariables = $usingVars
    }

    # edit arguments if supplied
    if (!(Test-PodeIsEmpty $ArgumentList)) {
        $_schedule.Arguments = $ArgumentList
    }
}

<#
.SYNOPSIS
Returns any defined schedules.
 
.DESCRIPTION
Returns any defined schedules, with support for filtering.
 
.PARAMETER Name
Any schedule Names to filter the schedules.
 
.PARAMETER StartTime
An optional StartTime to only return Schedules that will trigger after this date.
 
.PARAMETER EndTime
An optional EndTime to only return Schedules that will trigger before this date.
 
.EXAMPLE
Get-PodeSchedule
 
.EXAMPLE
Get-PodeSchedule -Name Name1, Name2
 
.EXAMPLE
Get-PodeSchedule -Name Name1, Name2 -StartTime [datetime]::new(2020, 3, 1) -EndTime [datetime]::new(2020, 3, 31)
#>

function Get-PodeSchedule
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]
        $Name,

        [Parameter()]
        $StartTime = $null,

        [Parameter()]
        $EndTime = $null
    )

    $schedules = $PodeContext.Schedules.Values

    # further filter by schedule names
    if (($null -ne $Name) -and ($Name.Length -gt 0)) {
        $schedules = @(foreach ($_name in $Name) {
            foreach ($schedule in $schedules) {
                if ($schedule.Name -ine $_name) {
                    continue
                }

                $schedule
            }
        })
    }

    # filter by some start time
    if ($null -ne $StartTime) {
        $schedules = @(foreach ($schedule in $schedules) {
            if (($null -ne $schedule.StartTime) -and ($StartTime -lt $schedule.StartTime)) {
                continue
            }

            $_end = $EndTime
            if ($null -eq $_end) {
                $_end = $schedule.EndTime
            }

            if (($null -ne $schedule.EndTime) -and
                (($StartTime -gt $schedule.EndTime) -or
                    ((Get-PodeScheduleNextTrigger -Name $schedule.Name -DateTime $StartTime) -gt $_end))) {
                continue
            }

            $schedule
        })
    }

    # filter by some end time
    if ($null -ne $EndTime) {
        $schedules = @(foreach ($schedule in $schedules) {
            if (($null -ne $schedule.EndTime) -and ($EndTime -gt $schedule.EndTime)) {
                continue
            }

            $_start = $StartTime
            if ($null -eq $_start) {
                $_start = $schedule.StartTime
            }

            if (($null -ne $schedule.StartTime) -and
                (($EndTime -lt $schedule.StartTime) -or
                    ((Get-PodeScheduleNextTrigger -Name $schedule.Name -DateTime $_start) -gt $EndTime))) {
                continue
            }

            $schedule
        })
    }

    # return
    return $schedules
}

<#
.SYNOPSIS
Get the next trigger time for a Schedule.
 
.DESCRIPTION
Get the next trigger time for a Schedule, either from the Schedule's StartTime or from a defined DateTime.
 
.PARAMETER Name
The Name of the Schedule.
 
.PARAMETER DateTime
An optional specific DateTime to get the next trigger time after. This DateTime must be between the Schedule's StartTime and EndTime.
 
.EXAMPLE
Get-PodeScheduleNextTrigger -Name Schedule1
 
.EXAMPLE
Get-PodeScheduleNextTrigger -Name Schedule1 -DateTime [datetime]::new(2020, 3, 10)
#>

function Get-PodeScheduleNextTrigger
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [string]
        $Name,

        [Parameter()]
        $DateTime = $null
    )

    # ensure the schedule exists
    if (!$PodeContext.Schedules.ContainsKey($Name)) {
        throw "Schedule '$($Name)' does not exist"
    }

    $_schedule = $PodeContext.Schedules[$Name]

    # ensure date is after start/before end
    if (($null -ne $DateTime) -and ($null -ne $_schedule.StartTime) -and ($DateTime -lt $_schedule.StartTime)) {
        throw "Supplied date is before the start time of the schedule at $($_schedule.StartTime)"
    }

    if (($null -ne $DateTime) -and ($null -ne $_schedule.EndTime) -and ($DateTime -gt $_schedule.EndTime)) {
        throw "Supplied date is after the end time of the schedule at $($_schedule.EndTime)"
    }

    # get the next trigger
    if ($null -eq $DateTime) {
        $DateTime = $_schedule.StartTime
    }

    return (Get-PodeCronNextEarliestTrigger -Expressions $_schedule.Crons -StartTime $DateTime -EndTime $_schedule.EndTime)
}

<#
.SYNOPSIS
Automatically loads schedule ps1 files
 
.DESCRIPTION
Automatically loads schedule ps1 files from either a /schedules folder, or a custom folder. Saves space dot-sourcing them all one-by-one.
 
.PARAMETER Path
Optional Path to a folder containing ps1 files, can be relative or literal.
 
.EXAMPLE
Use-PodeSchedules
 
.EXAMPLE
Use-PodeSchedules -Path './my-schedules'
#>

function Use-PodeSchedules
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

    Use-PodeFolder -Path $Path -DefaultPath 'schedules'
}