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 Timeout
    An optional timeout, in seconds, for the Schedule's logic. (Default: -1 [never timeout])
 
.PARAMETER TimeoutFrom
    An optional timeout from either 'Create' or 'Start'. (Default: 'Create')
 
.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,

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

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

        [switch]
        $OnStart
    )

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

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

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

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

    if (($null -ne $StartTime) -and ($null -ne $EndTime) -and ($EndTime -le $StartTime)) {
        # [Schedule] Name: Cannot have a 'StartTime' after the 'EndTime'
        throw ($PodeLocale.scheduleStartTimeAfterEndTimeExceptionMessage -f $Name)
    }

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

    # check for scoped vars
    $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

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

    $PodeContext.Schedules.Enabled = $true
    $PodeContext.Schedules.Items[$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)
        Timeout         = @{
            Value = $Timeout
            From  = $TimeoutFrom
        }
    }
}

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

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

    # error if <=0
    if ($Maximum -le 0) {
        # Maximum concurrent schedules must be >=1 but got
        throw ($PodeLocale.maximumConcurrentSchedulesInvalidExceptionMessage -f $Maximum)
    }

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

    if ($_min -gt $Maximum) {
        # Maximum concurrent schedules cannot be less than the minimum of $_min but got $Maximum
        throw ($PodeLocale.maximumConcurrentSchedulesLessThanMinimumExceptionMessage -f $_min, $Maximum)
    }

    # set the max schedules
    $PodeContext.Threads.Schedules = $Maximum
    if ($null -ne $PodeContext.RunspacePools.Schedules) {
        $PodeContext.RunspacePools.Schedules.Pool.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.
 
.PARAMETER ArgumentList
A hashtable of arguments to supply to the Schedule's ScriptBlock.
 
.EXAMPLE
Invoke-PodeSchedule -Name 'schedule-name'
#>

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

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

    # ensure the schedule exists
    if (!$PodeContext.Schedules.Items.ContainsKey($Name)) {
        # Schedule 'Name' does not exist
        throw ($PodeLocale.scheduleDoesNotExistExceptionMessage -f $Name)
    }

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

<#
.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.Items.Remove($Name)
}

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

function Clear-PodeSchedules {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [CmdletBinding()]
    param()

    $PodeContext.Schedules.Items.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.Items.ContainsKey($Name)) {
        # Schedule 'Name' does not exist
        throw ($PodeLocale.scheduleDoesNotExistExceptionMessage -f $Name)
    }

    $_schedule = $PodeContext.Schedules.Items[$Name]

    # edit cron if supplied
    if (!(Test-PodeIsEmpty $Cron)) {
        $_schedule.Crons = (ConvertFrom-PodeCronExpression -Expression @($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 = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
        $_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.Items.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
Tests whether the passed Schedule exists.
 
.DESCRIPTION
Tests whether the passed Schedule exists by its name.
 
.PARAMETER Name
The Name of the Schedule.
 
.EXAMPLE
if (Test-PodeSchedule -Name ScheduleName) { }
#>

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

    return (($null -ne $PodeContext.Schedules.Items) -and $PodeContext.Schedules.Items.ContainsKey($Name))
}

<#
.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.Items.ContainsKey($Name)) {
        # Schedule 'Name' does not exist
        throw ($PodeLocale.scheduleDoesNotExistExceptionMessage -f $Name)
    }

    $_schedule = $PodeContext.Schedules.Items[$Name]

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

    if (($null -ne $DateTime) -and ($null -ne $_schedule.EndTime) -and ($DateTime -gt $_schedule.EndTime)) {
        # Supplied date is after the end time of the schedule at $_schedule.EndTime
        throw ($PodeLocale.suppliedDateAfterScheduleEndTimeExceptionMessage -f $_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 {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]
        $Path
    )

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

<#
.SYNOPSIS
Get all Schedule Processes.
 
.DESCRIPTION
Get all Schedule Processes, with support for filtering.
 
.PARAMETER Name
An optional Name of the Schedule to filter by, can be one or more.
 
.PARAMETER Id
An optional ID of the Schedule process to filter by, can be one or more.
 
.PARAMETER State
An optional State of the Schedule process to filter by, can be one or more.
 
.EXAMPLE
Get-PodeScheduleProcess
 
.EXAMPLE
Get-PodeScheduleProcess -Name 'ScheduleName'
 
.EXAMPLE
Get-PodeScheduleProcess -Id 'ScheduleId'
 
.EXAMPLE
Get-PodeScheduleProcess -State 'Running'
#>

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

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

        [Parameter()]
        [ValidateSet('All', 'Pending', 'Running', 'Completed', 'Failed')]
        [string[]]
        $State = 'All'
    )

    $processes = $PodeContext.Schedules.Processes.Values

    # filter processes by name
    if (($null -ne $Name) -and ($Name.Length -gt 0)) {
        $processes = @(foreach ($_name in $Name) {
                foreach ($process in $processes) {
                    if ($process.Schedule -ine $_name) {
                        continue
                    }

                    $process
                }
            })
    }

    # filter processes by id
    if (($null -ne $Id) -and ($Id.Length -gt 0)) {
        $processes = @(foreach ($_id in $Id) {
                foreach ($process in $processes) {
                    if ($process.ID -ine $_id) {
                        continue
                    }

                    $process
                }
            })
    }

    # filter processes by status
    if ($State -inotcontains 'All') {
        $processes = @(foreach ($process in $processes) {
                if ($State -inotcontains $process.State) {
                    continue
                }

                $process
            })
    }

    # return processes
    return $processes
}