├── LICENSE
├── Makefile
├── README.md
├── UPGRADE.md
├── composer.json
├── config
├── admin_routing.yaml
├── config.yaml
├── grids.yaml
├── resources.yaml
├── services.yaml
├── twig_hooks.yaml
└── validation.yaml
├── doc
└── images
│ ├── Commands.png
│ └── ScheduledCommands.png
├── install
└── Application
│ └── config
│ ├── packages
│ ├── sylius_fixtures.yaml
│ ├── sylius_scheduler_command.yaml
│ └── test
│ │ └── sylius_scheduler_command.yaml
│ └── routes
│ └── sylius_scheduler_command.yaml
├── migrations
├── Version20200107151826.php
├── Version20210810054537.php
├── Version20210828114655.php
└── Version20211224090032.php
├── node_modules
├── public
├── column
│ └── index.js
└── controller
│ ├── index.css
│ └── index.js
├── src
├── Action
│ └── CleanLogAction.php
├── Checker
│ ├── EveryMinuteIsDueChecker.php
│ ├── IsDueCheckerInterface.php
│ └── SoftLimitThresholdIsDueChecker.php
├── Command
│ ├── PurgeScheduledCommandCommand.php
│ └── SynoliaSchedulerRunCommand.php
├── Components
│ └── Exceptions
│ │ └── Checker
│ │ └── IsNotDueException.php
├── Controller
│ ├── DownloadController.php
│ ├── EmptyLogsController.php
│ ├── LogViewerController.php
│ └── ScheduledCommandExecuteImmediateController.php
├── DataRetriever
│ └── LogDataRetriever.php
├── DependencyInjection
│ └── SynoliaSyliusSchedulerCommandExtension.php
├── DoctrineEvent
│ └── ScheduledCommandPostRemoveEvent.php
├── Entity
│ ├── Command.php
│ ├── CommandInterface.php
│ ├── ScheduledCommand.php
│ └── ScheduledCommandInterface.php
├── Enum
│ └── ScheduledCommandStateEnum.php
├── EventSubscriber
│ └── ConsoleSubscriber.php
├── Fixture
│ └── SchedulerCommandFixture.php
├── Form
│ ├── CommandChoiceType.php
│ ├── CommandType.php
│ └── ScheduledCommandType.php
├── Grid
│ └── FieldType
│ │ ├── DatetimeFieldType.php
│ │ ├── ScheduledCommandExecutionTimeType.php
│ │ ├── ScheduledCommandHumanReadableExpressionType.php
│ │ ├── ScheduledCommandStateType.php
│ │ └── ScheduledCommandUrlType.php
├── Humanizer
│ ├── CronExpressionHumanizer.php
│ └── HumanizerInterface.php
├── Listener
│ └── Grid
│ │ ├── GoToCommandsButtonGridListener.php
│ │ └── GoToHistoryButtonGridListener.php
├── Menu
│ └── AdminMenuListener.php
├── Parser
│ ├── CommandParser.php
│ └── CommandParserInterface.php
├── Planner
│ ├── ScheduledCommandPlanner.php
│ └── ScheduledCommandPlannerInterface.php
├── Provider
│ └── CalendarWithTimezone.php
├── Repository
│ ├── CommandRepository.php
│ ├── CommandRepositoryInterface.php
│ ├── ScheduledCommandRepository.php
│ └── ScheduledCommandRepositoryInterface.php
├── Runner
│ ├── ScheduleCommandRunner.php
│ └── ScheduleCommandRunnerInterface.php
├── SynoliaSyliusSchedulerCommandPlugin.php
├── Twig
│ └── BytesFormatterExtension.php
├── Validator
│ ├── LogfilePrefixPropertyValidator.php
│ └── LogfilePropertyValidator.php
└── Voter
│ ├── IsDueVoter.php
│ └── IsDueVoterInterface.php
├── templates
├── Controller
│ ├── css.html.twig
│ ├── js.html.twig
│ ├── modal.html.twig
│ └── show.html.twig
└── Grid
│ ├── Action
│ ├── empty_log_file.html.twig
│ ├── empty_logs_file.html.twig
│ ├── execute_immediate.html.twig
│ └── link.html.twig
│ └── Column
│ ├── human_readable_expression.html.twig
│ ├── js.html.twig
│ ├── log_file.html.twig
│ └── scheduled_command_state.html.twig
└── translations
├── messages.en.yml
├── messages.fr.yml
├── messages.nl.yml
├── validators.en.yaml
└── validators.fr.yaml
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Synolia
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .DEFAULT_GOAL := help
2 | SHELL=/bin/bash
3 | COMPOSER_ROOT=composer
4 | TEST_DIRECTORY=tests/Application
5 | INSTALL_DIRECTORY=install/Application
6 | CONSOLE=cd ${TEST_DIRECTORY} && php bin/console -e test
7 | COMPOSER=cd ${TEST_DIRECTORY} && composer
8 | YARN=cd ${TEST_DIRECTORY} && yarn
9 |
10 | SYLIUS_VERSION=2.0
11 | SYMFONY_VERSION=7.2
12 | PHP_VERSION=8.2
13 | PLUGIN_NAME=synolia/sylius-scheduler-command-plugin
14 |
15 | ###
16 | ### DEVELOPMENT
17 | ### ¯¯¯¯¯¯¯¯¯¯¯
18 |
19 | install: sylius ## Install Plugin on Sylius [SYLIUS_VERSION=2.0] [SYMFONY_VERSION=7.2] [PHP_VERSION=8.2]
20 | .PHONY: install
21 |
22 | reset: ## Remove dependencies
23 | ifneq ("$(wildcard ${TEST_DIRECTORY}/bin/console)","")
24 | ${CONSOLE} doctrine:database:drop --force --if-exists || true
25 | endif
26 | rm -rf ${TEST_DIRECTORY}
27 | .PHONY: reset
28 |
29 | phpunit: phpunit-configure phpunit-run ## Run PHPUnit
30 | .PHONY: phpunit
31 |
32 | ###
33 | ### OTHER
34 | ### ¯¯¯¯¯¯
35 |
36 | sylius: sylius-standard update-dependencies install-plugin install-sylius
37 | .PHONY: sylius
38 |
39 | sylius-standard:
40 | ifeq ($(shell [[ $(SYLIUS_VERSION) == *dev ]] && echo true ),true)
41 | ${COMPOSER_ROOT} create-project sylius/sylius-standard:${SYLIUS_VERSION} ${TEST_DIRECTORY} --no-install --no-scripts
42 | else
43 | ${COMPOSER_ROOT} create-project sylius/sylius-standard ${TEST_DIRECTORY} "~${SYLIUS_VERSION}" --no-install --no-scripts
44 | endif
45 | ${COMPOSER} config allow-plugins true
46 | ifeq ($(shell [[ $(SYLIUS_VERSION) == *dev ]] && echo true ),true)
47 | ${COMPOSER} require --no-update sylius/sylius:"${SYLIUS_VERSION}"
48 | else
49 | ${COMPOSER} require --no-update sylius/sylius:"~${SYLIUS_VERSION}"
50 | endif
51 |
52 | update-dependencies:
53 | ${COMPOSER} config extra.symfony.require "~${SYMFONY_VERSION}"
54 | ${COMPOSER} require symfony/asset:~${SYMFONY_VERSION} --no-scripts --no-update
55 | ${COMPOSER} update --no-progress -n
56 |
57 | install-plugin:
58 | ${COMPOSER} config repositories.plugin '{"type": "path", "url": "../../"}'
59 | ${COMPOSER} config extra.symfony.allow-contrib true
60 | ${COMPOSER} config minimum-stability "dev"
61 | ${COMPOSER} config prefer-stable true
62 | ${COMPOSER} req ${PLUGIN_NAME}:* --prefer-source --no-scripts
63 | cp -r ${INSTALL_DIRECTORY} tests
64 |
65 | install-sylius:
66 | ${CONSOLE} doctrine:database:create -n --if-not-exists
67 | ${CONSOLE} doctrine:migrations:migrate -n
68 | ${CONSOLE} messenger:setup-transports -n
69 | ${CONSOLE} sylius:fixtures:load default -n
70 | ${YARN} install
71 | ${YARN} build
72 | ${CONSOLE} cache:clear
73 |
74 | phpunit-configure:
75 | cp phpunit.xml.dist ${TEST_DIRECTORY}/phpunit.xml
76 |
77 | phpunit-run:
78 | cd ${TEST_DIRECTORY} && ./vendor/bin/phpunit --testdox
79 |
80 | behat-configure: ## Configure Behat
81 | (cd ${TEST_DIRECTORY} && cp behat.yml.dist behat.yml)
82 | (cd ${TEST_DIRECTORY} && sed -i "s#vendor/sylius/sylius/src/Sylius/Behat/Resources/config/suites.yml#vendor/${PLUGIN_NAME}/tests/Behat/Resources/suites.yml#g" behat.yml)
83 | (cd ${TEST_DIRECTORY} && sed -i "s#vendor/sylius/sylius/features#vendor/${PLUGIN_NAME}/features#g" behat.yml)
84 | (cd ${TEST_DIRECTORY} && sed -i "s#@cli#@javascript#g" behat.yml)
85 | (cd ${TEST_DIRECTORY} && sed -i '2i \ \ \ \ - { resource: "../vendor/${PLUGIN_NAME}/tests/Behat/Resources/services.yml\" }' config/services_test.yaml)
86 | ${CONSOLE} cache:clear
87 |
88 | grumphp: ## Run GrumPHP
89 | vendor/bin/grumphp run
90 |
91 | help: SHELL=/bin/bash
92 | help: ## Display this help
93 | @IFS=$$'\n'; for line in `grep -h -E '^[a-zA-Z_#-]+:?.*?##.*$$' $(MAKEFILE_LIST)`; do if [ "$${line:0:2}" = "##" ]; then \
94 | echo $$line | awk 'BEGIN {FS = "## "}; {printf "\033[33m %s\033[0m\n", $$2}'; else \
95 | echo $$line | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m%s\n", $$1, $$2}'; fi; \
96 | done; unset IFS;
97 | .PHONY: help
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/synolia/SyliusSchedulerCommandPlugin/blob/main/LICENSE)
2 | [](https://github.com/synolia/SyliusSchedulerCommandPlugin/actions/workflows/analysis.yaml)
3 | [](https://github.com/synolia/SyliusSchedulerCommandPlugin/actions/workflows/sylius.yaml)
4 | [](https://packagist.org/packages/synolia/sylius-scheduler-command-plugin)
5 | [](https://packagist.org/packages/synolia/sylius-scheduler-command-plugin)
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Scheduler Command Plugin
18 |
19 |
20 |
21 | Schedule Symfony Commands in your Sylius admin panel.
22 |
23 | ## Commands list
24 | 
25 |
26 | ## Scheduled Commands list
27 | 
28 |
29 | ## Features
30 |
31 | * See the list of planned command
32 | * Add, edit, enable/disable or delete scheduled commands
33 | * For each command, you have to define :
34 | * Name
35 | * Selected Command from the list of Symfony commands
36 | * Based on Cron schedule expression see [Cron formats](https://abunchofutils.com/u/computing/cron-format-helper/)
37 | * Output Log file prefix (optional)
38 | * Priority (highest is priority)
39 | * Run the Command immediately (at the next passage of the command `synolia:scheduler-run`)
40 | * Run a Command juste one time (from history page clic on `Launch a command` button)
41 | * Download or live view of log files directly from the admin panel
42 | * Define commands with a Factory (from a Doctrine migration, for example)
43 |
44 | ## Requirements
45 |
46 | | | Version |
47 | |:-------|:--------|
48 | | PHP | ^8.2 |
49 | | Sylius | ^1.12 |
50 |
51 | ## Installation
52 |
53 | 1. Add the bundle and dependencies in your composer.json :
54 |
55 | composer config extra.symfony.allow-contrib true
56 | composer req synolia/sylius-scheduler-command-plugin
57 |
58 | 2. Apply migrations to your database:
59 |
60 | bin/console doctrine:migrations:migrate
61 |
62 | 3. Launch Run command in your Crontab
63 |
64 | * * * * * /_PROJECT_DIRECTORY_/bin/console synolia:scheduler-run
65 |
66 | 4. (optional) Showing humanized cron expression
67 |
68 | composer require lorisleiva/cron-translator
69 |
70 | 5. Till `symfony/recipes-contrib` is updated for the v3, you must add `sylius_scheduler_command.yaml` from `install/Application/config/{packages,routes}` to your project by respecting the same folder architecture.
71 |
72 | cp -R vendor/synolia/sylius-scheduler-command-plugin/install/Application/config/packages/* config/packages/
73 | cp -R vendor/synolia/sylius-scheduler-command-plugin/install/Application/config/routes/* config/routes/
74 |
75 | ## Usage
76 |
77 | * Log into admin panel
78 | * Click on `Scheduled commands` in the Scheduled commands section in main menu to manage your Scheduled commands
79 | * Click on `Scheduled commands history` in the Scheduled commands section in main menu to see history of commands
80 |
81 | ## Fixtures
82 | Inside sylius fixture file `config/packages/sylius_fixtures.yaml` you can add scheduled command fixtures to your suite.
83 | ```yaml
84 | sylius_fixtures:
85 | suites:
86 | my_fixture_suite:
87 | fixtures:
88 | scheduler_command:
89 | options:
90 | scheduled_commands:
91 | -
92 | name: 'Reset Sylius'
93 | command: 'sylius:fixtures:load'
94 | cronExpression: '0 0 * * *'
95 | logFilePrefix: 'reset'
96 | priority: 0
97 | enabled: true
98 | -
99 | name: 'Cancel Unpaid Orders'
100 | command: 'sylius:cancel-unpaid-orders'
101 | cronExpression: '0 0 * * *'
102 | priority: 1
103 | enabled: false
104 | ```
105 |
106 | ## Commands
107 | ### synolia:scheduler-run
108 |
109 | Execute scheduled commands.
110 |
111 | * options:
112 | * --id (run only a specific scheduled command)
113 |
114 | **Run all scheduled commands :** php bin/console synolia:scheduler-run
115 |
116 | **Run one scheduled command :** php bin/console synolia:scheduler-run --only-one
117 |
118 | **Run a specific scheduled command :** php bin/console synolia:scheduler-run --id=5
119 |
120 | Is it possible to choose the timezone of the command execution by setting the `SYNOLIA_SCHEDULER_PLUGIN_TIMEZONE` environment variable, example:
121 |
122 | ```
123 | SYNOLIA_SCHEDULER_PLUGIN_TIMEZONE=Europe/Paris
124 | ```
125 |
126 | ### synolia:scheduler:purge-history
127 |
128 | Purge scheduled command history greater than {X} days old.
129 |
130 | * options:
131 | * --all (purge everything)
132 | * --days (number of days to keep)
133 | * --state (array of schedule states)
134 | * --dry-run
135 |
136 | **Example to remove all finished and in error scheduled commands after 7 days :**
137 |
138 | php bin/console synolia:scheduler:purge-history --state=finished --state=error --days=7
139 |
140 | ## Optional services
141 | ```yaml
142 | services:
143 | ...
144 | # By enabling this service, it will be requested to vote after the other EveryMinuteIsDueChecker checker.
145 | # Using some cloud providers, even if the master cron is set to run every minute, it is actually not run that often
146 | # This service allows you to set a soft threshold limit, so if your provider is actually running the master cron every 5 minutes
147 | # This service will execute the cron if we are still in the threshold limit ONLY IF it was not already executed another time in the same range.
148 | #
149 | # CONFIGURATION SCENARIO: cron set to be run at 01:07 in the scheduler command plugin
150 | #
151 | # SCENARIO CASES AT 1 CRON PASS EVERY 5 MINUTES FROM THE PROVIDER
152 | # cron passes at 01:04 - 1..5 minutes: IS NOT DUE
153 | # cron passes at 01:05 - 1..5 minutes: IS NOT DUE
154 | # cron passes at 01:06 - 1..5 minutes: IS NOT DUE
155 | # cron passes at 01:07 - 1..5 minutes: IS DUE (but it should already be handled by EveryMinuteIsDueChecker)
156 | # cron passes at 01:08 - 1..5 minutes: IS DUE
157 | # cron passes at 01:09 - 1..5 minutes: IS DUE #should not if another has started during the threshold period
158 | # cron passes at 01:10 - 1..5 minutes: IS DUE #should not if another has started during the threshold period
159 | # cron passes at 01:11 - 1..5 minutes: IS DUE #should not if another has started during the threshold period
160 | # cron passes at 01:12 - 1..5 minutes: IS DUE #should not if another has started during the threshold period
161 | # cron passes at 01:13 - 1..5 minutes: IS NOT DUE
162 | Synolia\SyliusSchedulerCommandPlugin\Checker\SoftLimitThresholdIsDueChecker:
163 | tags:
164 | - { name: !php/const Synolia\SyliusSchedulerCommandPlugin\Checker\IsDueCheckerInterface::TAG_ID }
165 | #optionnal, default value is 5 minutes
166 | arguments:
167 | $threshold: 5 #soft limit threshold in minutes
168 | ```
169 |
170 | ## Development
171 |
172 | See [How to contribute](CONTRIBUTING.md).
173 |
174 | ## License
175 |
176 | This library is under the MIT license.
177 |
178 | ## Credits
179 |
180 | Developed by [Synolia](https://synolia.com/).
181 |
--------------------------------------------------------------------------------
/UPGRADE.md:
--------------------------------------------------------------------------------
1 | # Upgrade
2 |
3 | ## from ^2.0 to ^3.0
4 |
5 | ### Fixtures
6 | * `logFile` field must be renamed to `logFilePrefix` and must not end with the file extension
7 |
8 | Also, for old existing schedules in your database, please remove the log file extension in column `logFilePrefix`.
9 |
10 | ## from 3.8 to 3.9
11 |
12 | * The constructors of `Synolia\SyliusSchedulerCommandPlugin\Checker\EveryMinuteIsDueChecker` and `Synolia\SyliusSchedulerCommandPlugin\Checker\SoftLimitThresholdIsDueChecker` has been modified, a new argument has been added :
13 |
14 | ```php
15 | public function __construct(
16 | // ...
17 | private ?DateTimeProviderInterface $dateTimeProvider = null,
18 | )
19 | ```
20 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "synolia/sylius-scheduler-command-plugin",
3 | "type": "sylius-plugin",
4 | "keywords": [
5 | "sylius",
6 | "sylius-plugin",
7 | "scheduler",
8 | "command"
9 | ],
10 | "description": "Scheduler Command Plugin.",
11 | "license": "MIT",
12 | "require": {
13 | "php": "^8.2",
14 | "ext-json": "*",
15 | "ext-intl": "*",
16 | "dragonmantank/cron-expression": "^3.4",
17 | "sylius/sylius": "^2.0",
18 | "symfony/framework-bundle": "^5.4|^6.0|^7.0",
19 | "symfony/lock": "^6.4|^7.0",
20 | "symfony/polyfill-intl-icu": "^1.26",
21 | "symfony/process": "^6.4|^7.0",
22 | "symfony/service-contracts": "^3.5",
23 | "webmozart/assert": "^1.11"
24 | },
25 | "require-dev": {
26 | "behat/behat": "^3.12",
27 | "behat/mink-selenium2-driver": "^1.6",
28 | "dmore/behat-chrome-extension": "^1.4",
29 | "dmore/chrome-mink-driver": "^2.9",
30 | "friends-of-behat/mink": "^1.10",
31 | "friends-of-behat/mink-browserkit-driver": "^1.6",
32 | "friends-of-behat/mink-debug-extension": "^2.1",
33 | "friends-of-behat/mink-extension": "^2.7",
34 | "friends-of-behat/page-object-extension": "^0.3",
35 | "friends-of-behat/suite-settings-extension": "^1.1",
36 | "friends-of-behat/symfony-extension": "^2.4",
37 | "friends-of-behat/variadic-extension": "^1.5",
38 | "friendsoftwig/twigcs": "6.4.0",
39 | "j13k/yaml-lint": "^1.1",
40 | "php-parallel-lint/php-parallel-lint": "^1.4",
41 | "phpmd/phpmd": "^2.15.0",
42 | "phpro/grumphp": "^2.9",
43 | "phpspec/phpspec": "^7.3",
44 | "phpstan/extension-installer": "^1.3",
45 | "phpstan/phpstan": "^2.0",
46 | "phpstan/phpstan-doctrine": "^2.0",
47 | "phpstan/phpstan-strict-rules": "^2.0",
48 | "phpstan/phpstan-webmozart-assert": "^2.0",
49 | "phpunit/phpunit": "^9.5",
50 | "rector/rector": "^2.0",
51 | "seld/jsonlint": "^1.11",
52 | "slevomat/coding-standard": "^8.7",
53 | "squizlabs/php_codesniffer": "^3.11",
54 | "sylius-labs/coding-standard": "^4.3",
55 | "symfony/browser-kit": "^6.4",
56 | "symfony/debug-bundle": "^6.4",
57 | "symfony/dotenv": "^6.4",
58 | "symfony/intl": "^6.4",
59 | "symfony/web-profiler-bundle": "^6.4",
60 | "symplify/easy-coding-standard": "^12.5"
61 | },
62 | "suggest": {
63 | "lorisleiva/cron-translator": "Allow showing humanized and translated cron expression."
64 | },
65 | "prefer-stable": true,
66 | "autoload": {
67 | "psr-4": {
68 | "Synolia\\SyliusSchedulerCommandPlugin\\": "src/",
69 | "Tests\\Synolia\\SyliusSchedulerCommandPlugin\\": "tests/"
70 | }
71 | },
72 | "config": {
73 | "sort-packages": true,
74 | "allow-plugins": {
75 | "dealerdirect/phpcodesniffer-composer-installer": true,
76 | "ergebnis/composer-normalize": true,
77 | "php-http/discovery": true,
78 | "phpro/grumphp": true,
79 | "phpstan/extension-installer": true,
80 | "symfony/thanks": true
81 | }
82 | },
83 | "scripts": {
84 | "fix-ecs": "ecs check -c ruleset/ecs.php --fix --ansi --clear-cache"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/config/admin_routing.yaml:
--------------------------------------------------------------------------------
1 | synolia_admin_commands:
2 | resource: |
3 | alias: synolia.command
4 | templates: "@SyliusAdmin\\shared\\crud"
5 | section: admin
6 | grid: synolia_admin_commands
7 | except: ['show']
8 | permission: true
9 | type: sylius.resource
10 |
11 | synolia_admin_scheduled_commands:
12 | resource: |
13 | alias: synolia.scheduled_command
14 | templates: "@SyliusAdmin\\shared\\crud"
15 | section: admin
16 | grid: synolia_admin_scheduled_commands
17 | except: ['update', 'show']
18 | permission: true
19 | type: sylius.resource
20 |
21 | controllers:
22 | resource:
23 | path: ../src/Controller/
24 | namespace: Synolia\SyliusSchedulerCommandPlugin\Controller
25 | type: attribute
--------------------------------------------------------------------------------
/config/config.yaml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: "resources.yaml" }
3 | - { resource: "grids.yaml" }
4 | - { resource: "services.yaml" }
5 | - { resource: "twig_hooks.yaml" }
6 |
--------------------------------------------------------------------------------
/config/grids.yaml:
--------------------------------------------------------------------------------
1 | sylius_grid:
2 | grids:
3 | synolia_admin_commands:
4 | driver:
5 | options:
6 | class: Synolia\SyliusSchedulerCommandPlugin\Entity\Command
7 | fields:
8 | name:
9 | type: string
10 | sortable: ~
11 | label: synolia.ui.scheduled_command.name
12 | command:
13 | type: string
14 | sortable: ~
15 | label: synolia.ui.scheduled_command.command
16 | cronExpression:
17 | type: scheduled_human_readable_expression
18 | label: synolia.ui.scheduled_command.cron_expression
19 | options:
20 | template: '@SynoliaSyliusSchedulerCommandPlugin/Grid/Column/human_readable_expression.html.twig'
21 | priority:
22 | type: string
23 | sortable: ~
24 | label: synolia.ui.scheduled_command.priority
25 | enabled:
26 | type: twig
27 | sortable: ~
28 | label: synolia.ui.scheduled_command.enabled
29 | options:
30 | template: "@SyliusUi/grid/field/enabled.html.twig"
31 | sorting:
32 | name: asc
33 | command: asc
34 | priority: asc
35 | enabled: asc
36 | filters:
37 | name:
38 | label: synolia.ui.scheduled_command.name
39 | type: string
40 | enabled: true
41 | command:
42 | label: synolia.ui.scheduled_command.command
43 | type: string
44 | enabled: true
45 | cronExpression:
46 | label: synolia.ui.scheduled_command.cron_expression
47 | type: string
48 | enabled: true
49 | priority:
50 | label: synolia.ui.scheduled_command.priority
51 | type: string
52 | enabled: true
53 | enabled:
54 | label: synolia.ui.scheduled_command.enabled
55 | type: boolean
56 | enabled: true
57 | actions:
58 | main:
59 | create:
60 | type: create
61 | item:
62 | execute:
63 | type: schedule_command_execute_immediate
64 | label: synolia.ui.scheduled_command.execute_immediate
65 | options:
66 | link:
67 | route: execute_immediate_schedule
68 | parameters:
69 | commandId: resource.id
70 | update:
71 | type: update
72 | delete:
73 | type: delete
74 | bulk:
75 | delete:
76 | type: delete
77 | synolia_admin_scheduled_commands:
78 | driver:
79 | options:
80 | class: Synolia\SyliusSchedulerCommandPlugin\Entity\ScheduledCommand
81 | fields:
82 | name:
83 | type: string
84 | sortable: ~
85 | label: synolia.ui.scheduled_command.name
86 | state:
87 | type: scheduled_command_state
88 | sortable: ~
89 | label: synolia.ui.scheduled_command.state
90 | options:
91 | template: '@SynoliaSyliusSchedulerCommandPlugin/Grid/Column/scheduled_command_state.html.twig'
92 | executedAt:
93 | type: scheduled_command_executed_at
94 | sortable: createdAt
95 | label: synolia.ui.scheduled_command.last_execution
96 | options:
97 | date_format: !php/const \IntlDateFormatter::SHORT
98 | time_format: !php/const \IntlDateFormatter::SHORT
99 | commandExecutionTime:
100 | type: scheduled_command_execution_time
101 | label: synolia.ui.scheduled_command.command_execution_time
102 | logFile:
103 | type: scheduled_command_url
104 | sortable: ~
105 | label: synolia.ui.scheduled_command.log_file
106 | options:
107 | template: '@SynoliaSyliusSchedulerCommandPlugin/Grid/Column/log_file.html.twig'
108 | sorting:
109 | executedAt: desc
110 | filters:
111 | id:
112 | label: synolia.ui.scheduled_command.id
113 | type: string
114 | enabled: false
115 | name:
116 | label: synolia.ui.scheduled_command.name
117 | type: string
118 | enabled: true
119 | command:
120 | label: synolia.ui.scheduled_command.command
121 | type: string
122 | enabled: true
123 | state:
124 | label: synolia.ui.scheduled_command.state
125 | type: string
126 | enabled: true
127 | actions:
128 | main:
129 | create:
130 | type: create
131 | label: synolia.ui.launch_a_command
132 | item:
133 | delete:
134 | type: delete
135 | bulk:
136 | delete:
137 | type: delete
138 | templates:
139 | action:
140 | schedule_command_execute_immediate: "@SynoliaSyliusSchedulerCommandPlugin/Grid/Action/execute_immediate.html.twig"
141 | link: "@SynoliaSyliusSchedulerCommandPlugin/Grid/Action/link.html.twig"
142 |
--------------------------------------------------------------------------------
/config/resources.yaml:
--------------------------------------------------------------------------------
1 | sylius_resource:
2 | resources:
3 | synolia.command:
4 | driver: doctrine/orm
5 | classes:
6 | model: Synolia\SyliusSchedulerCommandPlugin\Entity\Command
7 | form: Synolia\SyliusSchedulerCommandPlugin\Form\CommandType
8 | repository: Synolia\SyliusSchedulerCommandPlugin\Repository\CommandRepository
9 | synolia.scheduled_command:
10 | driver: doctrine/orm
11 | classes:
12 | model: Synolia\SyliusSchedulerCommandPlugin\Entity\ScheduledCommand
13 | form: Synolia\SyliusSchedulerCommandPlugin\Form\ScheduledCommandType
14 | repository: Synolia\SyliusSchedulerCommandPlugin\Repository\ScheduledCommandRepository
15 |
--------------------------------------------------------------------------------
/config/services.yaml:
--------------------------------------------------------------------------------
1 | parameters:
2 | env(SYNOLIA_SCHEDULER_PLUGIN_PING_INTERVAL): 300
3 | env(SYNOLIA_SCHEDULER_PLUGIN_KEEP_ALIVE): true
4 | env(SYNOLIA_SCHEDULER_PLUGIN_LOGS_DIR): '%kernel.logs_dir%'
5 | env(SYNOLIA_SCHEDULER_PLUGIN_TIMEFORMAT_24H): false
6 | env(SYNOLIA_SCHEDULER_PLUGIN_TIMEZONE): ~
7 |
8 | services:
9 | _defaults:
10 | autowire: true
11 | autoconfigure: true
12 |
13 | Synolia\SyliusSchedulerCommandPlugin\:
14 | resource: '../src/*'
15 | exclude: '../src/{Entity,Migrations,SynoliaSyliusSchedulerCommandPlugin.php}'
--------------------------------------------------------------------------------
/config/twig_hooks.yaml:
--------------------------------------------------------------------------------
1 | sylius_twig_hooks:
2 | hooks:
3 | 'synolia.admin.command.logs.javascripts':
4 | js:
5 | priority: 0
6 | template: '@SynoliaSyliusSchedulerCommandPlugin/Controller/js.html.twig'
7 | 'synolia.admin.command.logs.stylesheets':
8 | css:
9 | priority: 0
10 | template: '@SynoliaSyliusSchedulerCommandPlugin/Controller/css.html.twig'
11 | 'sylius_admin.scheduled_command.index#javascripts':
12 | js:
13 | priority: 0
14 | template: '@SynoliaSyliusSchedulerCommandPlugin/Grid/Column/js.html.twig'
15 | 'sylius_admin.scheduled_command.index':
16 | modal:
17 | priority: 0
18 | template: '@SynoliaSyliusSchedulerCommandPlugin/Controller/modal.html.twig'
--------------------------------------------------------------------------------
/config/validation.yaml:
--------------------------------------------------------------------------------
1 | Synolia\SyliusSchedulerCommandPlugin\Entity\Command:
2 | properties:
3 | name:
4 | - NotBlank: ~
5 | - Length:
6 | min: 2
7 | command:
8 | - NotBlank: ~
9 | cronExpression:
10 | - NotBlank: ~
11 | - Length:
12 | min: 9
13 | priority:
14 | - NotBlank: ~
15 | - PositiveOrZero: ~
16 | logFilePrefix:
17 | - Callback: [ Synolia\SyliusSchedulerCommandPlugin\Validator\LogfilePrefixPropertyValidator, validate ]
18 |
19 | Synolia\SyliusSchedulerCommandPlugin\Entity\ScheduledCommand:
20 | properties:
21 | name:
22 | - NotNull: ~
23 | - NotBlank: ~
24 | - Length:
25 | min: 2
26 | command:
27 | - NotBlank: ~
28 | logFile:
29 | - Callback: [ Synolia\SyliusSchedulerCommandPlugin\Validator\LogfilePropertyValidator, validate ]
30 |
31 |
--------------------------------------------------------------------------------
/doc/images/Commands.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/synolia/SyliusSchedulerCommandPlugin/f565c8df166738fff2000cecf3c6c5e4bba491c5/doc/images/Commands.png
--------------------------------------------------------------------------------
/doc/images/ScheduledCommands.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/synolia/SyliusSchedulerCommandPlugin/f565c8df166738fff2000cecf3c6c5e4bba491c5/doc/images/ScheduledCommands.png
--------------------------------------------------------------------------------
/install/Application/config/packages/sylius_fixtures.yaml:
--------------------------------------------------------------------------------
1 | sylius_fixtures:
2 | suites:
3 | default:
4 | listeners:
5 | orm_purger: ~
6 | logger: ~
7 | fixtures:
8 | scheduler_command:
9 | options:
10 | scheduled_commands:
11 | -
12 | name: 'Reset Sylius'
13 | command: 'sylius:fixtures:load'
14 | cronExpression: '0 0 * * *'
15 | logFilePrefix: 'reset'
16 | priority: 0
17 | enabled: false
18 |
--------------------------------------------------------------------------------
/install/Application/config/packages/sylius_scheduler_command.yaml:
--------------------------------------------------------------------------------
1 | imports:
2 | - { resource: "@SynoliaSyliusSchedulerCommandPlugin/config/config.yaml" }
3 |
--------------------------------------------------------------------------------
/install/Application/config/packages/test/sylius_scheduler_command.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | _defaults:
3 | autowire: true
4 | autoconfigure: true
5 | public: true
6 |
7 | Synolia\SyliusSchedulerCommandPlugin\Checker\SoftLimitThresholdIsDueChecker: ~
8 |
--------------------------------------------------------------------------------
/install/Application/config/routes/sylius_scheduler_command.yaml:
--------------------------------------------------------------------------------
1 | synolia_scheduled_command:
2 | resource: "@SynoliaSyliusSchedulerCommandPlugin/config/admin_routing.yaml"
3 | prefix: '/%sylius_admin.path_name%'
4 |
--------------------------------------------------------------------------------
/migrations/Version20200107151826.php:
--------------------------------------------------------------------------------
1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
24 |
25 | $this->addSql('CREATE TABLE scheduled_command (
26 | id INT AUTO_INCREMENT NOT NULL,
27 | name VARCHAR(255) NOT NULL,
28 | command VARCHAR(255) NOT NULL,
29 | arguments VARCHAR(255) DEFAULT NULL,
30 | cronExpression VARCHAR(255) NOT NULL,
31 | lastExecution DATETIME DEFAULT NULL,
32 | lastReturnCode INT DEFAULT NULL,
33 | logFile VARCHAR(255) DEFAULT NULL,
34 | priority INT NOT NULL,
35 | executeImmediately TINYINT(1) NOT NULL,
36 | enabled TINYINT(1) NOT NULL,
37 | commandEndTime DATETIME DEFAULT NULL,
38 | PRIMARY KEY(id)
39 | ) DEFAULT CHARACTER SET UTF8 COLLATE `UTF8_unicode_ci` ENGINE = InnoDB');
40 | }
41 |
42 | public function down(Schema $schema): void
43 | {
44 | // this down() migration is auto-generated, please modify it to your needs
45 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
46 |
47 | $this->addSql('DROP TABLE scheduled_command');
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/migrations/Version20210810054537.php:
--------------------------------------------------------------------------------
1 | addSql('CREATE TABLE synolia_commands (
24 | id INT AUTO_INCREMENT NOT NULL,
25 | name VARCHAR(255) NOT NULL,
26 | command VARCHAR(255) NOT NULL,
27 | arguments VARCHAR(255) DEFAULT NULL,
28 | cronExpression VARCHAR(255) NOT NULL,
29 | logFilePrefix VARCHAR(255) DEFAULT NULL,
30 | priority INT NOT NULL,
31 | executeImmediately TINYINT(1) NOT NULL,
32 | enabled TINYINT(1) NOT NULL,
33 | PRIMARY KEY(id)
34 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
35 | $this->addSql('CREATE TABLE synolia_scheduled_commands (
36 | id INT AUTO_INCREMENT NOT NULL,
37 | name VARCHAR(255) NOT NULL,
38 | command VARCHAR(255) NOT NULL,
39 | state VARCHAR(255) NOT NULL,
40 | arguments VARCHAR(255) DEFAULT NULL,
41 | executed_at DATETIME DEFAULT NULL,
42 | lastReturnCode INT DEFAULT NULL,
43 | logFile VARCHAR(255) DEFAULT NULL,
44 | commandEndTime DATETIME DEFAULT NULL,
45 | created_At DATETIME NOT NULL,
46 | PRIMARY KEY(id)
47 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
48 | $this->addSql('DROP TABLE scheduled_command');
49 | }
50 |
51 | public function down(Schema $schema): void
52 | {
53 | // this down() migration is auto-generated, please modify it to your needs
54 | $this->addSql('CREATE TABLE scheduled_command (
55 | id INT AUTO_INCREMENT NOT NULL,
56 | name VARCHAR(255) CHARACTER SET utf8 NOT NULL COLLATE `utf8_unicode_ci`,
57 | command VARCHAR(255) CHARACTER SET utf8 NOT NULL COLLATE `utf8_unicode_ci`,
58 | arguments VARCHAR(255) CHARACTER SET utf8 DEFAULT NULL COLLATE `utf8_unicode_ci`,
59 | cronExpression VARCHAR(255) CHARACTER SET utf8 NOT NULL COLLATE `utf8_unicode_ci`,
60 | lastExecution DATETIME DEFAULT NULL,
61 | lastReturnCode INT DEFAULT NULL,
62 | logFile VARCHAR(255) CHARACTER SET utf8 DEFAULT NULL COLLATE `utf8_unicode_ci`,
63 | priority INT NOT NULL,
64 | executeImmediately TINYINT(1) NOT NULL,
65 | enabled TINYINT(1) NOT NULL,
66 | commandEndTime DATETIME DEFAULT NULL,
67 | PRIMARY KEY(id)
68 | ) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB COMMENT = \'\'');
69 | $this->addSql('DROP TABLE synolia_commands');
70 | $this->addSql('DROP TABLE synolia_scheduled_commands');
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/migrations/Version20210828114655.php:
--------------------------------------------------------------------------------
1 | addSql('ALTER TABLE synolia_scheduled_commands ADD owner_id INT DEFAULT NULL');
24 | $this->addSql('ALTER TABLE
25 | synolia_scheduled_commands
26 | ADD
27 | CONSTRAINT FK_813781F7E3C61F9 FOREIGN KEY (owner_id) REFERENCES synolia_commands (id) ON DELETE
28 | SET
29 | NULL');
30 | $this->addSql('CREATE INDEX IDX_813781F7E3C61F9 ON synolia_scheduled_commands (owner_id)');
31 | }
32 |
33 | public function down(Schema $schema): void
34 | {
35 | // this down() migration is auto-generated, please modify it to your needs
36 | $this->addSql('ALTER TABLE synolia_scheduled_commands DROP FOREIGN KEY FK_813781F7E3C61F9');
37 | $this->addSql('DROP INDEX IDX_813781F7E3C61F9 ON synolia_scheduled_commands');
38 | $this->addSql('ALTER TABLE synolia_scheduled_commands DROP owner_id');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/migrations/Version20211224090032.php:
--------------------------------------------------------------------------------
1 | addSql('ALTER TABLE synolia_commands ADD timeout INT DEFAULT NULL');
24 | $this->addSql('ALTER TABLE synolia_scheduled_commands ADD timeout INT DEFAULT NULL');
25 | $this->addSql('ALTER TABLE synolia_commands ADD idleTimeout INT DEFAULT NULL');
26 | $this->addSql('ALTER TABLE synolia_scheduled_commands ADD idleTimeout INT DEFAULT NULL');
27 | }
28 |
29 | public function down(Schema $schema): void
30 | {
31 | // this down() migration is auto-generated, please modify it to your needs
32 | $this->addSql('ALTER TABLE synolia_commands DROP idleTimeout');
33 | $this->addSql('ALTER TABLE synolia_scheduled_commands DROP idleTimeout');
34 | $this->addSql('ALTER TABLE synolia_commands DROP timeout');
35 | $this->addSql('ALTER TABLE synolia_scheduled_commands DROP timeout');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/node_modules:
--------------------------------------------------------------------------------
1 | tests/Application/node_modules
--------------------------------------------------------------------------------
/public/column/index.js:
--------------------------------------------------------------------------------
1 | function fireLog(e) {
2 | e.preventDefault();
3 | const url = e.currentTarget.getAttribute('data-href');
4 | fetch(url)
5 | .then(response => response.text())
6 | .then(data => {
7 | const logModal = document.getElementById('log-modal');
8 | const doc = new DOMParser().parseFromString(data, 'text/html');
9 | setHTMLWithScript('.modal-body', doc.querySelector('.page').innerHTML);
10 | var modal = new bootstrap.Modal(logModal);
11 | modal.show();
12 | logModal.addEventListener('hide.bs.modal', () => {
13 | for (let i = 0; i < 10000; i++) {
14 | window.clearInterval(i);
15 | }
16 | })
17 | })
18 | }
19 |
20 | function setHTMLWithScript(selector, html) {
21 | const container = document.querySelector(selector);
22 | container.innerHTML = html;
23 |
24 | const scripts = container.querySelectorAll("script");
25 | scripts.forEach(oldScript => {
26 | const newScript = document.createElement("script");
27 | if (oldScript.src) {
28 | newScript.src = oldScript.src;
29 | } else {
30 | newScript.textContent = oldScript.textContent;
31 | }
32 | oldScript.replaceWith(newScript);
33 | });
34 | }
--------------------------------------------------------------------------------
/public/controller/index.css:
--------------------------------------------------------------------------------
1 | #results {
2 | padding: 5px;
3 | overflow-y: scroll;
4 | height: calc(100% - 150px);
5 | }
6 |
7 | #log-modal .description,
8 | #log-modal > .content,
9 | #log-modal #content {
10 | height: 100%;
11 | }
12 |
13 | #results .loader:before {
14 | border-color: rgba(0,0,0,.1);
15 | }
16 |
17 | #results .loader:after {
18 | border-color: #767676 transparent transparent;
19 | }
20 |
--------------------------------------------------------------------------------
/public/controller/index.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | let lastSize = 0;
3 | let grep = "";
4 | let invert = 0;
5 | let documentHeight = 0;
6 | let scrollPosition = 0;
7 | let scroll = false;
8 |
9 | const grepInput = document.getElementById("grep");
10 | const results = document.getElementById("results");
11 | const floatElements = document.querySelectorAll(".float");
12 |
13 | // Handle "Enter" keyup in #grep
14 | grepInput.addEventListener("keyup", function (e) {
15 | if (e.key === "Enter") {
16 | lastSize = 0;
17 | grep = this.value;
18 | results.innerHTML = "";
19 | }
20 | });
21 |
22 | // Focus on #grep input
23 | grepInput.focus();
24 | // Periodic log update
25 | const updateLog = setInterval(function () {
26 | fetch(`${sy_route}?refresh=1&lastsize=${lastSize}&grep-keywords=${grep}&invert=${invert}`)
27 | .then(response => response.json())
28 | .then(data => {
29 | lastSize = data.size;
30 | data.data.forEach(value => {
31 | const entry = document.createElement("div");
32 | entry.innerHTML = value + "
";
33 | results.prepend(entry);
34 | });
35 |
36 | if (scroll) {
37 | scrollToBottom();
38 | }
39 |
40 | if (data.data.length < 1) {
41 | const loader = results.querySelector(".loader");
42 | if (loader) loader.remove();
43 | }
44 | })
45 | .catch(error => {
46 | results.innerHTML += error.message;
47 | clearInterval(updateLog);
48 | });
49 | }, sy_updateTime);
50 |
51 | // Fix float element on scroll
52 | window.addEventListener("scroll", function () {
53 | const scrollTop = window.scrollY;
54 | floatElements.forEach(el => {
55 | if (scrollTop > 0) {
56 | el.style.position = "fixed";
57 | el.style.top = "0";
58 | el.style.left = "auto";
59 | } else {
60 | el.style.position = "static";
61 | }
62 | });
63 |
64 | documentHeight = document.documentElement.scrollHeight;
65 | scrollPosition = window.innerHeight + window.scrollY;
66 | scroll = documentHeight <= scrollPosition;
67 | });
68 |
69 | // Scroll to bottom on resize if needed
70 | window.addEventListener("resize", function () {
71 | if (scroll) {
72 | scrollToBottom();
73 | }
74 | });
75 |
76 | scrollToBottom();
77 |
78 | // Scroll to bottom function
79 | function scrollToBottom() {
80 | window.scrollTo({
81 | top: document.body.scrollHeight,
82 | behavior: "smooth"
83 | });
84 | }
85 | })();
--------------------------------------------------------------------------------
/src/Action/CleanLogAction.php:
--------------------------------------------------------------------------------
1 | ['permission' => true]], methods: ['GET'])]
18 | public function __invoke(
19 | TranslatorInterface $translator,
20 | CommandRepository $commandRepository,
21 | string $command,
22 | string $logsDir,
23 | ): Response {
24 | /** @var ScheduledCommand|null $scheduleCommand */
25 | $scheduleCommand = $commandRepository->find($command);
26 |
27 | if (null === $scheduleCommand) {
28 | $this->addFlash('error', $translator->trans('sylius.ui.scheduled_command_not_exists'));
29 |
30 | return $this->redirectToGrid();
31 | }
32 |
33 | if (null === $scheduleCommand->getLogFile()) {
34 | $this->addFlash('error', $translator->trans('sylius.ui.log_file_undefined'));
35 |
36 | return $this->redirectToGrid();
37 | }
38 |
39 | $filePath = $logsDir . \DIRECTORY_SEPARATOR . $scheduleCommand->getLogFile();
40 | if (!\file_exists($filePath)) {
41 | $this->addFlash('error', $translator->trans('sylius.ui.no_log_file_found'));
42 |
43 | return $this->redirectToGrid();
44 | }
45 |
46 | try {
47 | file_put_contents($filePath, '');
48 | } catch (\Throwable) {
49 | $this->addFlash('error', $translator->trans('sylius.ui.error_emptying_log_file'));
50 | }
51 |
52 | $this->addFlash('success', $translator->trans('sylius.ui.log_file_successfully_emptied'));
53 |
54 | return $this->redirectToGrid();
55 | }
56 |
57 | private function redirectToGrid(): RedirectResponse
58 | {
59 | return $this->redirectToRoute(
60 | 'synolia_admin_command_index',
61 | [],
62 | Response::HTTP_MOVED_PERMANENTLY,
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Checker/EveryMinuteIsDueChecker.php:
--------------------------------------------------------------------------------
1 | dateTimeProvider?->now() ?? new \DateTime();
38 | }
39 |
40 | $cron = new CronExpression($command->getCronExpression());
41 |
42 | if (!$cron->isDue($dateTime)) {
43 | throw new IsNotDueException();
44 | }
45 |
46 | return true;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Checker/IsDueCheckerInterface.php:
--------------------------------------------------------------------------------
1 | dateTimeProvider?->now() ?? new \DateTime();
41 | }
42 |
43 | $cron = new CronExpression($command->getCronExpression());
44 | if ($cron->isDue($dateTime)) {
45 | return true;
46 | }
47 |
48 | $previousRunDate = $cron->getPreviousRunDate();
49 | $previousRunDateThreshold = (clone $previousRunDate)->add(new \DateInterval(\sprintf('PT%dM', $this->threshold)));
50 |
51 | $lastCreatedScheduledCommand = $this->scheduledCommandRepository->findLastCreatedCommand($command);
52 |
53 | // if never, do my command is valid for the least "threshold" minutes
54 | if (!$lastCreatedScheduledCommand instanceof \Synolia\SyliusSchedulerCommandPlugin\Entity\ScheduledCommandInterface) {
55 | if ($dateTime->getTimestamp() >= $previousRunDate->getTimestamp() && $dateTime->getTimestamp() <= $previousRunDateThreshold->getTimestamp()) {
56 | return true;
57 | }
58 |
59 | throw new IsNotDueException();
60 | }
61 |
62 | // check if last command has been started since scheduled datetime +0..5 minutes
63 | if (
64 | $lastCreatedScheduledCommand->getCreatedAt()->getTimestamp() >= $previousRunDate->getTimestamp() &&
65 | $lastCreatedScheduledCommand->getCreatedAt()->getTimestamp() <= $previousRunDateThreshold->getTimestamp() &&
66 | $dateTime->getTimestamp() <= $previousRunDateThreshold->getTimestamp()
67 | ) {
68 | throw new IsNotDueException();
69 | }
70 |
71 | if (
72 | $dateTime->getTimestamp() >= $previousRunDate->getTimestamp() &&
73 | $dateTime->getTimestamp() <= $previousRunDateThreshold->getTimestamp()
74 | ) {
75 | return true;
76 | }
77 |
78 | throw new IsNotDueException();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Command/PurgeScheduledCommandCommand.php:
--------------------------------------------------------------------------------
1 | addOption('all', 'p', InputOption::VALUE_NONE, 'Remove all schedules with specified state (default is finished).')
43 | ->addOption('days', 'd', InputOption::VALUE_OPTIONAL, '{X} days old', self::DEFAULT_PURGE_PERIODE_IN_DAYS)
44 | ->addOption('state', 's', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'State of scheduled history to be cleaned', [self::DEFAULT_STATE])
45 | ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Dry run')
46 | ;
47 | }
48 |
49 | protected function execute(InputInterface $input, OutputInterface $output): int
50 | {
51 | $this->io = new SymfonyStyle($input, $output);
52 |
53 | $purgeAll = $input->getOption('all');
54 | $daysOld = (int) $input->getOption('days');
55 | $state = $input->getOption('state');
56 | /** @var bool $dryRun */
57 | $dryRun = $input->getOption('dry-run') ?? false;
58 |
59 | /** @var ScheduledCommandInterface[] $schedules */
60 | $schedules = $this->getScheduledHistory($purgeAll, $daysOld, $state);
61 |
62 | $counter = 0;
63 | foreach ($schedules as $schedule) {
64 | $this->logger->info(\sprintf(
65 | 'Removed scheduled command "%s" (%d)',
66 | $schedule->getName(),
67 | $schedule->getId(),
68 | ));
69 |
70 | if ($dryRun) {
71 | continue;
72 | }
73 |
74 | $this->entityManager->remove($schedule);
75 |
76 | if ($counter % self::DEFAULT_BATCH === 0) {
77 | $this->entityManager->flush();
78 | }
79 | ++$counter;
80 | }
81 |
82 | $this->entityManager->flush();
83 |
84 | return 0;
85 | }
86 |
87 | private function getScheduledHistory(bool $purgeAll, int $daysOld, array $states): iterable
88 | {
89 | if ($purgeAll) {
90 | $this->io->note(\sprintf(
91 | 'All schedules with states ["%s"] will be purged.',
92 | \implode(',', $states),
93 | ));
94 |
95 | return $this->scheduledCommandRepository->findAllHavingState($states);
96 | }
97 |
98 | $maxDate = new \DateTime();
99 | $maxDate->modify(\sprintf('-%d days', $daysOld));
100 |
101 | $this->io->note(\sprintf(
102 | 'Schedules with states ["%s"] lesser than %s days(s) (%s) will be purged.',
103 | \implode(',', $states),
104 | $daysOld,
105 | $maxDate->format('Y-m-d'),
106 | ));
107 |
108 | return $this->scheduledCommandRepository->findAllSinceXDaysWithState($maxDate, $states);
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Command/SynoliaSchedulerRunCommand.php:
--------------------------------------------------------------------------------
1 | addOption('id', 'i', InputOption::VALUE_OPTIONAL, 'Command ID');
47 | $this->addOption('only-one', 'o', InputOption::VALUE_NONE, 'Launch only one command');
48 | }
49 |
50 | protected function execute(InputInterface $input, OutputInterface $output): int
51 | {
52 | $io = new SymfonyStyle($input, $output);
53 |
54 | $scheduledCommandId = $input->getOption('id');
55 |
56 | if (null !== $scheduledCommandId) {
57 | /** @var ScheduledCommandInterface|null $scheduledCommand */
58 | $scheduledCommand = $this->scheduledCommandRepository->find((int) $scheduledCommandId);
59 |
60 | if (!$scheduledCommand instanceof ScheduledCommandInterface) {
61 | return Command::SUCCESS;
62 | }
63 |
64 | $this->executeCommand($scheduledCommand, $io);
65 |
66 | return Command::SUCCESS;
67 | }
68 |
69 | $commands = $this->getCommands($input);
70 |
71 | /** @var CommandInterface $command */
72 | foreach ($commands as $command) {
73 | // delayed execution just after, to keep cron comparison effective
74 | if ($this->shouldExecuteCommand($command, $io)) {
75 | $this->scheduledCommandPlanner->plan($command);
76 | // The execution is planned, and does not need to be launched again in the future.
77 | $command->setExecuteImmediately(false);
78 | }
79 | }
80 |
81 | if (!$this->lock()) {
82 | $output->writeln('The command is already running in another process.');
83 | $this->logger->info('Scheduler is already running.');
84 |
85 | return Command::SUCCESS;
86 | }
87 |
88 | /** @var ScheduledCommandInterface[] $scheduledCommands */
89 | $scheduledCommands = $this->scheduledCommandRepository->findAllRunnable();
90 |
91 | if (0 === \count($scheduledCommands)) {
92 | $io->success('Nothing to do.');
93 | }
94 |
95 | foreach ($scheduledCommands as $scheduledCommand) {
96 | $io->note(\sprintf(
97 | 'Execute Command "%s"',
98 | $scheduledCommand->getCommand(),
99 | ));
100 |
101 | try {
102 | $this->runScheduledCommand($io, $scheduledCommand);
103 | } catch (ConnectionLost) {
104 | $this->runScheduledCommand($io, $scheduledCommand);
105 | }
106 |
107 | if (true === $input->getOption('only-one')) {
108 | break;
109 | }
110 | }
111 |
112 | $this->release();
113 |
114 | return Command::SUCCESS;
115 | }
116 |
117 | private function runScheduledCommand(SymfonyStyle $io, ScheduledCommandInterface $scheduledCommand): void
118 | {
119 | /** prevent update during running time */
120 | $this->scheduledCommandRepository->find($scheduledCommand->getId());
121 |
122 | $this->executeCommand($scheduledCommand, $io);
123 | }
124 |
125 | private function executeCommand(ScheduledCommandInterface $scheduledCommand, SymfonyStyle $io): void
126 | {
127 | try {
128 | /** @var Application $application */
129 | $application = $this->getApplication();
130 | $command = $application->find($scheduledCommand->getCommand());
131 | } catch (\InvalidArgumentException $e) {
132 | $scheduledCommand->setLastReturnCode(-1);
133 | //persist last return code
134 | $this->entityManager->flush();
135 | $io->error('Cannot find ' . $scheduledCommand->getCommand());
136 |
137 | return;
138 | }
139 |
140 | // Execute command and get return code
141 | try {
142 | $io->writeln(
143 | 'Execute : ' . $scheduledCommand->getCommand()
144 | . ' ' . $scheduledCommand->getArguments() . '',
145 | );
146 |
147 | $scheduledCommand->setExecutedAt(new \DateTime());
148 | $this->changeState($scheduledCommand, ScheduledCommandStateEnum::IN_PROGRESS);
149 | $result = $this->scheduleCommandRunner->runFromCron($scheduledCommand);
150 |
151 | try {
152 | $this->changeState($scheduledCommand, $this->getStateForResult($result));
153 | } catch (ConnectionLost) {
154 | $this->changeState($scheduledCommand, $this->getStateForResult($result));
155 | }
156 | } catch (\Exception $e) {
157 | $this->changeState($scheduledCommand, ScheduledCommandStateEnum::ERROR);
158 | $io->warning($e->getMessage());
159 | $result = -1;
160 | }
161 |
162 | $scheduledCommand->setLastReturnCode($result);
163 | $this->entityManager->flush();
164 |
165 | /*
166 | * This clear() is necessary to avoid conflict between commands
167 | * and to be sure that none entity are managed before entering in a new command
168 | */
169 | $this->entityManager->clear();
170 |
171 | unset($command);
172 | gc_collect_cycles();
173 | }
174 |
175 | private function getCommands(InputInterface $input): iterable
176 | {
177 | $commands = $this->commandRepository->findEnabledCommand();
178 | if ($input->getOption('id') !== null) {
179 | $commands = $this->scheduledCommandRepository->findBy(['id' => $input->getOption('id')]);
180 | }
181 |
182 | return $commands;
183 | }
184 |
185 | private function shouldExecuteCommand(CommandInterface $command, SymfonyStyle $io): bool
186 | {
187 | if ($command->isExecuteImmediately()) {
188 | $io->note('Immediately execution asked for : ' . $command->getCommand());
189 |
190 | return true;
191 | }
192 |
193 | // Could be removed as getCommands fetch only enabled commands
194 | if (!$command->isEnabled()) {
195 | return false;
196 | }
197 |
198 | return $this->isDueVoter->isDue($command);
199 | }
200 |
201 | private function changeState(ScheduledCommandInterface $scheduledCommand, string $state): void
202 | {
203 | $scheduledCommand->setState($state);
204 | $this->entityManager->flush();
205 | }
206 |
207 | private function getStateForResult(int $returnResultCode): string
208 | {
209 | if ($returnResultCode === 143) {
210 | return ScheduledCommandStateEnum::TERMINATION;
211 | }
212 |
213 | if ($returnResultCode !== 0) {
214 | return ScheduledCommandStateEnum::ERROR;
215 | }
216 |
217 | return ScheduledCommandStateEnum::FINISHED;
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/src/Components/Exceptions/Checker/IsNotDueException.php:
--------------------------------------------------------------------------------
1 | ['permission' => true]], methods: ['GET'])]
23 | public function downloadLogFile(string $command): Response
24 | {
25 | $scheduleCommand = $this->scheduledCommandRepository->find($command);
26 |
27 | if (
28 | null === $scheduleCommand ||
29 | null === $scheduleCommand->getLogFile()
30 | ) {
31 | return new Response('', Response::HTTP_NOT_FOUND);
32 | }
33 |
34 | $filePath = $this->logsDir . \DIRECTORY_SEPARATOR . $scheduleCommand->getLogFile();
35 | if (!\file_exists($filePath)) {
36 | return new Response('', Response::HTTP_NOT_FOUND);
37 | }
38 |
39 | return $this->file($filePath);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Controller/EmptyLogsController.php:
--------------------------------------------------------------------------------
1 | ['permission' => true, 'repository' => ['method' => 'findById', 'arguments' => ['$ids']]]], methods: ['GET|PUT'])]
26 | public function emptyLogs(Request $request): Response
27 | {
28 | $commandIds = $request->get('ids');
29 |
30 | foreach ($commandIds as $commandId) {
31 | $command = $this->scheduledCommandRepository->find($commandId);
32 | if ($command !== null && $command->getLogFile() !== null) {
33 | @\file_put_contents($this->logsDir . \DIRECTORY_SEPARATOR . $command->getLogFile(), '');
34 | }
35 | }
36 |
37 | $this->addFlash('success', $this->translator->trans('sylius.ui.scheduled_command.bulk_empty_logs'));
38 |
39 | return $this->redirectToRoute(
40 | 'synolia_admin_command_index',
41 | [],
42 | Response::HTTP_MOVED_PERMANENTLY,
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Controller/LogViewerController.php:
--------------------------------------------------------------------------------
1 | ['permission' => true]], methods: ['GET'])]
31 | public function getLogs(Request $request, string $command): JsonResponse
32 | {
33 | $scheduleCommand = $this->scheduledCommandRepository->find($command);
34 |
35 | if (
36 | null === $scheduleCommand ||
37 | null === $scheduleCommand->getLogFile()
38 | ) {
39 | return new JsonResponse('', Response::HTTP_NO_CONTENT);
40 | }
41 |
42 | if ((bool) $request->get('refresh')) {
43 | $result = $this->logDataRetriever->getLog(
44 | $this->logsDir . \DIRECTORY_SEPARATOR . $scheduleCommand->getLogFile(),
45 | (int) $request->get('lastsize'),
46 | (string) $request->get('grep-keywords'),
47 | (bool) $request->get('invert'),
48 | );
49 |
50 | return new JsonResponse([
51 | 'size' => $result['size'],
52 | 'data' => $result['data'],
53 | ]);
54 | }
55 |
56 | $result = $this->logDataRetriever->getLog($this->logsDir . \DIRECTORY_SEPARATOR . $scheduleCommand->getLogFile());
57 |
58 | return new JsonResponse([
59 | 'size' => $result['size'],
60 | 'data' => $result['data'],
61 | ]);
62 | }
63 |
64 | #[Route('/scheduled-commands/{command}/view-log', name: 'sylius_admin_scheduler_view_log_file', defaults: ['_sylius' => ['permission' => true]], methods: ['GET'])]
65 | public function show(string $command): Response
66 | {
67 | $scheduledCommand = $this->scheduledCommandRepository->find($command);
68 |
69 | if (
70 | null === $scheduledCommand ||
71 | null === $scheduledCommand->getLogFile()
72 | ) {
73 | $this->addFlash('error', $this->translator->trans('sylius.ui.does_not_exists_or_missing_log_file'));
74 |
75 | return $this->redirectToRoute('synolia_admin_command_index');
76 | }
77 |
78 | return $this->render('@SynoliaSyliusSchedulerCommandPlugin/Controller/show.html.twig', [
79 | 'route' => $this->generateUrl('sylius_admin_scheduler_get_log_file', ['command' => $command]),
80 | 'updateTime' => $this->updateTime,
81 | 'scheduledCommand' => $scheduledCommand,
82 | ]);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Controller/ScheduledCommandExecuteImmediateController.php:
--------------------------------------------------------------------------------
1 | ['permission' => true]], methods: ['GET|PUT'])]
31 | public function executeImmediate(Request $request, string $commandId): Response
32 | {
33 | $command = $this->commandRepository->find($commandId);
34 | Assert::isInstanceOf($command, CommandInterface::class);
35 |
36 | $scheduledCommand = $this->scheduledCommandPlanner->plan($command);
37 |
38 | $this->executeFromCron($scheduledCommand);
39 |
40 | /** @var Session $session */
41 | $session = $request->getSession();
42 | $session->getFlashBag()->add('success', \sprintf(
43 | 'Command "%s" as been planned for execution.',
44 | $scheduledCommand->getName(),
45 | ));
46 |
47 | return $this->redirectToRoute('synolia_admin_command_index');
48 | }
49 |
50 | public function executeFromCron(ScheduledCommandInterface $scheduledCommand): int
51 | {
52 | $process = Process::fromShellCommandline($this->getCommandLine($scheduledCommand));
53 | $process->setTimeout($scheduledCommand->getTimeout());
54 | $process->setIdleTimeout($scheduledCommand->getIdleTimeout());
55 | $process->run();
56 | $result = $process->getExitCode();
57 | $scheduledCommand->setCommandEndTime(new \DateTime());
58 |
59 | if (null === $result) {
60 | $result = 0;
61 | }
62 |
63 | return $result;
64 | }
65 |
66 | private function getCommandLine(ScheduledCommandInterface $scheduledCommand): string
67 | {
68 | return sprintf(
69 | '%s/bin/console synolia:scheduler-run --id=%d > /dev/null 2>&1 &',
70 | $this->projectDir,
71 | $scheduledCommand->getId(),
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/DataRetriever/LogDataRetriever.php:
--------------------------------------------------------------------------------
1 | $this->maxSizeToLoad) {
34 | $maxLength = ($this->maxSizeToLoad / 2);
35 | }
36 |
37 | /**
38 | * Actually load the data
39 | */
40 | $data = [];
41 | if ($maxLength > 0) {
42 | $filePointer = \fopen($filePath, 'rb');
43 |
44 | if (false === $filePointer) {
45 | throw new FileNotFoundException('Could not load data for the log file.');
46 | }
47 |
48 | \fseek($filePointer, (int) -$maxLength, \SEEK_END);
49 | $data = \explode("\n", (string) \fread($filePointer, $maxLength));
50 | }
51 | $data = $this->grepData($data, $grepKeyword, $invertGrep);
52 |
53 | /**
54 | * If the last entry in the array is an empty string lets remove it.
55 | */
56 | if (\end($data) === '') {
57 | \array_pop($data);
58 | }
59 |
60 | return [
61 | 'size' => $filesize,
62 | 'file' => $filePath,
63 | 'data' => $data,
64 | ];
65 | }
66 |
67 | /**
68 | * Run the grep function to return only the lines we're interested in.
69 | */
70 | private function grepData(array $data, string $grepKeyword, bool $invertGrep): array
71 | {
72 | $lines = preg_grep("/$grepKeyword/", $data, $invertGrep ? \PREG_GREP_INVERT : 0);
73 |
74 | return \is_array($lines) ? $lines : [];
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/DependencyInjection/SynoliaSyliusSchedulerCommandExtension.php:
--------------------------------------------------------------------------------
1 | load('services.yaml');
25 | }
26 |
27 | /**
28 | * @inheritdoc
29 | */
30 | public function prepend(ContainerBuilder $container): void
31 | {
32 | $this->prependDoctrineMigrations($container);
33 | }
34 |
35 | protected function getMigrationsNamespace(): string
36 | {
37 | return 'Synolia\SyliusSchedulerCommandPlugin\Migrations';
38 | }
39 |
40 | protected function getMigrationsDirectory(): string
41 | {
42 | return '@SynoliaSyliusSchedulerCommandPlugin/migrations';
43 | }
44 |
45 | protected function getNamespacesOfMigrationsExecutedBefore(): array
46 | {
47 | return [];
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/DoctrineEvent/ScheduledCommandPostRemoveEvent.php:
--------------------------------------------------------------------------------
1 | 'doctrine.event_subscriber', 'connection' => 'default'])]
15 | class ScheduledCommandPostRemoveEvent implements EventSubscriber
16 | {
17 | public function __construct(
18 | #[Autowire(param: 'kernel.logs_dir')]
19 | private readonly string $logsDir,
20 | ) {
21 | }
22 |
23 | public function getSubscribedEvents(): array
24 | {
25 | return [
26 | Events::postRemove,
27 | ];
28 | }
29 |
30 | public function postRemove(LifecycleEventArgs $eventArgs): void
31 | {
32 | $scheduledCommand = $eventArgs->getEntity();
33 |
34 | if (!$scheduledCommand instanceof ScheduledCommandInterface) {
35 | return;
36 | }
37 |
38 | if (null === $scheduledCommand->getLogFile() || '' === $scheduledCommand->getLogFile()) {
39 | return;
40 | }
41 |
42 | $filePath = $this->logsDir . \DIRECTORY_SEPARATOR . $scheduledCommand->getLogFile();
43 |
44 | if (!\file_exists($filePath)) {
45 | return;
46 | }
47 |
48 | @unlink($filePath);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Entity/Command.php:
--------------------------------------------------------------------------------
1 |
90 | *
91 | * @ORM\OneToMany(targetEntity="Synolia\SyliusSchedulerCommandPlugin\Entity\ScheduledCommandInterface", mappedBy="owner")
92 | */
93 | #[ORM\OneToMany(mappedBy: 'owner', targetEntity: ScheduledCommandInterface::class)]
94 | private Collection $scheduledCommands;
95 |
96 | public function __construct()
97 | {
98 | $this->scheduledCommands = new ArrayCollection();
99 | }
100 |
101 | public function getId(): ?int
102 | {
103 | return $this->id;
104 | }
105 |
106 | public function getName(): string
107 | {
108 | return $this->name;
109 | }
110 |
111 | public function setName(string $name): self
112 | {
113 | $this->name = $name;
114 |
115 | return $this;
116 | }
117 |
118 | public function getCommand(): string
119 | {
120 | return $this->command;
121 | }
122 |
123 | public function setCommand(string $command): self
124 | {
125 | $this->command = $command;
126 |
127 | return $this;
128 | }
129 |
130 | public function getArguments(): ?string
131 | {
132 | return $this->arguments;
133 | }
134 |
135 | public function setArguments(?string $arguments): self
136 | {
137 | $this->arguments = $arguments;
138 |
139 | return $this;
140 | }
141 |
142 | public function getCronExpression(): string
143 | {
144 | return $this->cronExpression;
145 | }
146 |
147 | public function setCronExpression(string $cronExpression): self
148 | {
149 | $this->cronExpression = $cronExpression;
150 |
151 | return $this;
152 | }
153 |
154 | public function getLogFilePrefix(): ?string
155 | {
156 | return $this->logFilePrefix;
157 | }
158 |
159 | public function setLogFilePrefix(?string $logFilePrefix): self
160 | {
161 | $this->logFilePrefix = $logFilePrefix;
162 |
163 | return $this;
164 | }
165 |
166 | public function getPriority(): int
167 | {
168 | return $this->priority;
169 | }
170 |
171 | public function setPriority(int $priority): self
172 | {
173 | $this->priority = $priority;
174 |
175 | return $this;
176 | }
177 |
178 | public function isExecuteImmediately(): bool
179 | {
180 | return $this->executeImmediately;
181 | }
182 |
183 | public function setExecuteImmediately(bool $executeImmediately): self
184 | {
185 | $this->executeImmediately = $executeImmediately;
186 |
187 | return $this;
188 | }
189 |
190 | public function isEnabled(): bool
191 | {
192 | return $this->enabled;
193 | }
194 |
195 | public function setEnabled(bool $enabled): self
196 | {
197 | $this->enabled = $enabled;
198 |
199 | return $this;
200 | }
201 |
202 | public function getScheduledCommands(): Collection
203 | {
204 | return $this->scheduledCommands;
205 | }
206 |
207 | public function addScheduledCommand(ScheduledCommandInterface $scheduledCommand): self
208 | {
209 | if ($this->scheduledCommands->contains($scheduledCommand)) {
210 | return $this;
211 | }
212 |
213 | $this->scheduledCommands->add($scheduledCommand);
214 |
215 | return $this;
216 | }
217 |
218 | public function removeScheduledCommand(ScheduledCommandInterface $scheduledCommand): self
219 | {
220 | if (!$this->scheduledCommands->contains($scheduledCommand)) {
221 | return $this;
222 | }
223 |
224 | $this->scheduledCommands->removeElement($scheduledCommand);
225 | // needed to update the owning side of the relationship!
226 | $scheduledCommand->setOwner(null);
227 |
228 | return $this;
229 | }
230 |
231 | public function getTimeout(): ?int
232 | {
233 | return $this->timeout;
234 | }
235 |
236 | public function setTimeout(?int $timeout): CommandInterface
237 | {
238 | $this->timeout = $timeout;
239 |
240 | return $this;
241 | }
242 |
243 | public function getIdleTimeout(): ?int
244 | {
245 | return $this->idleTimeout;
246 | }
247 |
248 | public function setIdleTimeout(?int $idleTimeout): CommandInterface
249 | {
250 | $this->idleTimeout = $idleTimeout;
251 |
252 | return $this;
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/src/Entity/CommandInterface.php:
--------------------------------------------------------------------------------
1 | createdAt = new \DateTime();
95 | }
96 |
97 | public function getId(): ?int
98 | {
99 | return $this->id;
100 | }
101 |
102 | public function getName(): string
103 | {
104 | return $this->name;
105 | }
106 |
107 | public function setName(string $name): ScheduledCommandInterface
108 | {
109 | $this->name = $name;
110 |
111 | return $this;
112 | }
113 |
114 | public function getCommand(): string
115 | {
116 | return $this->command;
117 | }
118 |
119 | public function setCommand(string $command): ScheduledCommandInterface
120 | {
121 | $this->command = $command;
122 |
123 | return $this;
124 | }
125 |
126 | public function getArguments(): ?string
127 | {
128 | return $this->arguments;
129 | }
130 |
131 | public function setArguments(?string $arguments): ScheduledCommandInterface
132 | {
133 | $this->arguments = $arguments;
134 |
135 | return $this;
136 | }
137 |
138 | public function getExecutedAt(): ?\DateTime
139 | {
140 | return $this->executedAt;
141 | }
142 |
143 | public function setExecutedAt(?\DateTime $executedAt): ScheduledCommandInterface
144 | {
145 | $this->executedAt = $executedAt;
146 |
147 | return $this;
148 | }
149 |
150 | public function getLastReturnCode(): ?int
151 | {
152 | return $this->lastReturnCode;
153 | }
154 |
155 | public function setLastReturnCode(?int $lastReturnCode): ScheduledCommandInterface
156 | {
157 | $this->lastReturnCode = $lastReturnCode;
158 |
159 | return $this;
160 | }
161 |
162 | public function getLogFile(): ?string
163 | {
164 | return $this->logFile;
165 | }
166 |
167 | public function setLogFile(?string $logFile): ScheduledCommandInterface
168 | {
169 | $this->logFile = $logFile;
170 |
171 | return $this;
172 | }
173 |
174 | public function getCommandEndTime(): ?\DateTime
175 | {
176 | return $this->commandEndTime;
177 | }
178 |
179 | public function setCommandEndTime(?\DateTime $commandEndTime): ScheduledCommandInterface
180 | {
181 | $this->commandEndTime = $commandEndTime;
182 |
183 | return $this;
184 | }
185 |
186 | public function getCreatedAt(): \DateTime
187 | {
188 | return $this->createdAt;
189 | }
190 |
191 | public function setState(string $state): self
192 | {
193 | $this->state = $state;
194 |
195 | return $this;
196 | }
197 |
198 | public function getState(): string
199 | {
200 | return $this->state;
201 | }
202 |
203 | public function getOwner(): ?CommandInterface
204 | {
205 | return $this->owner;
206 | }
207 |
208 | public function setOwner(?CommandInterface $owner): self
209 | {
210 | $this->owner = $owner;
211 |
212 | return $this;
213 | }
214 |
215 | public function getTimeout(): ?int
216 | {
217 | return $this->timeout;
218 | }
219 |
220 | public function setTimeout(?int $timeout): ScheduledCommandInterface
221 | {
222 | $this->timeout = $timeout;
223 |
224 | return $this;
225 | }
226 |
227 | public function getIdleTimeout(): ?int
228 | {
229 | return $this->idleTimeout;
230 | }
231 |
232 | public function setIdleTimeout(?int $idleTimeout): ScheduledCommandInterface
233 | {
234 | $this->idleTimeout = $idleTimeout;
235 |
236 | return $this;
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/src/Entity/ScheduledCommandInterface.php:
--------------------------------------------------------------------------------
1 | 'onConsoleTerminate',
27 | ConsoleEvents::SIGNAL => 'onConsoleSignal',
28 | ];
29 | }
30 |
31 | public function onConsoleTerminate(ConsoleTerminateEvent $event): void
32 | {
33 | $this->updateCommand($event);
34 | }
35 |
36 | public function onConsoleSignal(ConsoleSignalEvent $event): void
37 | {
38 | $this->updateCommand($event);
39 | }
40 |
41 | private function updateCommand(ConsoleSignalEvent|ConsoleTerminateEvent $event): void
42 | {
43 | try {
44 | $commandCode = $event->getCommand()?->getName() ?? 'no_command';
45 | /** @var ScheduledCommand|null $schedulerCommand */
46 | $schedulerCommand = $this->scheduledCommandRepository->findOneBy(['command' => $commandCode], ['id' => 'DESC']);
47 | } catch (Exception) {
48 | return;
49 | }
50 |
51 | if (null === $schedulerCommand) {
52 | return;
53 | }
54 |
55 | if ($schedulerCommand->getState() !== ScheduledCommandStateEnum::IN_PROGRESS) {
56 | return;
57 | }
58 |
59 | $exitCode = $event->getExitCode();
60 | if (false === $exitCode) {
61 | $exitCode = -1;
62 | }
63 |
64 | $schedulerCommand->setCommandEndTime(new \DateTime());
65 | $schedulerCommand->setState(ScheduledCommandStateEnum::TERMINATION);
66 | $schedulerCommand->setLastReturnCode($exitCode);
67 | $this->scheduledCommandRepository->add($schedulerCommand);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Fixture/SchedulerCommandFixture.php:
--------------------------------------------------------------------------------
1 | commandFactory->createNew();
33 | $command
34 | ->setName($commandArray['name'])
35 | ->setCommand($commandArray['command'])
36 | ->setArguments($commandArray['arguments'])
37 | ->setCronExpression($commandArray['cronExpression'])
38 | ->setLogFilePrefix($commandArray['logFilePrefix'])
39 | ->setPriority($commandArray['priority'])
40 | ->setTimeout($commandArray['timeout'] ?? null)
41 | ->setIdleTimeout($commandArray['idle_timeout'] ?? null)
42 | ->setExecuteImmediately($commandArray['executeImmediately'])
43 | ->setEnabled($commandArray['enabled'])
44 | ;
45 | $this->commandRepository->add($command);
46 | }
47 | }
48 |
49 | /**
50 | * @inheritdoc
51 | */
52 | public function getName(): string
53 | {
54 | return 'scheduler_command';
55 | }
56 |
57 | protected function configureOptionsNode(ArrayNodeDefinition $optionsNode): void
58 | {
59 | $optionsNode
60 | ->children()
61 | ->arrayNode('scheduled_commands')->arrayPrototype()->children()
62 | ->scalarNode('name')->isRequired()->end()
63 | ->scalarNode('command')->isRequired()->end()
64 | ->scalarNode('arguments')->defaultValue('')->end()
65 | ->scalarNode('cronExpression')->isRequired()->end()
66 | ->scalarNode('logFilePrefix')->defaultValue('')->end()
67 | ->integerNode('priority')->isRequired()->end()
68 | ->integerNode('timeout')->defaultNull()->end()
69 | ->integerNode('idle_timeout')->defaultNull()->end()
70 | ->booleanNode('executeImmediately')->defaultFalse()->end()
71 | ->booleanNode('enabled')->defaultTrue()->end()
72 | ->end()
73 | ->end()
74 | ;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/Form/CommandChoiceType.php:
--------------------------------------------------------------------------------
1 | setDefaults(
21 | [
22 | 'choices' => $this->commandParser->getCommands(),
23 | ],
24 | );
25 | }
26 |
27 | public function getParent(): string
28 | {
29 | return ChoiceType::class;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Form/CommandType.php:
--------------------------------------------------------------------------------
1 | add('name', TextType::class, [
24 | 'empty_data' => '',
25 | ])
26 | ->add('command', CommandChoiceType::class)
27 | ->add('arguments')
28 | ->add('cronExpression')
29 | ->add('logFilePrefix')
30 | ->add('priority', IntegerType::class, [
31 | 'empty_data' => 0,
32 | ])
33 | ->add('timeout', IntegerType::class, [
34 | 'required' => false,
35 | 'constraints' => [
36 | new Constraints\PositiveOrZero(),
37 | ],
38 | 'attr' => [
39 | 'min' => 0,
40 | ],
41 | ])
42 | ->add('idle_timeout', IntegerType::class, [
43 | 'required' => false,
44 | 'constraints' => [
45 | new Constraints\PositiveOrZero(),
46 | ],
47 | 'attr' => [
48 | 'min' => 0,
49 | ],
50 | ])
51 | ->add('executeImmediately')
52 | ->add('enabled')
53 | ;
54 | }
55 |
56 | public function configureOptions(OptionsResolver $resolver): void
57 | {
58 | $resolver->setDefaults([
59 | 'data_class' => CommandInterface::class,
60 | ]);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Form/ScheduledCommandType.php:
--------------------------------------------------------------------------------
1 | add('name', TextType::class, [
24 | 'empty_data' => '',
25 | ])
26 | ->add('command', CommandChoiceType::class)
27 | ->add('arguments')
28 | ->add('timeout', IntegerType::class, [
29 | 'required' => false,
30 | 'constraints' => [
31 | new Constraints\PositiveOrZero(),
32 | ],
33 | 'attr' => [
34 | 'min' => 0,
35 | ],
36 | ])
37 | ->add('idle_timeout', IntegerType::class, [
38 | 'required' => false,
39 | 'constraints' => [
40 | new Constraints\PositiveOrZero(),
41 | ],
42 | 'attr' => [
43 | 'min' => 0,
44 | ],
45 | ])
46 | ->add('logFile')
47 | ;
48 | }
49 |
50 | public function configureOptions(OptionsResolver $resolver): void
51 | {
52 | $resolver->setDefaults([
53 | 'data_class' => ScheduledCommandInterface::class,
54 | ]);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Grid/FieldType/DatetimeFieldType.php:
--------------------------------------------------------------------------------
1 | 'scheduled_command_executed_at'])]
16 | final readonly class DatetimeFieldType implements FieldTypeInterface
17 | {
18 | public function __construct(
19 | #[Autowire('@sylius.grid.data_extractor.property_access')]
20 | private DataExtractorInterface $dataExtractor,
21 | private LocaleContextInterface $localeContext,
22 | ) {
23 | }
24 |
25 | /**
26 | * @inheritDoc
27 | */
28 | public function render(Field $field, $data, array $options): string
29 | {
30 | $value = $this->dataExtractor->get($field, $data);
31 | if (!$value instanceof \DateTimeInterface) {
32 | return '';
33 | }
34 |
35 | /** @var \IntlDateFormatter|null $fmt */
36 | $fmt = \datefmt_create($this->localeContext->getLocaleCode(), $options['date_format'], $options['time_format']);
37 |
38 | if (!$fmt instanceof \IntlDateFormatter) {
39 | return '';
40 | }
41 |
42 | /** @phpstan-ignore-next-line */
43 | return $fmt->format($value) ?: '';
44 | }
45 |
46 | public function configureOptions(OptionsResolver $resolver): void
47 | {
48 | $resolver->setDefaults([
49 | 'format' => 'Y-m-d H:i:s',
50 | 'date_format' => \IntlDateFormatter::SHORT,
51 | 'time_format' => \IntlDateFormatter::SHORT,
52 | ]);
53 | $resolver->setAllowedTypes('format', 'string');
54 | $resolver->setAllowedTypes('date_format', 'integer');
55 | $resolver->setAllowedTypes('time_format', 'integer');
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Grid/FieldType/ScheduledCommandExecutionTimeType.php:
--------------------------------------------------------------------------------
1 | 'scheduled_command_execution_time'])]
13 | class ScheduledCommandExecutionTimeType implements FieldTypeInterface
14 | {
15 | private const HOUR_IN_SECONDES = 3600;
16 |
17 | private const MINUTE_IN_SECONDES = 60;
18 |
19 | /**
20 | * @inheritdoc
21 | */
22 | public function render(Field $field, $scheduleCommand, array $options): string
23 | {
24 | if ($scheduleCommand->getExecutedAt() === null) {
25 | return '';
26 | }
27 |
28 | $baseDateTime = $scheduleCommand->getCommandEndTime() ?? new \DateTime();
29 | $time = $baseDateTime->getTimestamp() - $scheduleCommand->getExecutedAt()->getTimestamp();
30 |
31 | if ($time > self::HOUR_IN_SECONDES) {
32 | $hours = (int) ($time / self::HOUR_IN_SECONDES) . 'h ';
33 | $minutes = (int) (($time % self::HOUR_IN_SECONDES) / self::MINUTE_IN_SECONDES) . 'm ';
34 | $seconds = (($time % self::HOUR_IN_SECONDES) % self::MINUTE_IN_SECONDES) . 's';
35 |
36 | return $hours . $minutes . $seconds;
37 | }
38 |
39 | if ($time > self::MINUTE_IN_SECONDES) {
40 | $minutes = (int) ($time / self::MINUTE_IN_SECONDES) . 'm ';
41 | $seconds = (int) $time % self::MINUTE_IN_SECONDES . 's';
42 |
43 | return $minutes . $seconds;
44 | }
45 |
46 | return (int) $time . 's';
47 | }
48 |
49 | /** @inheritdoc */
50 | public function configureOptions(OptionsResolver $resolver): void
51 | {
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Grid/FieldType/ScheduledCommandHumanReadableExpressionType.php:
--------------------------------------------------------------------------------
1 | 'scheduled_human_readable_expression'])]
15 | final readonly class ScheduledCommandHumanReadableExpressionType implements FieldTypeInterface
16 | {
17 | public function __construct(private Environment $twig, private HumanizerInterface $humanizer)
18 | {
19 | }
20 |
21 | /**
22 | * @inheritdoc
23 | */
24 | public function render(Field $field, $scheduleCommand, array $options): string
25 | {
26 | return $this->twig->render(
27 | $options['template'],
28 | [
29 | 'schedulerCommand' => $scheduleCommand,
30 | 'value' => $this->humanizer->humanize($scheduleCommand->getCronExpression()),
31 | ],
32 | );
33 | }
34 |
35 | /** @inheritdoc */
36 | public function configureOptions(OptionsResolver $resolver): void
37 | {
38 | $resolver->setRequired('template');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Grid/FieldType/ScheduledCommandStateType.php:
--------------------------------------------------------------------------------
1 | 'scheduled_command_state'])]
14 | final readonly class ScheduledCommandStateType implements FieldTypeInterface
15 | {
16 | public function __construct(private Environment $twig)
17 | {
18 | }
19 |
20 | /**
21 | * @inheritdoc
22 | */
23 | public function render(Field $field, $scheduleCommand, array $options): string
24 | {
25 | return $this->twig->render($options['template'], [
26 | 'schedulerCommand' => $scheduleCommand,
27 | ]);
28 | }
29 |
30 | /** @inheritdoc */
31 | public function configureOptions(OptionsResolver $resolver): void
32 | {
33 | $resolver->setRequired('template');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Grid/FieldType/ScheduledCommandUrlType.php:
--------------------------------------------------------------------------------
1 | 'scheduled_command_url'])]
16 | final readonly class ScheduledCommandUrlType implements FieldTypeInterface
17 | {
18 | public function __construct(
19 | private UrlGeneratorInterface $urlGenerator,
20 | private Environment $twig,
21 | #[Autowire(env: 'string:SYNOLIA_SCHEDULER_PLUGIN_LOGS_DIR')]
22 | private string $logsDir,
23 | ) {
24 | }
25 |
26 | /**
27 | * @inheritdoc
28 | */
29 | public function render(Field $field, $scheduleCommand, array $options): string
30 | {
31 | $size = 0;
32 |
33 | $viewUrl = $this->urlGenerator->generate(
34 | 'sylius_admin_scheduler_view_log_file',
35 | [
36 | 'command' => $scheduleCommand->getId(),
37 | ],
38 | );
39 |
40 | $url = $this->urlGenerator->generate(
41 | 'download_schedule_log_file',
42 | [
43 | 'command' => $scheduleCommand->getId(),
44 | ],
45 | );
46 |
47 | $filePath = $this->logsDir . \DIRECTORY_SEPARATOR . $scheduleCommand->getLogFile();
48 | if (\file_exists($filePath)) {
49 | $size = filesize($filePath);
50 | }
51 |
52 | return $this->twig->render(
53 | $options['template'],
54 | [
55 | 'schedulerCommand' => $scheduleCommand,
56 | 'url' => $url,
57 | 'viewUrl' => $viewUrl,
58 | 'size' => $size,
59 | ],
60 | );
61 | }
62 |
63 | /** @inheritdoc */
64 | public function configureOptions(OptionsResolver $resolver): void
65 | {
66 | $resolver->setRequired('template');
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Humanizer/CronExpressionHumanizer.php:
--------------------------------------------------------------------------------
1 | getLocale();
28 |
29 | try {
30 | return CronTranslator::translate($expression, $locale, $this->timeFormat24Hours);
31 | } catch (\Throwable) {
32 | return $expression;
33 | }
34 | }
35 |
36 | private function getLocale(): string
37 | {
38 | try {
39 | $locale = $this->localeContext->getLocaleCode();
40 |
41 | if (\strlen($locale) === 2) {
42 | return $locale;
43 | }
44 |
45 | return mb_substr($locale, 0, 2);
46 | } catch (LocaleNotFoundException) {
47 | return 'en';
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Humanizer/HumanizerInterface.php:
--------------------------------------------------------------------------------
1 | getGrid();
18 |
19 | if (!$grid->hasActionGroup('main')) {
20 | $grid->addActionGroup(ActionGroup::named('main'));
21 | }
22 |
23 | $actionGroup = $grid->getActionGroup('main');
24 |
25 | if ($actionGroup->hasAction('go_to_commands')) {
26 | return;
27 | }
28 |
29 | $action = Action::fromNameAndType('go_to_commands', 'link');
30 | $action->setLabel('synolia.menu.admin.main.configuration.scheduler_command');
31 | $action->setOptions([
32 | 'class' => 'btn-ghost-azure',
33 | 'icon' => 'tabler:list',
34 | 'link' => [
35 | 'route' => 'synolia_admin_command_index',
36 | 'parameters' => [],
37 | ],
38 | ]);
39 |
40 | $actionGroup->addAction($action);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Listener/Grid/GoToHistoryButtonGridListener.php:
--------------------------------------------------------------------------------
1 | getGrid();
18 |
19 | if (!$grid->hasActionGroup('main')) {
20 | $grid->addActionGroup(ActionGroup::named('main'));
21 | }
22 |
23 | $actionGroup = $grid->getActionGroup('main');
24 |
25 | if ($actionGroup->hasAction('go_to_history')) {
26 | return;
27 | }
28 |
29 | $action = Action::fromNameAndType('go_to_history', 'link');
30 | $action->setLabel('synolia.menu.admin.main.configuration.scheduler_command_history');
31 | $action->setOptions([
32 | 'class' => 'btn-ghost-azure',
33 | 'icon' => 'tabler:clock',
34 | 'link' => [
35 | 'route' => 'synolia_admin_scheduled_command_index',
36 | 'parameters' => [],
37 | ],
38 | ]);
39 |
40 | $actionGroup->addAction($action);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Menu/AdminMenuListener.php:
--------------------------------------------------------------------------------
1 | getMenu();
16 | $newSubmenu = $menu
17 | ->addChild('scheduler')
18 | ->setLabel('synolia.menu.admin.main.configuration.scheduler_command')
19 | ->setLabelAttribute('icon', 'tabler:list')
20 | ;
21 | $newSubmenu
22 | ->addChild('scheduler-command', [
23 | 'route' => 'synolia_admin_command_index',
24 | ])
25 | ->setAttribute('type', 'link')
26 | ->setLabel('synolia.menu.admin.main.configuration.scheduler_command')
27 | ;
28 | $newSubmenu
29 | ->addChild('scheduler-command-history', [
30 | 'route' => 'synolia_admin_scheduled_command_index',
31 | ])
32 | ->setAttribute('type', 'link')
33 | ->setLabel('synolia.menu.admin.main.configuration.scheduler_command_history')
34 | ;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Parser/CommandParser.php:
--------------------------------------------------------------------------------
1 | excludedNamespaces = $excludedNamespaces;
24 | }
25 |
26 | public function getCommands(): array
27 | {
28 | $application = new Application($this->kernel);
29 |
30 | $commandsList = [];
31 | $commands = $application->all();
32 | foreach ($commands as $command) {
33 | $name = $command->getName() ?? '';
34 | $namespace = \explode(':', $name)[0];
35 | if (in_array($namespace, $this->excludedNamespaces, true)) {
36 | continue;
37 | }
38 | $commandsList[$namespace][$command->getName()] = $name;
39 | }
40 |
41 | return $commandsList;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Parser/CommandParserInterface.php:
--------------------------------------------------------------------------------
1 | scheduledCommandRepository->findBy([
29 | 'command' => $command->getCommand(),
30 | 'state' => ScheduledCommandStateEnum::WAITING,
31 | ]);
32 |
33 | if (0 !== count($scheduledCommands)) {
34 | return array_shift($scheduledCommands);
35 | }
36 |
37 | /** @var ScheduledCommandInterface $scheduledCommand */
38 | $scheduledCommand = $this->scheduledCommandFactory->createNew();
39 |
40 | $scheduledCommand
41 | ->setName($command->getName())
42 | ->setCommand($command->getCommand())
43 | ->setArguments($command->getArguments())
44 | ->setTimeout($command->getTimeout())
45 | ->setIdleTimeout($command->getIdleTimeout())
46 | ->setOwner($command)
47 | ;
48 |
49 | if (null !== $command->getLogFilePrefix() && '' !== $command->getLogFilePrefix()) {
50 | $scheduledCommand->setLogFile(\sprintf(
51 | '%s-%s-%s.log',
52 | $command->getLogFilePrefix(),
53 | (new \DateTime())->format('Y-m-d'),
54 | \uniqid(),
55 | ));
56 | }
57 |
58 | $this->entityManager->persist($scheduledCommand);
59 | $this->entityManager->flush();
60 |
61 | $this->logger->info('Command has been planned for execution.', ['command_name' => $command->getName()]);
62 |
63 | return $scheduledCommand;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Planner/ScheduledCommandPlannerInterface.php:
--------------------------------------------------------------------------------
1 | timezone ?? date_default_timezone_get();
29 |
30 | return new DateTimeImmutable(timezone: new DateTimeZone($timezone));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Repository/CommandRepository.php:
--------------------------------------------------------------------------------
1 | $commands */
18 | $commands = $this->findBy(['enabled' => true], ['priority' => 'DESC']);
19 |
20 | return $commands;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Repository/CommandRepositoryInterface.php:
--------------------------------------------------------------------------------
1 | $command */
18 | $command = $this->findBy(['state' => ScheduledCommandStateEnum::WAITING]);
19 |
20 | return $command;
21 | }
22 |
23 | public function findLastCreatedCommand(CommandInterface $command): ?ScheduledCommandInterface
24 | {
25 | try {
26 | return $this->createQueryBuilder('scheduled')
27 | ->where('scheduled.owner = :owner')
28 | ->setParameter('owner', $command->getId())
29 | ->orderBy('scheduled.createdAt', 'DESC')
30 | ->setMaxResults(1)
31 | ->getQuery()
32 | ->getOneOrNullResult()
33 | ;
34 | } catch (NonUniqueResultException) {
35 | return null;
36 | }
37 | }
38 |
39 | public function findAllSinceXDaysWithState(\DateTimeInterface $dateTime, array $states): iterable
40 | {
41 | return $this->createQueryBuilder('scheduled')
42 | ->where('scheduled.state IN (:states)')
43 | ->andWhere('scheduled.createdAt < :createdAt')
44 | ->setParameter('states', $states)
45 | ->setParameter('createdAt', $dateTime->format('Y-m-d 00:00:00'))
46 | ->getQuery()
47 | ->getResult()
48 | ;
49 | }
50 |
51 | public function findAllHavingState(array $states): iterable
52 | {
53 | return $this->createQueryBuilder('scheduled')
54 | ->where('scheduled.state IN (:states)')
55 | ->setParameter('states', $states)
56 | ->getQuery()
57 | ->getResult()
58 | ;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Repository/ScheduledCommandRepositoryInterface.php:
--------------------------------------------------------------------------------
1 | 'sylius.grid_field', 'type' => 'scheduled_command_url'])]
16 | class ScheduleCommandRunner implements ScheduleCommandRunnerInterface
17 | {
18 | public function __construct(
19 | private readonly ScheduledCommandRepositoryInterface $scheduledCommandRepository,
20 | private readonly EntityManagerInterface $entityManager,
21 | #[Autowire(env: 'string:SYNOLIA_SCHEDULER_PLUGIN_LOGS_DIR')]
22 | private readonly string $logsDir,
23 | #[Autowire(param: 'kernel.project_dir')]
24 | private readonly string $projectDir,
25 | #[Autowire(env: 'int:SYNOLIA_SCHEDULER_PLUGIN_PING_INTERVAL')]
26 | private readonly int $pingInterval = 60,
27 | #[Autowire(env: 'bool:SYNOLIA_SCHEDULER_PLUGIN_KEEP_ALIVE')]
28 | private readonly bool $keepConnectionAlive = false,
29 | ) {
30 | }
31 |
32 | public function runImmediately(string $scheduledCommandId): bool
33 | {
34 | /** @var ScheduledCommandInterface|null $scheduledCommand */
35 | $scheduledCommand = $this->scheduledCommandRepository->find($scheduledCommandId);
36 | if (!$scheduledCommand instanceof ScheduledCommandInterface) {
37 | return false;
38 | }
39 |
40 | $process = Process::fromShellCommandline(
41 | $this->getCommandLine($scheduledCommand),
42 | $this->projectDir,
43 | );
44 |
45 | $scheduledCommand->setExecutedAt(new \DateTime());
46 | $process->setTimeout($scheduledCommand->getTimeout());
47 | $process->setIdleTimeout($scheduledCommand->getIdleTimeout());
48 | $this->startProcess($process);
49 |
50 | $result = $process->getExitCode();
51 |
52 | if (null === $result) {
53 | $result = 0;
54 | }
55 |
56 | $scheduledCommand->setLastReturnCode($result);
57 | $scheduledCommand->setCommandEndTime(new \DateTime());
58 | $this->entityManager->flush();
59 |
60 | return true;
61 | }
62 |
63 | public function runFromCron(ScheduledCommandInterface $scheduledCommand): int
64 | {
65 | $process = Process::fromShellCommandline($this->getCommandLine($scheduledCommand));
66 | $process->setTimeout($scheduledCommand->getTimeout());
67 | $process->setIdleTimeout($scheduledCommand->getIdleTimeout());
68 |
69 | try {
70 | $this->startProcess($process);
71 | } catch (ProcessTimedOutException) {
72 | }
73 |
74 | $result = $process->getExitCode();
75 | $scheduledCommand->setCommandEndTime(new \DateTime());
76 |
77 | if (null === $result) {
78 | $result = 0;
79 | }
80 |
81 | return $result;
82 | }
83 |
84 | private function startProcess(Process $process): void
85 | {
86 | if (!$this->keepConnectionAlive) {
87 | $process->run();
88 |
89 | return;
90 | }
91 |
92 | $process->start();
93 | while ($process->isRunning()) {
94 | $process->checkTimeout();
95 |
96 | try {
97 | $this->entityManager->getConnection()->executeQuery($this->entityManager->getConnection()->getDatabasePlatform()->getDummySelectSQL());
98 | } catch (\Doctrine\DBAL\Exception) {
99 | }
100 |
101 | for ($i = 0; $i < $this->pingInterval; ++$i) {
102 | if (!$process->isRunning()) {
103 | return;
104 | }
105 | \sleep(1);
106 |
107 | $process->checkTimeout();
108 | }
109 | }
110 | }
111 |
112 | private function getLogOutput(ScheduledCommandInterface $scheduledCommand): ?string
113 | {
114 | if ($scheduledCommand->getLogFile() === null || $scheduledCommand->getLogFile() === '') {
115 | return null;
116 | }
117 |
118 | return $this->logsDir . \DIRECTORY_SEPARATOR . $scheduledCommand->getLogFile();
119 | }
120 |
121 | private function getCommandLine(ScheduledCommandInterface $scheduledCommand): string
122 | {
123 | $commandLine = sprintf(
124 | '%s/bin/console %s %s',
125 | $this->projectDir,
126 | $scheduledCommand->getCommand(),
127 | $scheduledCommand->getArguments() ?? '',
128 | );
129 |
130 | $logOutput = $this->getLogOutput($scheduledCommand);
131 | if (null !== $logOutput) {
132 | $commandLine = sprintf(
133 | '%s/bin/console %s %s >> %s 2>> %s',
134 | $this->projectDir,
135 | $scheduledCommand->getCommand(),
136 | $scheduledCommand->getArguments() ?? '',
137 | $logOutput,
138 | $logOutput,
139 | );
140 | }
141 |
142 | return $commandLine;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/Runner/ScheduleCommandRunnerInterface.php:
--------------------------------------------------------------------------------
1 | formatBytes(...)),
18 | ];
19 | }
20 |
21 | public function formatBytes(int $bytes): string
22 | {
23 | if (self::EMPTY_FILE_SIZE === $bytes) {
24 | return '0B';
25 | }
26 |
27 | try {
28 | $number = floor(log($bytes, 1024));
29 |
30 | return round($bytes / (1024 ** $number), [0, 2, 2, 2, 3][$number]) . ['B', 'kB', 'MB', 'GB', 'TB'][$number];
31 | } catch (\Throwable) {
32 | return '0B';
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Validator/LogfilePrefixPropertyValidator.php:
--------------------------------------------------------------------------------
1 | buildViolation('synolia.forms.constraints.cannot_use_slash', [
21 | '%%DIRECTORY_SEPARATOR%%' => DIRECTORY_SEPARATOR,
22 | ])
23 | ->atPath('logFilePrefix')
24 | ->addViolation()
25 | ;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Validator/LogfilePropertyValidator.php:
--------------------------------------------------------------------------------
1 | buildViolation('synolia.forms.constraints.cannot_use_slash', [
21 | '%%DIRECTORY_SEPARATOR%%' => DIRECTORY_SEPARATOR,
22 | ])
23 | ->atPath('logFile')
24 | ->addViolation()
25 | ;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Voter/IsDueVoter.php:
--------------------------------------------------------------------------------
1 | checkers as $checker) {
24 | try {
25 | return $checker->isDue($command, $dateTime);
26 | } catch (IsNotDueException) {
27 | continue;
28 | }
29 | }
30 |
31 | return false;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Voter/IsDueVoterInterface.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/Controller/js.html.twig:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/Controller/modal.html.twig:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/templates/Controller/show.html.twig:
--------------------------------------------------------------------------------
1 | {% extends '@SyliusAdmin/shared/layout/base.html.twig' %}
2 | {% import '@SyliusAdmin/shared/helper/header.html.twig' as headers %}
3 |
4 | {% block title %}{{ 'synolia.ui.live_view_of_scheduled_command'|trans({'%scheduledCommandName%': scheduledCommand.name|e}) }} {{ parent() }}{% endblock %}
5 |
6 | {% block body %}
7 | {{ headers.default(1, 'synolia.ui.live_view_of_scheduled_command'|trans({'%scheduledCommandName%': scheduledCommand.name|e}), 'tabler:settings') }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {% hook 'synolia.admin.command.logs.stylesheets' %}
18 |
26 | {% hook 'synolia.admin.command.logs.javascripts' %}
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/templates/Grid/Action/empty_log_file.html.twig:
--------------------------------------------------------------------------------
1 | {% set path = options.link.url|default(path(options.link.route, options.link.parameters)) %}
2 |
3 |
9 |
--------------------------------------------------------------------------------
/templates/Grid/Action/empty_logs_file.html.twig:
--------------------------------------------------------------------------------
1 | {% set path = options.link.url|default(path(options.link.route)) %}
2 |
3 |
9 |
--------------------------------------------------------------------------------
/templates/Grid/Action/execute_immediate.html.twig:
--------------------------------------------------------------------------------
1 | {% import '@SyliusAdmin/shared/helper/button.html.twig' as buttons %}
2 |
3 | {% set path = options.link.url|default(path(options.link.route, options.link.parameters)) %}
4 |
5 | {{ buttons.default({url: path, text: action.label|trans, icon: 'tabler:refresh', class: 'btn-primary'}) }}
6 |
--------------------------------------------------------------------------------
/templates/Grid/Action/link.html.twig:
--------------------------------------------------------------------------------
1 | {% import '@SyliusAdmin/shared/helper/button.html.twig' as buttons %}
2 |
3 | {% set path = options.link.url|default(path(options.link.route, options.link.parameters)) %}
4 |
5 | {{ buttons.default({url: path, text: action.label|trans, icon: options.icon, class: options.class}) }}
6 |
--------------------------------------------------------------------------------
/templates/Grid/Column/human_readable_expression.html.twig:
--------------------------------------------------------------------------------
1 | {% if schedulerCommand.cronExpression is not empty %}
2 | {{ value }}
3 | {% endif %}
4 |
--------------------------------------------------------------------------------
/templates/Grid/Column/js.html.twig:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/Grid/Column/log_file.html.twig:
--------------------------------------------------------------------------------
1 | {% if schedulerCommand.logFile is not null and schedulerCommand.logFile is not empty %}
2 |
12 | {% endif %}
13 |
--------------------------------------------------------------------------------
/templates/Grid/Column/scheduled_command_state.html.twig:
--------------------------------------------------------------------------------
1 | {% if schedulerCommand.state == 'waiting' %}
2 |
3 | {{ ux_icon('tabler:calendar') }}
4 | {{ ('synolia.ui.statuses.' ~ schedulerCommand.state)|trans }}
5 |
6 | {% endif %}
7 | {% if schedulerCommand.state == 'in_progress' %}
8 |
9 | {{ ux_icon('tabler:refresh') }}
10 | {{ ('synolia.ui.statuses.' ~ schedulerCommand.state)|trans }}
11 |
12 | {% endif %}
13 | {% if schedulerCommand.state == 'finished' %}
14 |
15 | {{ ux_icon('tabler:check') }}
16 | {{ ('synolia.ui.statuses.' ~ schedulerCommand.state)|trans }}
17 |
18 | {% endif %}
19 | {% if schedulerCommand.state == 'error' %}
20 |
21 | {{ ux_icon('tabler:bell') }}
22 | {{ ('synolia.ui.statuses.' ~ schedulerCommand.state)|trans }}
23 |
24 | {% endif %}
25 | {% if schedulerCommand.state == 'termination' %}
26 |
27 | {{ ux_icon('tabler:bell') }}
28 | {{ ('synolia.ui.statuses.' ~ schedulerCommand.state)|trans }}
29 |
30 | {% endif %}
31 |
--------------------------------------------------------------------------------
/translations/messages.en.yml:
--------------------------------------------------------------------------------
1 | synolia:
2 | ui:
3 | commands: Scheduled commands
4 | scheduled_commands: Scheduled commands history
5 | scheduled_command:
6 | id: Id
7 | name: Name
8 | command: Command
9 | state: State
10 | cron_expression: Cron expression
11 | last_execution: Executed At
12 | last_return_code: Last return code
13 | log_file: Log file
14 | priority: Priority
15 | enabled: Enabled
16 | command_execution_time: Execution time
17 | execute_immediate: Execute immediately
18 | emtpy_logs: Empty logs
19 | bulk_empty_logs: The log files have been emptied.
20 | edit_command: Edit scheduled command
21 | new_scheduled_command: Create immediate scheduled command
22 | new_command: Create scheduled command
23 | launch_a_command: Launch a command
24 | log_file_undefined: Scheduled command has no defined log file.
25 | no_log_file_found: No log file found.
26 | error_emptying_log_file: There was an error while trying to empty the log file.
27 | log_file_successfully_emptied: Log file successfully emptied.
28 | scheduled_command_not_exists: Scheduled Command does not exists.
29 | live_view: Live view
30 | download: Download
31 | live_view_of_scheduled_command: 'Live view of scheduled command "%scheduledCommandName%"'
32 | does_not_exists_or_missing_log_file: 'Scheduler Command does not exists or has no log file.'
33 | statuses:
34 | waiting: Waiting
35 | in_progress: In Progress
36 | finished: Finished
37 | error: Error
38 | termination: Cancelled
39 | menu:
40 | admin:
41 | main:
42 | configuration:
43 | scheduler_command: Scheduled commands
44 | scheduler_command_history: Scheduled commands history
45 | sylius_plus:
46 | rbac:
47 | parent:
48 | commands: Scheduled commands
49 | scheduled_commands: Scheduled commands history
50 |
--------------------------------------------------------------------------------
/translations/messages.fr.yml:
--------------------------------------------------------------------------------
1 | synolia:
2 | ui:
3 | commands: Commandes planifiées
4 | scheduled_commands: Historique des commandes planifiées
5 | scheduled_command:
6 | id: Id
7 | name: Nom
8 | command: Commande
9 | state: Etat
10 | cron_expression: Expression de Cron
11 | last_execution: Lancé à
12 | last_return_code: Dernier code de retour
13 | log_file: Fichier de log
14 | priority: Priorité
15 | enabled: Activé
16 | command_execution_time: Temps d'execution
17 | execute_immediate: Executer immédiatement
18 | emtpy_logs: Vider les logs
19 | bulk_empty_logs: Les fichiers de logs ont bien été vidé.
20 | edit_command: Editer la commande planifiée
21 | new_scheduled_command: Créer une commande planifiée immédiate
22 | new_command: Créer une commande planifiée
23 | launch_a_command: Lancer une commande
24 | live_view: Voir en direct
25 | download: Télécharger
26 | live_view_of_scheduled_command: 'Vue en direct de la commande "%scheduledCommandName%"'
27 | does_not_exists_or_missing_log_file: 'La commande n''existe pas ou n''a pas de fichier log.'
28 | statuses:
29 | waiting: En attente
30 | in_progress: En cours
31 | finished: Terminée
32 | error: En erreur
33 | termination: Annulée
34 | menu:
35 | admin:
36 | main:
37 | configuration:
38 | scheduler_command: Commandes planifiées
39 | scheduler_command_history: Historique des planifications
40 | sylius_plus:
41 | rbac:
42 | parent:
43 | commands: Commandes planifiées
44 | scheduled_commands: Historique des planifications
45 |
--------------------------------------------------------------------------------
/translations/messages.nl.yml:
--------------------------------------------------------------------------------
1 | synolia:
2 | ui:
3 | commands: 'Geplande commando''s'
4 | scheduled_commands: Geplande commando geschiedenis
5 | scheduled_command:
6 | id: Id
7 | name: Naam
8 | command: Commando
9 | state: Status
10 | cron_expression: Cron expressie
11 | last_execution: Laatst uitgevoerd
12 | last_return_code: Laatste retourcode
13 | log_file: Log-bestand
14 | priority: Prioriteit
15 | enabled: Ingeschakeld
16 | command_execution_time: Uitvoer duur
17 | execute_immediate: Voer direct uit
18 | emtpy_logs: Logs legen
19 | bulk_empty_logs: De log-bestanden zijn geleegd.
20 | edit_command: Bewerk commando
21 | new_scheduled_command: Creëer direct een commando
22 | new_command: Creëer commando
23 | launch_a_command: Lanceer een commando
24 | log_file_undefined: Geplande commando heeft geen log-bestand gedefinieerd.
25 | no_log_file_found: Geen log-bestand gevonden.
26 | error_emptying_log_file: Er was een fout bij het leegmaken van het log-bestand.
27 | log_file_successfully_emptied: Log-bestand succesvol geleegd.
28 | scheduled_command_not_exists: Gepland commando bestaat niet.
29 | live_view: Live weergave
30 | download: Download
31 | live_view_of_scheduled_command: 'Live weergave van commando "%scheduledCommandName%"'
32 | does_not_exists_or_missing_log_file: 'Gepland commando bestaat niet of heeft geen log-bestand.'
33 | statuses:
34 | waiting: In afwachting
35 | in_progress: Bezig
36 | finished: Voltooid
37 | error: Fout
38 | menu:
39 | admin:
40 | main:
41 | configuration:
42 | scheduler_command: 'Geplande commando''s'
43 | scheduler_command_history: Geplande commando geschiedenis
44 | sylius_plus:
45 | rbac:
46 | parent:
47 | commands: 'Geplande commando''s'
48 | scheduled_commands: Geplande commando geschiedenis
49 |
--------------------------------------------------------------------------------
/translations/validators.en.yaml:
--------------------------------------------------------------------------------
1 | synolia:
2 | forms:
3 | constraints:
4 | cannot_use_slash: You cannot use a "%%DIRECTORY_SEPARATOR%%" for security reason
5 |
--------------------------------------------------------------------------------
/translations/validators.fr.yaml:
--------------------------------------------------------------------------------
1 | synolia:
2 | forms:
3 | constraints:
4 | cannot_use_slash: Vous ne pouvez pas utiliser de "%%DIRECTORY_SEPARATOR%%" pour des raisons de sécurité
5 |
--------------------------------------------------------------------------------