Private/Schedules.ps1
function Find-PodeSchedule { param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Name ) return $PodeContext.Schedules.Items[$Name] } function Test-PodeSchedulesExist { return (($null -ne $PodeContext.Schedules) -and (($PodeContext.Schedules.Enabled) -or ($PodeContext.Schedules.Items.Count -gt 0))) } function Start-PodeScheduleRunspace { if (!(Test-PodeSchedulesExist)) { return } Add-PodeTimer -Name '__pode_schedule_housekeeper__' -Interval 30 -ScriptBlock { try { if ($PodeContext.Schedules.Processes.Count -eq 0) { return } $now = [datetime]::UtcNow foreach ($key in $PodeContext.Schedules.Processes.Keys.Clone()) { try { $process = $PodeContext.Schedules.Processes[$key] # if it's completed or expired, dispose and remove if ($process.Runspace.Handler.IsCompleted -or ($process.ExpireTime -lt $now)) { Close-PodeScheduleInternal -Process $process } } catch { $_ | Write-PodeErrorLog } } $process = $null } catch { $_ | Write-PodeErrorLog } } $script = { try { # select the schedules that trigger on-start $_now = [DateTime]::Now $PodeContext.Schedules.Items.Values | Where-Object { $_.OnStart } | ForEach-Object { Invoke-PodeInternalSchedule -Schedule $_ } # complete any schedules Complete-PodeInternalSchedule -Now $_now # first, sleep for a period of time to get to 00 seconds (start of minute) Start-Sleep -Seconds (60 - [DateTime]::Now.Second) while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { try { $_now = [DateTime]::Now # select the schedules that need triggering $PodeContext.Schedules.Items.Values | Where-Object { !$_.Completed -and (($null -eq $_.StartTime) -or ($_.StartTime -le $_now)) -and (($null -eq $_.EndTime) -or ($_.EndTime -ge $_now)) -and (Test-PodeCronExpressions -Expressions $_.Crons -DateTime $_now) } | ForEach-Object { try { Invoke-PodeInternalSchedule -Schedule $_ } catch { $_ | Write-PodeErrorLog } } # complete any schedules Complete-PodeInternalSchedule -Now $_now # cron expression only goes down to the minute, so sleep for 1min Start-Sleep -Seconds (60 - [DateTime]::Now.Second) } catch { $_ | Write-PodeErrorLog } } } catch [System.OperationCanceledException] { $_ | Write-PodeErrorLog -Level Debug } catch { $_ | Write-PodeErrorLog throw $_.Exception } } Add-PodeRunspace -Type Main -Name 'Schedules' -ScriptBlock $script -NoProfile } function Close-PodeScheduleInternal { param( [Parameter()] [hashtable] $Process ) if ($null -eq $Process) { return } Close-PodeDisposable -Disposable $Process.Runspace.Pipeline $null = $PodeContext.Schedules.Processes.Remove($Process.ID) } <# .SYNOPSIS Completes schedules that have exceeded their end time. .DESCRIPTION The `Complete-PodeInternalSchedule` function checks for schedules that have an end time and marks them as completed if their end time is earlier than the current time. .PARAMETER Now Specifies the current date and time. This parameter is mandatory. .INPUTS None. You cannot pipe objects to Complete-PodeInternalSchedule. .OUTPUTS None. The function modifies the state of schedules in the PodeContext. .EXAMPLE # Example usage: $now = Get-Date Complete-PodeInternalSchedule -Now $now # Schedules that have ended are marked as completed. .NOTES This is an internal function and may change in future releases of Pode. #> function Complete-PodeInternalSchedule { param( [Parameter(Mandatory = $true)] [datetime] $Now ) # set any expired schedules as being completed foreach ($schedule in $PodeContext.Schedules.Items.Values) { if (($null -ne $schedule.EndTime) -and ($schedule.EndTime -lt $Now)) { $schedule.Completed = $true } } } function Invoke-PodeInternalSchedule { param( [Parameter(Mandatory = $true)] $Schedule ) $Schedule.OnStart = $false # increment total number of triggers for the schedule $Schedule.Count++ # set last trigger to current next trigger if ($null -ne $Schedule.NextTriggerTime) { $Schedule.LastTriggerTime = $Schedule.NextTriggerTime } else { $Schedule.LastTriggerTime = [datetime]::Now } # check if we have hit the limit, and remove if (($Schedule.Limit -gt 0) -and ($Schedule.Count -ge $Schedule.Limit)) { $Schedule.Completed = $true } # reset the cron and next trigger if (!$Schedule.Completed) { $Schedule.Crons = Reset-PodeRandomCronExpressions -Expressions $Schedule.Crons $Schedule.NextTriggerTime = Get-PodeCronNextEarliestTrigger -Expressions $Schedule.Crons -EndTime $Schedule.EndTime } else { $Schedule.NextTriggerTime = $null } # trigger the schedules logic Invoke-PodeInternalScheduleLogic -Schedule $Schedule } function Invoke-PodeInternalScheduleLogic { param( [Parameter(Mandatory = $true)] [hashtable] $Schedule, [Parameter()] [hashtable] $ArgumentList = $null ) try { # generate processId for schedule $processId = New-PodeGuid # setup event param $parameters = @{ ProcessId = $processId ArgumentList = $ArgumentList } # what is the expire time if using "create" timeout? $expireTime = [datetime]::MaxValue $createTime = [datetime]::UtcNow if (($Schedule.Timeout.From -ieq 'Create') -and ($Schedule.Timeout.Value -ge 0)) { $expireTime = $createTime.AddSeconds($Schedule.Timeout.Value) } # add the schedule process $PodeContext.Schedules.Processes[$processId] = @{ ID = $processId Schedule = $Schedule.Name Runspace = $null CreateTime = $createTime StartTime = $null ExpireTime = $expireTime Timeout = $Schedule.Timeout State = 'Pending' } # start the schedule runspace $scriptblock = Get-PodeScheduleScriptBlock $runspace = Add-PodeRunspace -Type Schedules -Name $Schedule.Name -ScriptBlock $scriptblock -Parameters $parameters -PassThru # add runspace to process $PodeContext.Schedules.Processes[$processId].Runspace = $runspace } catch { $_ | Write-PodeErrorLog } } function Get-PodeScheduleScriptBlock { return { param($ProcessId, $ArgumentList) try { # get the schedule process, error if not found $process = $PodeContext.Schedules.Processes[$ProcessId] if ($null -eq $process) { # Schedule process does not exist: $ProcessId throw ($PodeLocale.scheduleProcessDoesNotExistExceptionMessage -f $ProcessId) } # set start time and state $process.StartTime = [datetime]::UtcNow $process.State = 'Running' # set expire time if 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 schedule, error if not found $schedule = Find-PodeSchedule -Name $process.Schedule if ($null -eq $schedule) { throw ($PodeLocale.scheduleDoesNotExistExceptionMessage -f $process.Schedule) } # build the script arguments $ScheduleEvent = @{ Lockable = $PodeContext.Threading.Lockables.Global Sender = $schedule Timestamp = [DateTime]::UtcNow Metadata = @{} } $_args = @{ Event = $ScheduleEvent } if ($null -ne $schedule.Arguments) { foreach ($key in $schedule.Arguments.Keys) { $_args[$key] = $schedule.Arguments[$key] } } if ($null -ne $ArgumentList) { foreach ($key in $ArgumentList.Keys) { $_args[$key] = $ArgumentList[$key] } } # add any using variables if ($null -ne $schedule.UsingVariables) { foreach ($usingVar in $schedule.UsingVariables) { $_args[$usingVar.NewName] = $usingVar.Value } } # invoke the script from the schedule Invoke-PodeScriptBlock -ScriptBlock $schedule.Script -Arguments $_args -Scoped -Splat # set 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 } } } |