From 65be24fd8b698485fe713ba3c9ebb9218477ac8a Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Fri, 23 Oct 2026 10:04:05 +0200 Subject: [PATCH] #220 - Record a process stopped on purpose by a StopTask with the stopped status (ProcessEvent::isProcessStopped(), when provided by the process bundle) Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + .../04-process_executions_and_logs.md | 4 +++- docs/reference/09-notifications.md | 2 +- phpstan.neon | 19 +++++++++++++++++++ src/Entity/Enum/ProcessExecutionStatus.php | 2 +- .../ProcessEventSubscriber.php | 13 ++++++++++++- src/Notifier/NotificationTrigger.php | 2 +- .../ProcessEventSubscriberTest.php | 17 +++++++++++++++++ 8 files changed, 55 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a874b49..a831150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Latest ------ ## Changes +* [#220](https://github.com/cleverage/ui-process-bundle/issues/220) A process stopped on purpose by a `StopTask` (e.g. not going on when the source file does not exist) is recorded with the `stopped` status, and notified as `stopped`, instead of `finish`. Requires the process bundle version providing `ProcessEvent::isProcessStopped()` (cleverage/process-bundle#337); with an older version, the execution keeps the `finish` status. Update documentation, add tests. * [#207](https://github.com/cleverage/ui-process-bundle/issues/207) The `main` firewall configured by the bundle (`security.configure_firewall`) limits the login attempts (Symfony `login_throttling`, default settings: 5 failed attempts per minute for a username and an IP address, 25 for an IP address); the settings can be changed in the application firewall. Require `symfony/rate-limiter`. Update documentation and tests. * [#186](https://github.com/cleverage/ui-process-bundle/issues/186) `ProcessExecuteController`, `ProcessExecuteHandler`, `LaunchType`, `ProcessConfigurationsManager` and `IsValidProcessCodeValidator` are typed on `ProcessManagerInterface` / `ProcessConfigurationRegistryInterface` (process-bundle 5.4) instead of the concrete classes, `@final since 5.4`: the process manager and the process configuration registry can be decorated or replaced for the UI. * [#209](https://github.com/cleverage/ui-process-bundle/issues/209) The files sent as input of a process (launch form, HTTP API) are deleted from the upload directory once the process has been executed, successfully or not (they were kept forever). Set the new `cleverage_ui_process.delete_uploaded_files` parameter to `false` to keep them. Update documentation, add tests. diff --git a/docs/reference/04-process_executions_and_logs.md b/docs/reference/04-process_executions_and_logs.md index bc0c25d..29816a7 100644 --- a/docs/reference/04-process_executions_and_logs.md +++ b/docs/reference/04-process_executions_and_logs.md @@ -11,7 +11,9 @@ Process executions An event subscriber listens to the process bundle events: - on process start, a `ProcessExecution` is created with the `started` status, the process code, the context and the name of the log file, -- on process end, its status becomes `finish` and its end date is set, +- on process end, its status becomes `finish` and its end date is set, or `stopped` when the process has been stopped + on purpose by a [StopTask](https://github.com/cleverage/process-bundle/blob/main/docs/reference/tasks/stop_task.md) + (e.g. not going on when the source file does not exist; requires the process bundle version following 5.4), - on process failure, its status becomes `failed` and its end date is set, or `stopped` when the process has been [stopped from the UI](#stopping-a-process-execution). diff --git a/docs/reference/09-notifications.md b/docs/reference/09-notifications.md index 02f42f2..e864525 100644 --- a/docs/reference/09-notifications.md +++ b/docs/reference/09-notifications.md @@ -87,7 +87,7 @@ Statuses | `failed` | Failed (status `failed`). | `high` | | `finish_with_report` | Finished (status `finish`) with log levels counted in its [report](04-process_executions_and_logs.md#report), e.g. `{"Warning": 3}`. The counted levels depend on `logs.report_increment_level` (default `Warning`). | `medium` | | `finish` | Finished without log levels counted in its report. | `low` | -| `stopped` | Stopped from the UI (status `stopped`, see [stopping a process execution](04-process_executions_and_logs.md#stopping-a-process-execution)). Not notified by default. | `low` | +| `stopped` | Stopped from the UI (status `stopped`, see [stopping a process execution](04-process_executions_and_logs.md#stopping-a-process-execution)) or on purpose by a `StopTask` of the process. Not notified by default. | `low` | The importance selects the channels of the notifier `channel_policy` when no `channels` are configured. diff --git a/phpstan.neon b/phpstan.neon index e9a9e7e..16f44c1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,3 +3,22 @@ parameters: paths: - src - tests + ignoreErrors: + # ProcessEvent::isProcessStopped() is only provided by the process bundle version following 5.4 (#220): remove + # these entries once this version is required + - + identifier: function.alreadyNarrowedType + paths: + - src/EventSubscriber/ProcessEventSubscriber.php + - tests/EventSubscriber/ProcessEventSubscriberTest.php + reportUnmatched: false + - + identifier: function.impossibleType + paths: + - src/EventSubscriber/ProcessEventSubscriber.php + - tests/EventSubscriber/ProcessEventSubscriberTest.php + reportUnmatched: false + - + identifier: arguments.count + path: tests/EventSubscriber/ProcessEventSubscriberTest.php + reportUnmatched: false diff --git a/src/Entity/Enum/ProcessExecutionStatus.php b/src/Entity/Enum/ProcessExecutionStatus.php index 226c4e2..d3e4ed0 100644 --- a/src/Entity/Enum/ProcessExecutionStatus.php +++ b/src/Entity/Enum/ProcessExecutionStatus.php @@ -21,7 +21,7 @@ enum ProcessExecutionStatus: string implements TranslatableInterface case Started = 'started'; case Finish = 'finish'; case Failed = 'failed'; - /** Stopped on a stop request (see ProcessExecution::requestStop()) */ + /** Stopped on a stop request (see ProcessExecution::requestStop()), or on purpose by a StopTask */ case Stopped = 'stopped'; public function trans(TranslatorInterface $translator, ?string $locale = null): string diff --git a/src/EventSubscriber/ProcessEventSubscriber.php b/src/EventSubscriber/ProcessEventSubscriber.php index 7133ce5..7fcec1c 100644 --- a/src/EventSubscriber/ProcessEventSubscriber.php +++ b/src/EventSubscriber/ProcessEventSubscriber.php @@ -56,7 +56,9 @@ public function onProcessStart(ProcessEvent $event): void public function success(ProcessEvent $event): void { if ($event->getProcessCode() === $this->processExecutionManager->getCurrentProcessExecution()?->getCode()) { - $this->processExecutionManager->getCurrentProcessExecution()->setStatus(ProcessExecutionStatus::Finish); + $this->processExecutionManager->getCurrentProcessExecution()->setStatus( + $this->isStoppedOnPurpose($event) ? ProcessExecutionStatus::Stopped : ProcessExecutionStatus::Finish + ); $this->processExecutionManager->getCurrentProcessExecution()->end(); $this->endProcessExecution($event); } @@ -88,6 +90,15 @@ private function endProcessExecution(ProcessEvent $event): void $this->dispatchEnded($event); } + /** + * The process has been stopped on purpose by a StopTask: it has ended normally, flagged as stopped + * (ProcessEvent::isProcessStopped(), since the process bundle version following 5.4). + */ + private function isStoppedOnPurpose(ProcessEvent $event): bool + { + return method_exists($event, 'isProcessStopped') && $event->isProcessStopped(); + } + /** * The process has been stopped on a stop request: a ProcessStoppedException in the chain of the previous * exceptions (wrapped in a ProcessFailedException, by each parent process for a sub-process). diff --git a/src/Notifier/NotificationTrigger.php b/src/Notifier/NotificationTrigger.php index 7d65449..0625e92 100644 --- a/src/Notifier/NotificationTrigger.php +++ b/src/Notifier/NotificationTrigger.php @@ -26,7 +26,7 @@ enum NotificationTrigger: string /** Finished with log levels counted in its report (see the "logs.report_increment_level" configuration) */ case FinishWithReport = 'finish_with_report'; case Finish = 'finish'; - /** Stopped on a stop request from the UI */ + /** Stopped on a stop request from the UI, or on purpose by a StopTask */ case Stopped = 'stopped'; public static function fromProcessExecution(ProcessExecution $processExecution): ?self diff --git a/tests/EventSubscriber/ProcessEventSubscriberTest.php b/tests/EventSubscriber/ProcessEventSubscriberTest.php index 8eb4e75..bcd1fe6 100644 --- a/tests/EventSubscriber/ProcessEventSubscriberTest.php +++ b/tests/EventSubscriber/ProcessEventSubscriberTest.php @@ -186,6 +186,23 @@ public static function provideStopErrors(): iterable ]; } + public function testProcessStoppedByAStopTask(): void + { + if (!method_exists(ProcessEvent::class, 'isProcessStopped')) { + self::markTestSkipped('ProcessEvent::isProcessStopped() requires a newer cleverage/process-bundle'); + } + $processExecution = new ProcessExecution('test.process', 'test.log'); + $processExecutionManager = new ProcessExecutionManager($this->createRepositoryStub()); + $processExecutionManager->setCurrentProcessExecution($processExecution); + + $this->createSubscriber(new ProcessHandler('/var/log/process', $processExecutionManager), $processExecutionManager) + ->success(new ProcessEvent('test.process', null, [], null, null, true)); + + self::assertSame(ProcessExecutionStatus::Stopped, $processExecution->status); + self::assertInstanceOf(\DateTimeImmutable::class, $processExecution->endDate); + self::assertNull($processExecutionManager->getCurrentProcessExecution()); + } + #[DataProvider('provideStopErrors')] public function testStoppedProcess(\Throwable $error): void {