diff --git a/docs/Tutorials/Schedules.md b/docs/Tutorials/Schedules.md index 9c754bfb1..2665501d0 100644 --- a/docs/Tutorials/Schedules.md +++ b/docs/Tutorials/Schedules.md @@ -126,6 +126,29 @@ Get-PodeSchedule -Name Name1 Get-PodeSchedule -Name Name1, Name2 ``` +## Getting Schedule Processes + +You can retrieve a list of processes triggered by Schedules via [`Get-PodeScheduleProcess`](../../Functions/Schedules/Get-PodeScheduleProcess) - this will return processes created either by a Schedule's natural time-based trigger, or via [`Invoke-PodeSchedule`](../../Functions/Schedules/Invoke-PodeSchedule). + +You can either retrieve all processes, or filter them by Schedule Name, or Process ID/Status: + +```powershell +# retrieves all schedule processes +Get-PodeScheduleProcess + +# retrieves all schedule processes for the "ScheduleName" process +Get-PodeScheduleProcess -Name 'ScheduleName' + +# retrieves the schedule process with ID "ScheduleId" +Get-PodeScheduleProcess -Id 'ScheduleId' + +# retrieves all running schedule processes +Get-PodeScheduleProcess -State 'Running' + +# retrieves all pending schedule processes for "ScheduleName" +Get-PodeScheduleProcess -Name 'ScheduleName' -State 'Running' +``` + ## Next Trigger Time When you retrieve a Schedule using [`Get-PodeSchedule`](../../Functions/Schedules/Get-PodeSchedule), each Schedule object will already have its next trigger time as `NextTriggerTime`. However, if you want to get a trigger time further ino the future than this, then you can use the [`Get-PodeScheduleNextTrigger`](../../Functions/Schedules/Get-PodeScheduleNextTrigger) function. diff --git a/docs/Tutorials/Tasks.md b/docs/Tutorials/Tasks.md index ac257a98c..f6a98e339 100644 --- a/docs/Tutorials/Tasks.md +++ b/docs/Tutorials/Tasks.md @@ -51,7 +51,7 @@ Add-PodeTask -Name 'Example' -ArgumentList @{ Name = 'Rick'; Environment = 'Mult } ``` -Tasks parameters **must** be bound in the param block in order to be used, but the values for the paramters can be set through the `-ArgumentList` hashtable parameter in either the Add-PodeTask definition or when invoking the task. The following snippet would populate the parameters to the task with the same values as the above example but the `-ArgumentList` parameter is populated during invocation. Note that Keys in the `-ArgumentList` hashtable parameter set during invocation override the same Keys set during task creation: +Tasks parameters **must** be bound in the param block in order to be used, but the values for the paramters can be set through the `-ArgumentList` hashtable parameter in either the Add-PodeTask definition or when invoking the task. The following snippet would populate the parameters to the task with the same values as the above example but the `-ArgumentList` parameter is populated during invocation. Note that Keys in the `-ArgumentList` hashtable parameter set during invocation override the same Keys set during task creation: ```powershell Add-PodeTask -Name 'Example' -ScriptBlock { @@ -184,6 +184,29 @@ Get-PodeTask -Name Example1 Get-PodeTask -Name Example1, Example2 ``` +## Getting Task Processes + +You can retrieve a list of processes triggered by Tasks via [`Get-PodeTaskProcess`](../../Functions/Tasks/Get-PodeTaskProcess) - this will return processes created via [`Invoke-PodeTask`](../../Functions/Tasks/Invoke-PodeTask). + +You can either retrieve all processes, or filter them by Task Name, or Process ID/Status: + +```powershell +# retrieves all task processes +Get-PodeTaskProcess + +# retrieves all task processes for the "TaskName" process +Get-PodeTaskProcess -Name 'TaskName' + +# retrieves the task process with ID "TaskId" +Get-PodeTaskProcess -Id 'TaskId' + +# retrieves all running task processes +Get-PodeTaskProcess -State 'Running' + +# retrieves all pending task processes for "TaskName" +Get-PodeTaskProcess -Name 'TaskName' -State 'Running' +``` + ## Task Object !!! warning @@ -191,8 +214,8 @@ Get-PodeTask -Name Example1, Example2 The following is the structure of the Task object internally, as well as the object that is returned from [`Get-PodeTask`](../../Functions/Tasks/Get-PodeTask): -| Name | Type | Description | -| ---- | ---- | ----------- | -| Name | string | The name of the Task | -| Script | scriptblock | The scriptblock of the Task | -| Arguments | hashtable | The arguments supplied from ArgumentList | +| Name | Type | Description | +| --------- | ----------- | ---------------------------------------- | +| Name | string | The name of the Task | +| Script | scriptblock | The scriptblock of the Task | +| Arguments | hashtable | The arguments supplied from ArgumentList | diff --git a/examples/Web-PagesKestrel.ps1 b/examples/Web-PagesKestrel.ps1 index 78082a66f..e3402f6c9 100644 --- a/examples/Web-PagesKestrel.ps1 +++ b/examples/Web-PagesKestrel.ps1 @@ -60,13 +60,13 @@ Start-PodeServer -Threads 2 -ListenerType Kestrel { Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http -Name '8081Address' -RedirectTo '8090Address' # allow the local ip and some other ips - Add-PodeAccessRule -Access Allow -Type IP -Values @('127.0.0.1', '[::1]') - Add-PodeAccessRule -Access Allow -Type IP -Values @('192.169.0.1', '192.168.0.2') + # Add-PodeAccessRule -Access Allow -Type IP -Values @('127.0.0.1', '[::1]') + # Add-PodeAccessRule -Access Allow -Type IP -Values @('192.169.0.1', '192.168.0.2') # deny an ip - Add-PodeAccessRule -Access Deny -Type IP -Values 10.10.10.10 - Add-PodeAccessRule -Access Deny -Type IP -Values '10.10.0.0/24' - Add-PodeAccessRule -Access Deny -Type IP -Values all + # Add-PodeAccessRule -Access Deny -Type IP -Values 10.10.10.10 + # Add-PodeAccessRule -Access Deny -Type IP -Values '10.10.0.0/24' + # Add-PodeAccessRule -Access Deny -Type IP -Values all # log requests to the terminal New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging diff --git a/pode.build.ps1 b/pode.build.ps1 index 4cd21aee6..7e972d452 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -353,7 +353,7 @@ Task IndexSamples { # List of directories to exclude $sampleMarkDownPath = './docs/Getting-Started/Samples.md' $excludeDirs = @('scripts', 'views', 'static', 'public', 'assets', 'timers', 'modules', - 'Authentication', 'certs', 'logs', 'relative', 'routes') + 'Authentication', 'certs', 'logs', 'relative', 'routes', 'issues') # Convert exlusion list into single regex pattern for directory matching $dirSeparator = [IO.Path]::DirectorySeparatorChar diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index b6cd8b697..8037a5947 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -285,6 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'مدة Access-Control-Max-Age غير صالحة المقدمة: {0}. يجب أن تكون أكبر من 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'تعريف OpenAPI باسم {0} موجود بالفعل.' renamePodeOADefinitionTagExceptionMessage = "لا يمكن استخدام Rename-PodeOADefinitionTag داخل Select-PodeOADefinition 'ScriptBlock'." + taskProcessDoesNotExistExceptionMessage = 'عملية المهمة غير موجودة: {0}' + scheduleProcessDoesNotExistExceptionMessage = 'عملية الجدول الزمني غير موجودة: {0}' definitionTagChangeNotAllowedExceptionMessage = 'لا يمكن تغيير علامة التعريف لمسار.' getRequestBodyNotAllowedExceptionMessage = 'لا يمكن أن تحتوي عمليات {0} على محتوى الطلب.' } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index fb7b0c6ad..8fe33f3f0 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -285,6 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Ungültige Access-Control-Max-Age-Dauer angegeben: {0}. Sollte größer als 0 sein.' openApiDefinitionAlreadyExistsExceptionMessage = 'Die OpenAPI-Definition mit dem Namen {0} existiert bereits.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag kann nicht innerhalb eines 'ScriptBlock' von Select-PodeOADefinition verwendet werden." + taskProcessDoesNotExistExceptionMessage = "Der Aufgabenprozess '{0}' existiert nicht." + scheduleProcessDoesNotExistExceptionMessage = "Der Aufgabenplanerprozess '{0}' existiert nicht." definitionTagChangeNotAllowedExceptionMessage = 'Definitionstag für eine Route kann nicht geändert werden.' getRequestBodyNotAllowedExceptionMessage = '{0}-Operationen können keinen Anforderungstext haben.' } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index 53aba0d1c..aa26c31ee 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -285,6 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Invalid Access-Control-Max-Age duration supplied: {0}. Should be greater than 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI definition named {0} already exists.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag cannot be used inside a Select-PodeOADefinition 'ScriptBlock'." + taskProcessDoesNotExistExceptionMessage = 'Task process does not exist: {0}' + scheduleProcessDoesNotExistExceptionMessage = 'Schedule process does not exist: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Definition Tag for a Route cannot be changed.' getRequestBodyNotAllowedExceptionMessage = '{0} operations cannot have a Request Body.' } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index 8f97eead4..084d582f0 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -285,7 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Invalid Access-Control-Max-Age duration supplied: {0}. Should be greater than 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI definition named {0} already exists.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag cannot be used inside a Select-PodeOADefinition 'ScriptBlock'." + taskProcessDoesNotExistExceptionMessage = 'Task process does not exist: {0}' + scheduleProcessDoesNotExistExceptionMessage = 'Schedule process does not exist: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Definition Tag for a Route cannot be changed.' getRequestBodyNotAllowedExceptionMessage = '{0} operations cannot have a Request Body.' -} - +} \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index 9ca60ee62..3ffa01b37 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -285,6 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Duración inválida para Access-Control-Max-Age proporcionada: {0}. Debe ser mayor que 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'La definición de OpenAPI con el nombre {0} ya existe.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag no se puede usar dentro de un 'ScriptBlock' de Select-PodeOADefinition." + taskProcessDoesNotExistExceptionMessage = "El proceso de la tarea '{0}' no existe." + scheduleProcessDoesNotExistExceptionMessage = "El proceso del programación '{0}' no existe." definitionTagChangeNotAllowedExceptionMessage = 'La etiqueta de definición para una Route no se puede cambiar.' getRequestBodyNotAllowedExceptionMessage = 'Las operaciones {0} no pueden tener un cuerpo de solicitud.' } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index 8b9d047d3..6c058f1c8 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -285,6 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Durée Access-Control-Max-Age invalide fournie : {0}. Doit être supérieure à 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'La définition OpenAPI nommée {0} existe déjà.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag ne peut pas être utilisé à l'intérieur d'un 'ScriptBlock' de Select-PodeOADefinition." + taskProcessDoesNotExistExceptionMessage = "Le processus de la tâche '{0}' n'existe pas." + scheduleProcessDoesNotExistExceptionMessage = "Le processus de l'horaire '{0}' n'existe pas." definitionTagChangeNotAllowedExceptionMessage = 'Le tag de définition pour une Route ne peut pas être modifié.' getRequestBodyNotAllowedExceptionMessage = 'Les opérations {0} ne peuvent pas avoir de corps de requête.' } diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index ff9ccfc73..80f855556 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -285,6 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Durata non valida fornita per Access-Control-Max-Age: {0}. Deve essere maggiore di 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'La definizione OpenAPI denominata {0} esiste già.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag non può essere utilizzato all'interno di un 'ScriptBlock' di Select-PodeOADefinition." + taskProcessDoesNotExistExceptionMessage = "Il processo dell'attività '{0}' non esiste." + scheduleProcessDoesNotExistExceptionMessage = "Il processo della programma '{0}' non esiste." definitionTagChangeNotAllowedExceptionMessage = 'Il tag di definizione per una Route non può essere cambiato.' getRequestBodyNotAllowedExceptionMessage = 'Le operazioni {0} non possono avere un corpo della richiesta.' } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index ffc193a46..eff3aa371 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -285,7 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = '無効な Access-Control-Max-Age 期間が提供されました:{0}。0 より大きくする必要があります。' openApiDefinitionAlreadyExistsExceptionMessage = '名前が {0} の OpenAPI 定義は既に存在します。' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag は Select-PodeOADefinition 'ScriptBlock' 内で使用できません。" + taskProcessDoesNotExistExceptionMessage = 'タスクプロセスが存在しません: {0}' + scheduleProcessDoesNotExistExceptionMessage = 'スケジュールプロセスが存在しません: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Routeの定義タグは変更できません。' getRequestBodyNotAllowedExceptionMessage = '{0}操作にはリクエストボディを含めることはできません。' -} - +} \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index 25e3c3d10..247dd7ae1 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -285,6 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = '잘못된 Access-Control-Max-Age 기간이 제공되었습니다: {0}. 0보다 커야 합니다.' openApiDefinitionAlreadyExistsExceptionMessage = '이름이 {0}인 OpenAPI 정의가 이미 존재합니다.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag은 Select-PodeOADefinition 'ScriptBlock' 내에서 사용할 수 없습니다." + taskProcessDoesNotExistExceptionMessage = '작업 프로세스가 존재하지 않습니다: {0}' + scheduleProcessDoesNotExistExceptionMessage = '스케줄 프로세스가 존재하지 않습니다: {0}' definitionTagChangeNotAllowedExceptionMessage = 'Route에 대한 정의 태그는 변경할 수 없습니다.' getRequestBodyNotAllowedExceptionMessage = '{0} 작업에는 요청 본문이 있을 수 없습니다.' -} +} \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index 3f20960a0..916b37ce4 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -285,7 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Ongeldige Access-Control-Max-Age duur opgegeven: {0}. Moet groter zijn dan 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'OpenAPI-definitie met de naam {0} bestaat al.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag kan niet worden gebruikt binnen een Select-PodeOADefinition 'ScriptBlock'." + taskProcessDoesNotExistExceptionMessage = "Taakproces '{0}' bestaat niet." + scheduleProcessDoesNotExistExceptionMessage = "Schema-proces '{0}' bestaat niet." definitionTagChangeNotAllowedExceptionMessage = 'Definitietag voor een route kan niet worden gewijzigd.' getRequestBodyNotAllowedExceptionMessage = '{0}-operaties kunnen geen Request Body hebben.' -} - +} \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index 6c4f2da03..1e85dbd3b 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -285,7 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Podano nieprawidłowy czas trwania Access-Control-Max-Age: {0}. Powinien być większy niż 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'Definicja OpenAPI o nazwie {0} już istnieje.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag nie może być używany wewnątrz 'ScriptBlock' Select-PodeOADefinition." + taskProcessDoesNotExistExceptionMessage = "Proces zadania '{0}' nie istnieje." + scheduleProcessDoesNotExistExceptionMessage = "Proces harmonogramu '{0}' nie istnieje." definitionTagChangeNotAllowedExceptionMessage = 'Tag definicji dla Route nie może zostać zmieniony.' getRequestBodyNotAllowedExceptionMessage = 'Operacje {0} nie mogą mieć treści żądania.' -} - +} \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index df0073012..8e4bf0b9e 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -285,6 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = 'Duração inválida fornecida para Access-Control-Max-Age: {0}. Deve ser maior que 0.' openApiDefinitionAlreadyExistsExceptionMessage = 'A definição OpenAPI com o nome {0} já existe.' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag não pode ser usado dentro de um 'ScriptBlock' Select-PodeOADefinition." + taskProcessDoesNotExistExceptionMessage = "O processo da tarefa '{0}' não existe." + scheduleProcessDoesNotExistExceptionMessage = "O processo do cronograma '{0}' não existe." definitionTagChangeNotAllowedExceptionMessage = 'A Tag de definição para uma Route não pode ser alterada.' getRequestBodyNotAllowedExceptionMessage = 'As operações {0} não podem ter um corpo de solicitação.' } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index daa9ad110..5604f41f9 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -285,6 +285,8 @@ invalidAccessControlMaxAgeDurationExceptionMessage = '提供的 Access-Control-Max-Age 时长无效:{0}。应大于 0。' openApiDefinitionAlreadyExistsExceptionMessage = '名为 {0} 的 OpenAPI 定义已存在。' renamePodeOADefinitionTagExceptionMessage = "Rename-PodeOADefinitionTag 不能在 Select-PodeOADefinition 'ScriptBlock' 内使用。" + taskProcessDoesNotExistExceptionMessage = "任务进程 '{0}' 不存在。" + scheduleProcessDoesNotExistExceptionMessage = "计划进程 '{0}' 不存在。" definitionTagChangeNotAllowedExceptionMessage = 'Route的定义标签无法更改。' getRequestBodyNotAllowedExceptionMessage = '{0} 操作不能包含请求体。' } \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index eae7d8c44..2a6d08293 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -141,6 +141,7 @@ 'ConvertFrom-PodeXml', 'Set-PodeDefaultFolder', 'Get-PodeDefaultFolder', + 'Invoke-PodeGC', # routes 'Add-PodeRoute', @@ -184,6 +185,7 @@ 'Use-PodeSchedules', 'Test-PodeSchedule', 'Clear-PodeSchedules', + 'Get-PodeScheduleProcess', # timers 'Add-PodeTimer', @@ -207,6 +209,7 @@ 'Close-PodeTask', 'Test-PodeTaskCompleted', 'Wait-PodeTask', + 'Get-PodeTaskProcess', # middleware 'Add-PodeMiddleware', diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 3e3395551..28584fa42 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -116,9 +116,9 @@ function New-PodeContext { } $ctx.Tasks = @{ - Enabled = ($EnablePool -icontains 'tasks') - Items = @{} - Results = @{} + Enabled = ($EnablePool -icontains 'tasks') + Items = @{} + Processes = @{} } $ctx.Fim = @{ @@ -142,6 +142,7 @@ function New-PodeContext { Files = 1 Tasks = 2 WebSockets = 2 + Timers = 1 } # set socket details for pode server @@ -432,6 +433,7 @@ function New-PodeContext { Gui = $null Tasks = $null Files = $null + Timers = $null } # threading locks, etc. @@ -524,16 +526,11 @@ function New-PodeRunspacePool { # setup main runspace pool $threadsCounts = @{ Default = 3 - Timer = 1 Log = 1 Schedule = 1 Misc = 1 } - if (!(Test-PodeTimersExist)) { - $threadsCounts.Timer = 0 - } - if (!(Test-PodeSchedulesExist)) { $threadsCounts.Schedule = 0 } @@ -591,6 +588,14 @@ function New-PodeRunspacePool { New-PodeWebSocketReceiver } + # setup timer runspace pool -if we have any timers + if (Test-PodeTimersExist) { + $PodeContext.RunspacePools.Timers = @{ + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Timers, $PodeContext.RunspaceState, $Host) + State = 'Waiting' + } + } + # setup schedule runspace pool -if we have any schedules if (Test-PodeSchedulesExist) { $PodeContext.RunspacePools.Schedules = @{ diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index d9d09ded0..92eaa286a 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -554,7 +554,7 @@ function Get-PodeSubnetRange { function Add-PodeRunspace { param( [Parameter(Mandatory = $true)] - [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files')] + [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files', 'Timers')] [string] $Type, @@ -747,9 +747,9 @@ function Close-PodeRunspace { } # dispose of task runspaces - if ($PodeContext.Tasks.Results.Count -gt 0) { - foreach ($key in $PodeContext.Tasks.Results.Keys.Clone()) { - Close-PodeTaskInternal -Result $PodeContext.Tasks.Results[$key] + if ($PodeContext.Tasks.Processes.Count -gt 0) { + foreach ($key in $PodeContext.Tasks.Processes.Keys.Clone()) { + Close-PodeTaskInternal -Process $PodeContext.Tasks.Processes[$key] } } @@ -774,7 +774,7 @@ function Close-PodeRunspace { } # garbage collect - [GC]::Collect() + Invoke-PodeGC } catch { $_ | Write-PodeErrorLog @@ -1689,7 +1689,7 @@ function ConvertTo-PodeResponseContent { } } - { $_ -match '^(.*\/)?(.*\+)?yaml$' } { + { $_ -match '^(.*\/)?(.*\+)?yaml$' } { if ($InputObject -isnot [string]) { if ($Depth -le 0) { return (ConvertTo-PodeYamlInternal -InputObject $InputObject ) diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index 21ad5b56c..f82904db9 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -375,66 +375,80 @@ function Start-PodeLoggingRunspace { } $script = { - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - # if there are no logs to process, just sleep for a few seconds - but after checking the batch - if ($PodeContext.LogsToProcess.Count -eq 0) { - Test-PodeLoggerBatch - Start-Sleep -Seconds 5 - continue - } + try { + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + try { + # if there are no logs to process, just sleep for a few seconds - but after checking the batch + if ($PodeContext.LogsToProcess.Count -eq 0) { + Test-PodeLoggerBatch + Start-Sleep -Seconds 5 + continue + } - # safely pop off the first log from the array - $log = (Lock-PodeObject -Return -Object $PodeContext.LogsToProcess -ScriptBlock { - $log = $PodeContext.LogsToProcess[0] - $null = $PodeContext.LogsToProcess.RemoveAt(0) - return $log - }) - - # run the log item through the appropriate method - $logger = Get-PodeLogger -Name $log.Name - $now = [datetime]::Now - - # if the log is null, check batch then sleep and skip - if ($null -eq $log) { - Start-Sleep -Milliseconds 100 - continue - } + # safely pop off the first log from the array + $log = (Lock-PodeObject -Return -Object $PodeContext.LogsToProcess -ScriptBlock { + $log = $PodeContext.LogsToProcess[0] + $null = $PodeContext.LogsToProcess.RemoveAt(0) + return $log + }) + + # run the log item through the appropriate method + $logger = Get-PodeLogger -Name $log.Name + $now = [datetime]::Now + + # if the log is null, check batch then sleep and skip + if ($null -eq $log) { + Start-Sleep -Milliseconds 100 + continue + } - # convert to log item into a writable format - $rawItems = $log.Item - $_args = @($log.Item) + @($logger.Arguments) - $result = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments $_args -UsingVariables $logger.UsingVariables -Return -Splat) - - # check batching - $batch = $logger.Method.Batch - if ($batch.Size -gt 1) { - # add current item to batch - $batch.Items += $result - $batch.RawItems += $log.Item - $batch.LastUpdate = $now - - # if the current amount of items matches the batch, write - $result = $null - if ($batch.Items.Length -ge $batch.Size) { - $result = $batch.Items - $rawItems = $batch.RawItems - } + # convert to log item into a writable format + $rawItems = $log.Item + $_args = @($log.Item) + @($logger.Arguments) + $result = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments $_args -UsingVariables $logger.UsingVariables -Return -Splat) + + # check batching + $batch = $logger.Method.Batch + if ($batch.Size -gt 1) { + # add current item to batch + $batch.Items += $result + $batch.RawItems += $log.Item + $batch.LastUpdate = $now + + # if the current amount of items matches the batch, write + $result = $null + if ($batch.Items.Length -ge $batch.Size) { + $result = $batch.Items + $rawItems = $batch.RawItems + } + + # if we're writing, reset the items + if ($null -ne $result) { + $batch.Items = @() + $batch.RawItems = @() + } + } - # if we're writing, reset the items - if ($null -ne $result) { - $batch.Items = @() - $batch.RawItems = @() - } - } + # send the writable log item off to the log writer + if ($null -ne $result) { + $_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems) + $null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat + } - # send the writable log item off to the log writer - if ($null -ne $result) { - $_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems) - $null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat + # small sleep to lower cpu usage + Start-Sleep -Milliseconds 100 + } + catch { + $_ | Write-PodeErrorLog + } } - - # small sleep to lower cpu usage - Start-Sleep -Milliseconds 100 + } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + throw $_.Exception } } diff --git a/src/Private/Schedules.ps1 b/src/Private/Schedules.ps1 index 79b365b92..dd031017d 100644 --- a/src/Private/Schedules.ps1 +++ b/src/Private/Schedules.ps1 @@ -18,53 +18,43 @@ function Start-PodeScheduleRunspace { return } - Add-PodeSchedule -Name '__pode_schedule_housekeeper__' -Cron '@minutely' -ScriptBlock { - if ($PodeContext.Schedules.Processes.Count -eq 0) { - return - } + Add-PodeTimer -Name '__pode_schedule_housekeeper__' -Interval 30 -ScriptBlock { + try { + if ($PodeContext.Schedules.Processes.Count -eq 0) { + return + } - foreach ($key in $PodeContext.Schedules.Processes.Keys.Clone()) { - $process = $PodeContext.Schedules.Processes[$key] + $now = [datetime]::UtcNow - # is it completed? - if (!$process.Runspace.Handler.IsCompleted) { - continue + 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 + } } - # dispose and remove the schedule process - Close-PodeScheduleInternal -Process $process + $process = $null + } + catch { + $_ | Write-PodeErrorLog } - - $process = $null } $script = { - # 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 { + # select the schedules that trigger on-start $_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) + $_.OnStart } | ForEach-Object { Invoke-PodeInternalSchedule -Schedule $_ } @@ -72,8 +62,46 @@ function Start-PodeScheduleRunspace { # complete any schedules Complete-PodeInternalSchedule -Now $_now - # cron expression only goes down to the minute, so sleep for 1min + # 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 } } @@ -128,17 +156,11 @@ function Complete-PodeInternalSchedule { $Now ) - # add any schedules to remove that have exceeded their end time - $Schedules = @($PodeContext.Schedules.Items.Values | - Where-Object { (($null -ne $_.EndTime) -and ($_.EndTime -lt $Now)) }) - - if (($null -eq $Schedules) -or ($Schedules.Length -eq 0)) { - return - } - # set any expired schedules as being completed - $Schedules | ForEach-Object { - $_.Completed = $true + foreach ($schedule in $PodeContext.Schedules.Items.Values) { + if (($null -ne $schedule.EndTime) -and ($schedule.EndTime -lt $Now)) { + $schedule.Completed = $true + } } } @@ -182,6 +204,7 @@ function Invoke-PodeInternalSchedule { function Invoke-PodeInternalScheduleLogic { param( [Parameter(Mandatory = $true)] + [hashtable] $Schedule, [Parameter()] @@ -190,44 +213,120 @@ function Invoke-PodeInternalScheduleLogic { ) try { + # generate processId for schedule + $processId = New-PodeGuid + # setup event param $parameters = @{ - Event = @{ - Lockable = $PodeContext.Threading.Lockables.Global - Sender = $Schedule - Metadata = @{} - } + ProcessId = $processId + ArgumentList = $ArgumentList } - # add any schedule args - foreach ($key in $Schedule.Arguments.Keys) { - $parameters[$key] = $Schedule.Arguments[$key] - } + # what is the expire time if using "create" timeout? + $expireTime = [datetime]::MaxValue + $createTime = [datetime]::UtcNow - # add adhoc schedule invoke args - if (($null -ne $ArgumentList) -and ($ArgumentList.Count -gt 0)) { - foreach ($key in $ArgumentList.Keys) { - $parameters[$key] = $ArgumentList[$key] - } + if (($Schedule.Timeout.From -ieq 'Create') -and ($Schedule.Timeout.Value -ge 0)) { + $expireTime = $createTime.AddSeconds($Schedule.Timeout.Value) } - # add any using variables - if ($null -ne $Schedule.UsingVariables) { - foreach ($usingVar in $Schedule.UsingVariables) { - $parameters[$usingVar.NewName] = $usingVar.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' } - $name = New-PodeGuid - $runspace = Add-PodeRunspace -Type Schedules -ScriptBlock (($Schedule.Script).GetNewClosure()) -Parameters $parameters -PassThru + # start the schedule runspace + $scriptblock = Get-PodeScheduleScriptBlock + $runspace = Add-PodeRunspace -Type Schedules -ScriptBlock $scriptblock -Parameters $parameters -PassThru - $PodeContext.Schedules.Processes[$name] = @{ - ID = $name - Schedule = $Schedule.Name - Runspace = $runspace - } + # 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 + } + } } \ No newline at end of file diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 82a971288..63b63f8a9 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -66,12 +66,12 @@ function Start-PodeInternalServer { # start runspace for loggers Start-PodeLoggingRunspace - # start runspace for timers - Start-PodeTimerRunspace - # start runspace for schedules Start-PodeScheduleRunspace + # start runspace for timers + Start-PodeTimerRunspace + # start runspace for gui Start-PodeGuiRunspace @@ -254,7 +254,7 @@ function Restart-PodeInternalServer { # clear tasks $PodeContext.Tasks.Items.Clear() - $PodeContext.Tasks.Results.Clear() + $PodeContext.Tasks.Processes.Clear() # clear file watchers $PodeContext.Fim.Items.Clear() diff --git a/src/Private/Tasks.ps1 b/src/Private/Tasks.ps1 index 1a4c1f7d2..db596544e 100644 --- a/src/Private/Tasks.ps1 +++ b/src/Private/Tasks.ps1 @@ -8,39 +8,38 @@ function Start-PodeTaskHousekeeper { } Add-PodeTimer -Name '__pode_task_housekeeper__' -Interval 30 -ScriptBlock { - if ($PodeContext.Tasks.Results.Count -eq 0) { - return - } - - $now = [datetime]::UtcNow - - foreach ($key in $PodeContext.Tasks.Results.Keys.Clone()) { - $result = $PodeContext.Tasks.Results[$key] - - # has it force expired? - if ($result.ExpireTime -lt $now) { - Close-PodeTaskInternal -Result $result - continue + try { + if ($PodeContext.Tasks.Processes.Count -eq 0) { + return } - # is it completed? - if (!$result.Runspace.Handler.IsCompleted) { - continue + $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 + } } - # is a completed time set? - if ($null -eq $result.CompletedTime) { - $result.CompletedTime = [datetime]::UtcNow - continue - } - - # is it expired by completion? if so, dispose and remove - if ($result.CompletedTime.AddMinutes(1) -lt $now) { - Close-PodeTaskInternal -Result $result - } + $process = $null + } + catch { + $_ | Write-PodeErrorLog } - - $result = $null } } @@ -48,21 +47,22 @@ function Close-PodeTaskInternal { param( [Parameter()] [hashtable] - $Result + $Process ) - if ($null -eq $Result) { + if ($null -eq $Process) { return } - Close-PodeDisposable -Disposable $Result.Runspace.Pipeline - Close-PodeDisposable -Disposable $Result.Result - $null = $PodeContext.Tasks.Results.Remove($Result.ID) + 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()] @@ -71,66 +71,151 @@ function Invoke-PodeInternalTask { [Parameter()] [int] - $Timeout = -1 + $Timeout = -1, + + [Parameter()] + [ValidateSet('Default', 'Create', 'Start')] + [string] + $TimeoutFrom = 'Default' ) try { + # generate processId for task + $processId = New-PodeGuid + # setup event param $parameters = @{ - Event = @{ - Lockable = $PodeContext.Threading.Lockables.Global - Sender = $Task - Metadata = @{} - } + ProcessId = $processId + ArgumentList = $ArgumentList } - # add any task args - foreach ($key in $Task.Arguments.Keys) { - $parameters[$key] = $Task.Arguments[$key] + # what's the timeout values to use? + if ($TimeoutFrom -eq 'Default') { + $TimeoutFrom = $Task.Timeout.From } - # add adhoc task invoke args - if (($null -ne $ArgumentList) -and ($ArgumentList.Count -gt 0)) { - foreach ($key in $ArgumentList.Keys) { - $parameters[$key] = $ArgumentList[$key] - } + if ($Timeout -eq -1) { + $Timeout = $Task.Timeout.Value } - # add any using variables - if ($null -ne $Task.UsingVariables) { - foreach ($usingVar in $Task.UsingVariables) { - $parameters[$usingVar.NewName] = $usingVar.Value - } - } - - $name = New-PodeGuid - $result = [System.Management.Automation.PSDataCollection[psobject]]::new() - $runspace = Add-PodeRunspace -Type Tasks -ScriptBlock (($Task.Script).GetNewClosure()) -Parameters $parameters -OutputStream $result -PassThru + # what is the expire time if using "create" timeout? + $expireTime = [datetime]::MaxValue + $createTime = [datetime]::UtcNow - if ($Timeout -ge 0) { - $expireTime = [datetime]::UtcNow.AddSeconds($Timeout) - } - else { - $expireTime = [datetime]::MaxValue + if (($TimeoutFrom -ieq 'Create') -and ($Timeout -ge 0)) { + $expireTime = $createTime.AddSeconds($Timeout) } - $PodeContext.Tasks.Results[$name] = @{ - ID = $name + # add task process + $result = [System.Management.Automation.PSDataCollection[psobject]]::new() + $PodeContext.Tasks.Processes[$processId] = @{ + ID = $processId Task = $Task.Name - Runspace = $runspace + Runspace = $null Result = $result + CreateTime = $createTime + StartTime = $null CompletedTime = $null ExpireTime = $expireTime - Timeout = $Timeout + Timeout = @{ + Value = $Timeout + From = $TimeoutFrom + } + State = 'Pending' } - return $PodeContext.Tasks.Results[$name] + # start the task runspace + $scriptblock = Get-PodeTaskScriptBlock + $runspace = Add-PodeRunspace -Type Tasks -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])] @@ -168,7 +253,8 @@ function Wait-PodeNetTaskInternal { # if the main task isnt complete, it timed out if (($null -ne $timeoutTask) -and (!$Task.IsCompleted)) { - throw [System.TimeoutException]::new($PodeLocale.taskTimedOutExceptionMessage -f $Timeout) #"Task has timed out after $($Timeout)ms") + # "Task has timed out after $($Timeout)ms") + throw [System.TimeoutException]::new($PodeLocale.taskTimedOutExceptionMessage -f $Timeout) } # only return a value if the result has one diff --git a/src/Private/Timers.ps1 b/src/Private/Timers.ps1 index 3dc1a9db3..31fb32412 100644 --- a/src/Private/Timers.ps1 +++ b/src/Private/Timers.ps1 @@ -19,50 +19,70 @@ function Start-PodeTimerRunspace { } $script = { - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - $_now = [DateTime]::Now - - # only run timers that haven't completed, and have a next trigger in the past - $PodeContext.Timers.Items.Values | Where-Object { - !$_.Completed -and ($_.OnStart -or ($_.NextTriggerTime -le $_now)) - } | ForEach-Object { - $_.OnStart = $false - $_.Count++ - - # set last trigger to current next trigger - if ($null -ne $_.NextTriggerTime) { - $_.LastTriggerTime = $_.NextTriggerTime + try { + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + try { + $_now = [DateTime]::Now + + # only run timers that haven't completed, and have a next trigger in the past + foreach ($timer in $PodeContext.Timers.Items.Values) { + if ($timer.Completed -or (!$timer.OnStart -and ($timer.NextTriggerTime -gt $_now))) { + continue + } + + try { + $timer.OnStart = $false + $timer.Count++ + + # set last trigger to current next trigger + if ($null -ne $timer.NextTriggerTime) { + $timer.LastTriggerTime = $timer.NextTriggerTime + } + else { + $timer.LastTriggerTime = $_now + } + + # has the timer completed? + if (($timer.Limit -gt 0) -and ($timer.Count -ge $timer.Limit)) { + $timer.Completed = $true + } + + # next trigger + if (!$timer.Completed) { + $timer.NextTriggerTime = $_now.AddSeconds($timer.Interval) + } + else { + $timer.NextTriggerTime = $null + } + + # run the timer + Invoke-PodeInternalTimer -Timer $timer + } + catch { + $_ | Write-PodeErrorLog + } + } + + Start-Sleep -Seconds 1 } - else { - $_.LastTriggerTime = [datetime]::Now + catch { + $_ | Write-PodeErrorLog } - - # has the timer completed? - if (($_.Limit -gt 0) -and ($_.Count -ge $_.Limit)) { - $_.Completed = $true - } - - # next trigger - if (!$_.Completed) { - $_.NextTriggerTime = $_now.AddSeconds($_.Interval) - } - else { - $_.NextTriggerTime = $null - } - - # run the timer - Invoke-PodeInternalTimer -Timer $_ } - - Start-Sleep -Seconds 1 + } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + throw $_.Exception } } - Add-PodeRunspace -Type Main -ScriptBlock $script + Add-PodeRunspace -Type Timers -ScriptBlock $script } function Invoke-PodeInternalTimer { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] param( [Parameter(Mandatory = $true)] @@ -74,10 +94,11 @@ function Invoke-PodeInternalTimer { ) try { - $global:TimerEvent = @{ - Lockable = $PodeContext.Threading.Lockables.Global - Sender = $Timer - Metadata = @{} + $TimerEvent = @{ + Lockable = $PodeContext.Threading.Lockables.Global + Sender = $Timer + Timestamp = [DateTime]::UtcNow + Metadata = @{} } # add main timer args @@ -92,9 +113,12 @@ function Invoke-PodeInternalTimer { } # invoke timer - $null = Invoke-PodeScriptBlock -ScriptBlock $Timer.Script -Arguments $_args -UsingVariables $Timer.UsingVariables -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $Timer.Script.GetNewClosure() -Arguments $_args -UsingVariables $Timer.UsingVariables -Scoped -Splat -NoNewClosure } catch { $_ | Write-PodeErrorLog } + finally { + Invoke-PodeGC + } } \ No newline at end of file diff --git a/src/Public/Schedules.ps1 b/src/Public/Schedules.ps1 index 2b74cb538..8fb460d33 100644 --- a/src/Public/Schedules.ps1 +++ b/src/Public/Schedules.ps1 @@ -26,6 +26,12 @@ 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. @@ -79,6 +85,15 @@ function Add-PodeSchedule { [hashtable] $ArgumentList, + [Parameter()] + [int] + $Timeout = -1, + + [Parameter()] + [ValidateSet('Create', 'Start')] + [string] + $TimeoutFrom = 'Create', + [switch] $OnStart ) @@ -137,6 +152,10 @@ function Add-PodeSchedule { Arguments = (Protect-PodeValue -Value $ArgumentList -Default @{}) OnStart = $OnStart Completed = ($null -eq $nextTrigger) + Timeout = @{ + Value = $Timeout + From = $TimeoutFrom + } } } @@ -545,4 +564,92 @@ function Use-PodeSchedules { ) 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 } \ No newline at end of file diff --git a/src/Public/Tasks.ps1 b/src/Public/Tasks.ps1 index 0f46cbded..02e45bce3 100644 --- a/src/Public/Tasks.ps1 +++ b/src/Public/Tasks.ps1 @@ -17,6 +17,12 @@ A literal, or relative, path to a file containing a ScriptBlock for the Task's l .PARAMETER ArgumentList A hashtable of arguments to supply to the Task's ScriptBlock. +.PARAMETER Timeout +A Timeout, in seconds, to abort running the Task process. (Default: -1 [never timeout]) + +.PARAMETER TimeoutFrom +Where to start the Timeout from, either 'Create', 'Start'. (Default: 'Create') + .EXAMPLE Add-PodeTask -Name 'Example1' -ScriptBlock { Invoke-SomeLogic } @@ -40,7 +46,16 @@ function Add-PodeTask { [Parameter()] [hashtable] - $ArgumentList + $ArgumentList, + + [Parameter()] + [int] + $Timeout = -1, + + [Parameter()] + [ValidateSet('Create', 'Start')] + [string] + $TimeoutFrom = 'Create' ) # ensure the task doesn't already exist if ($PodeContext.Tasks.Items.ContainsKey($Name)) { @@ -63,6 +78,10 @@ function Add-PodeTask { Script = $ScriptBlock UsingVariables = $usingVars Arguments = (Protect-PodeValue -Value $ArgumentList -Default @{}) + Timeout = @{ + Value = $Timeout + From = $TimeoutFrom + } } } @@ -118,6 +137,7 @@ Invoke a Task. .DESCRIPTION Invoke a Task either asynchronously or synchronously, with support for returning values. +The function returns the Task process onbject which was triggered. .PARAMETER Name The Name of the Task. @@ -126,10 +146,16 @@ The Name of the Task. A hashtable of arguments to supply to the Task's ScriptBlock. .PARAMETER Timeout -A Timeout, in seconds, to abort running the task. (Default: -1 [never timeout]) +A Timeout, in seconds, to abort running the Task process. (Default: -1 [never timeout]) + +.PARAMETER TimeoutFrom +Where to start the Timeout from, either 'Default', 'Create', or 'Start'. (Default: 'Default' - will use the value from Add-PodeTask) .PARAMETER Wait -If supplied, Pode will wait until the Task has finished executing, and then return any values. +If supplied, Pode will wait until the Task process has finished executing, and then return any values. + +.OUTPUTS +The triggered Task process. .EXAMPLE Invoke-PodeTask -Name 'Example1' -Wait -Timeout 5 @@ -155,6 +181,11 @@ function Invoke-PodeTask { [int] $Timeout = -1, + [Parameter()] + [ValidateSet('Default', 'Create', 'Start')] + [string] + $TimeoutFrom = 'Default', + [switch] $Wait ) @@ -166,7 +197,7 @@ function Invoke-PodeTask { } # run task logic - $task = Invoke-PodeInternalTask -Task $PodeContext.Tasks.Items[$Name] -ArgumentList $ArgumentList -Timeout $Timeout + $task = Invoke-PodeInternalTask -Task $PodeContext.Tasks.Items[$Name] -ArgumentList $ArgumentList -Timeout $Timeout -TimeoutFrom $TimeoutFrom # wait, and return result? if ($Wait) { @@ -365,18 +396,18 @@ function Close-PodeTask { $Task ) - Close-PodeTaskInternal -Result $Task + Close-PodeTaskInternal -Process $Task } <# .SYNOPSIS -Test if a running Task has completed. +Test if a running Task process has completed. .DESCRIPTION -Test if a running Task has completed. +Test if a running Task process has completed. .PARAMETER Task -The Task to be check. +The Task process to be check. The process returned by either Invoke-PodeTask or Get-PodeTaskProcess. .EXAMPLE Invoke-PodeTask -Name 'Example1' | Test-PodeTaskCompleted @@ -395,13 +426,13 @@ function Test-PodeTaskCompleted { <# .SYNOPSIS -Waits for a task to finish, and returns a result if there is one. +Waits for a Task process to finish, and returns a result if there is one. .DESCRIPTION -Waits for a task to finish, and returns a result if there is one. +Waits for a Task process to finish, and returns a result if there is one. .PARAMETER Task -The task to wait on. +The Task process to wait on. The process returned by either Invoke-PodeTask or Get-PodeTaskProcess. .PARAMETER Timeout An optional Timeout in milliseconds. @@ -434,4 +465,93 @@ function Wait-PodeTask { # Task type is invalid, expected either [System.Threading.Tasks.Task] or [hashtable] throw ($PodeLocale.invalidTaskTypeExceptionMessage) +} + + +<# +.SYNOPSIS +Get all Task Processes. + +.DESCRIPTION +Get all Task Processes, with support for filtering. These are the processes created when using Invoke-PodeTask. + +.PARAMETER Name +An optional Name of the Task to filter by, can be one or more. + +.PARAMETER Id +An optional ID of the Task process to filter by, can be one or more. + +.PARAMETER State +An optional State of the Task process to filter by, can be one or more. + +.EXAMPLE +Get-PodeTaskProcess + +.EXAMPLE +Get-PodeTaskProcess -Name 'TaskName' + +.EXAMPLE +Get-PodeTaskProcess -Id 'TaskId' + +.EXAMPLE +Get-PodeTaskProcess -State 'Running' +#> +function Get-PodeTaskProcess { + [CmdletBinding()] + param( + [Parameter()] + [string[]] + $Name, + + [Parameter()] + [string[]] + $Id, + + [Parameter()] + [ValidateSet('All', 'Pending', 'Running', 'Completed', 'Failed')] + [string[]] + $State = 'All' + ) + + $processes = $PodeContext.Tasks.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.Task -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 } \ No newline at end of file diff --git a/src/Public/Timers.ps1 b/src/Public/Timers.ps1 index 8042bca71..548f9db9b 100644 --- a/src/Public/Timers.ps1 +++ b/src/Public/Timers.ps1 @@ -259,7 +259,7 @@ function Edit-PodeTimer { # ensure the timer exists if (!$PodeContext.Timers.Items.ContainsKey($Name)) { - # Timer 'Name' does not exist + # Timer 'Name' does not exist throw ($PodeLocale.timerDoesNotExistExceptionMessage -f $Name) } diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index dde5ff1bb..d268f34b9 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1362,6 +1362,23 @@ function ConvertFrom-PodeXml { $oHash.$childname += (ConvertFrom-PodeXml $child) } } + return $oHash +} + +<# +.SYNOPSIS +Invokes the garbage collector. + +.DESCRIPTION +Invokes the garbage collector. + +.EXAMPLE +Invoke-PodeGC +#> +function Invoke-PodeGC { + [CmdletBinding()] + param() + [System.GC]::Collect() } \ No newline at end of file diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1 index e9d9cb76f..d1c4a6e43 100644 --- a/tests/unit/Helpers.Tests.ps1 +++ b/tests/unit/Helpers.Tests.ps1 @@ -2,7 +2,7 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param() BeforeAll { - Add-Type -AssemblyName "System.Net.Http" -ErrorAction SilentlyContinue + Add-Type -AssemblyName 'System.Net.Http' -ErrorAction SilentlyContinue $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } @@ -770,7 +770,7 @@ Describe 'Get-PodeEndpointInfo' { } It 'Throws an error for an invalid IP endpoint' { - { Get-PodeEndpointInfo -Address '700.0.0.a' } | Should -Throw -ExpectedMessage ($PodeLocale.failedToParseAddressExceptionMessage -f '700.0.0.a' ) #'*Failed to parse*' + { Get-PodeEndpointInfo -Address '700.0.0.a' } | Should -Throw -ExpectedMessage ($PodeLocale.failedToParseAddressExceptionMessage -f '700.0.0.a' ) #'*Failed to parse*' } It 'Throws an error for an out-of-range IP endpoint' { @@ -1669,8 +1669,18 @@ Describe 'New-PodeCron' { Describe 'ConvertTo-PodeYaml Tests' { BeforeAll { - $PodeContext = @{ Server = @{InternalCache = @{} } } + $PodeContext = @{ + Server = @{ + InternalCache = @{} + Web = @{ + OpenApi = @{ + UsePodeYamlInternal = $true + } + } + } + } } + Context 'When converting basic types' { It 'Converts strings correctly' { $result = 'hello world' | ConvertTo-PodeYaml diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 21a1a7514..222f7af1d 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -37,6 +37,7 @@ Describe 'Start-PodeInternalServer' { Mock Invoke-PodeEvent { } Mock Write-Verbose { } Mock Add-PodeScopedVariablesInbuilt { } + Mock Write-PodeHost { } } It 'Calls one-off script logic' { @@ -221,11 +222,11 @@ Describe 'Restart-PodeInternalServer' { Processes = @{} } Tasks = @{ - Enabled = $true - Items = @{ + Enabled = $true + Items = @{ key = 'value' } - Results = @{} + Processes = @{} } Fim = @{ Enabled = $true diff --git a/tests/unit/_.Tests.ps1 b/tests/unit/_.Tests.ps1 index 42b67b2f6..61e8aad23 100644 --- a/tests/unit/_.Tests.ps1 +++ b/tests/unit/_.Tests.ps1 @@ -7,7 +7,7 @@ BeforeDiscovery { # List of directories to exclude $excludeDirs = @('scripts', 'views', 'static', 'public', 'assets', 'timers', 'modules', - 'Authentication', 'certs', 'logs', 'relative', 'routes') + 'Authentication', 'certs', 'logs', 'relative', 'routes', 'issues') # Convert exlusion list into single regex pattern for directory matching $dirSeparator = [IO.Path]::DirectorySeparatorChar @@ -15,9 +15,9 @@ BeforeDiscovery { # get the example scripts $ps1Files = @(Get-ChildItem -Path $examplesPath -Filter *.ps1 -Recurse -File -Force | - Where-Object { - $_.FullName -inotmatch $excludeDirs - }).FullName + Where-Object { + $_.FullName -inotmatch $excludeDirs + }).FullName } BeforeAll {