diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d080c7d..3c23da3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,17 +10,17 @@ jobs: tests: name: All tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: matrix: - php: [ '8.2'] + php: [ '8.2', '8.5'] TYPO3: [ '13', '14' ] steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v6 - name: Install testing system - run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -t ${{ matrix.TYPO3 }} -s composerUpdate + run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -t ${{ matrix.TYPO3 }} -s composerInstall - name: Composer validate run: Build/Scripts/runTests.sh -p ${{ matrix.php }} -s composerValidate diff --git a/.gitignore b/.gitignore index c142ac8..a6b8c54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ public composer.lock vendor +var .php_cs.cache .php-cs-fixer.cache /.Build diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh index 4c8ab13..42944d0 100755 --- a/Build/Scripts/runTests.sh +++ b/Build/Scripts/runTests.sh @@ -49,43 +49,20 @@ No arguments: Run all unit tests with PHP 8.0 Options: -s <...> Specifies which test suite to run - - acceptance: backend acceptance tests - cgl: cgl test and fix all php files - composerUpdate: "composer update", handy if host has no PHP - composerValidate: "composer validate" - - functional: functional tests - lint: PHP linting - phpstan: phpstan analyze - - unit (default): PHP unit tests -t <13|14> Only with -s composerUpdate|acceptance|functional TYPO3 core major version the extension is embedded in for testing. - -d - Only with -s functional - Specifies on which DBMS tests are performed - - mariadb (default): use mariadb - - postgres: use postgres - - sqlite: use sqlite - -p <8.2|8.3|8.4|8.5> Specifies the PHP minor version to be used - 8.2 (default): use PHP 8.2 - -e "" - Only with -s acceptance|functional|unit - Additional options to send to phpunit (unit & functional tests) or codeception (acceptance - tests). For phpunit, options starting with "--" must be added after options starting with "-". - Example -e "-v --filter canRetrieveValueWithGP" to enable verbose output AND filter tests - named "canRetrieveValueWithGP" - - -x - Only with -s functional|unit|acceptance - Send information to host instance for test or system under test break points. This is especially - useful if a local PhpStorm instance is listening on default xdebug port 9003. A different port - can be selected with -y - -y Send xdebug information to a different port than default 9003 if an IDE like PhpStorm is not listening on default port. @@ -139,7 +116,7 @@ PHP_XDEBUG_PORT=9003 EXTRA_TEST_OPTIONS="" SCRIPT_VERBOSE=0 CGLCHECK_DRY_RUN="" -TYPO3="13" +TYPO3="14" # Option parsing # Reset in case getopts has been used previously in the shell @@ -207,7 +184,7 @@ if [ ${#INVALID_OPTIONS[@]} -ne 0 ]; then fi # Move "7.4" to "php74", the latter is the docker container name -DOCKER_PHP_IMAGE=$(echo "php${PHP_VERSION}" | sed -e 's/\.//') +DOCKER_PHP_IMAGE="ghcr.io/typo3/core-testing-$(echo "php${PHP_VERSION}" | sed -e 's/\.//'):latest" # Set $1 to first mass argument, this is the optional test file or test directory to execute shift $((OPTIND - 1)) @@ -222,12 +199,6 @@ fi # Suite execution case ${TEST_SUITE} in - acceptance) - setUpDockerComposeDotEnv - docker compose run acceptance_backend_mariadb10 - SUITE_EXIT_CODE=$? - docker compose down - ;; cgl) # Active dry-run for cgl needs not "-n" but specific options if [ -n "${CGLCHECK_DRY_RUN}" ]; then @@ -238,9 +209,9 @@ case ${TEST_SUITE} in SUITE_EXIT_CODE=$? docker compose down ;; - composerUpdate) + composerInstall) setUpDockerComposeDotEnv - docker compose run composer_update + docker compose run composer_install SUITE_EXIT_CODE=$? docker compose down ;; @@ -250,30 +221,6 @@ case ${TEST_SUITE} in SUITE_EXIT_CODE=$? docker compose down ;; - functional) - setUpDockerComposeDotEnv - case ${DBMS} in - mariadb) - docker compose run functional_mariadb10 - SUITE_EXIT_CODE=$? - ;; - postgres) - docker compose run functional_postgres10 - SUITE_EXIT_CODE=$? - ;; - sqlite) - mkdir -p ${CORE_ROOT}/.Build/Web/typo3temp/var/tests/functional-sqlite-dbs/ - docker compose run functional_sqlite - SUITE_EXIT_CODE=$? - ;; - *) - echo "Invalid -d option argument ${DBMS}" >&2 - echo >&2 - echo "${HELP}" >&2 - exit 1 - esac - docker compose down - ;; lint) setUpDockerComposeDotEnv docker compose run lint diff --git a/Build/php-cs-fixer.php b/Build/php-cs-fixer.php index eef762f..319c169 100644 --- a/Build/php-cs-fixer.php +++ b/Build/php-cs-fixer.php @@ -1,6 +1,12 @@ getFinder()->in(['Classes', 'Configuration']); -$config->getFinder()->exclude(['var', 'public']); +$config->getFinder()->exclude(['var', 'public', 'Build'])->in(__DIR__ . '/..'); +$config->addRules([ + 'nullable_type_declaration' => [ + 'syntax' => 'question_mark', + ], + 'nullable_type_declaration_for_default_null_value' => true, + 'declare_strict_types' => true, +]); return $config; diff --git a/Build/phpstan-baseline.neon b/Build/phpstan-baseline.neon deleted file mode 100644 index 364905f..0000000 --- a/Build/phpstan-baseline.neon +++ /dev/null @@ -1,2 +0,0 @@ -parameters: - ignoreErrors: diff --git a/Build/phpstan.neon b/Build/phpstan.neon index ead1b3e..bb5c2d3 100644 --- a/Build/phpstan.neon +++ b/Build/phpstan.neon @@ -1,6 +1,3 @@ -includes: - - ../.Build/vendor/phpstan/phpstan/conf/bleedingEdge.neon - - ./phpstan-baseline.neon parameters: level: 5 diff --git a/Build/testing-docker/docker-compose.yml b/Build/testing-docker/docker-compose.yml index 254aa2d..3c8775c 100644 --- a/Build/testing-docker/docker-compose.yml +++ b/Build/testing-docker/docker-compose.yml @@ -1,115 +1,7 @@ services: - chrome: - # Image for Mac M1 - # image: seleniarm/standalone-chromium:4.1.2-20220227 - image: selenium/standalone-chrome:4.0.0-20211102 - tmpfs: - - /dev/shm:rw,nosuid,nodev,noexec,relatime - - mariadb10: - # not using mariadb:10 for the time being, because 10.5.7 (currently latest) is broken - image: mariadb:10.5.6 - environment: - MYSQL_ROOT_PASSWORD: funcp - tmpfs: - - /var/lib/mysql/:rw,noexec,nosuid - - postgres10: - image: postgres:10-alpine - environment: - POSTGRES_PASSWORD: funcp - POSTGRES_USER: ${HOST_USER} - tmpfs: - - /var/lib/postgresql/data:rw,noexec,nosuid - - web: - image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - stop_grace_period: 1s - volumes: - - ${CORE_ROOT}:${CORE_ROOT} - - /etc/passwd:/etc/passwd:ro - - /etc/group:/etc/group:ro - environment: - TYPO3_PATH_ROOT: ${CORE_ROOT}/.Build/Web/typo3temp/var/tests/acceptance - TYPO3_PATH_APP: ${CORE_ROOT}/.Build/Web/typo3temp/var/tests/acceptance - command: > - /bin/sh -c " - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - XDEBUG_MODE=\"off\" \ - php -S web:8000 -t ${CORE_ROOT}/.Build/Web - else - DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'` - XDEBUG_MODE=\"debug,develop\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \ - php -S web:8000 -t ${CORE_ROOT}/.Build/Web - fi - " - - acceptance_backend_mariadb10: - image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - links: - - mariadb10 - - chrome - - web - environment: - typo3DatabaseName: func_test - typo3DatabaseUsername: root - typo3DatabasePassword: funcp - typo3DatabaseHost: mariadb10 - volumes: - - ${CORE_ROOT}:${CORE_ROOT} - - ${HOST_HOME}:${HOST_HOME} - - /etc/passwd:/etc/passwd:ro - - /etc/group:/etc/group:ro - working_dir: ${CORE_ROOT}/.Build - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - echo Waiting for database start...; - while ! nc -z mariadb10 3306; do - sleep 1; - done; - echo Database is up; - php -v | grep '^PHP'; - mkdir -p Web/typo3temp/var/tests/ - COMMAND=\"vendor/codeception/codeception/codecept run Backend -d -c Web/typo3conf/ext/form_custom_templates/Tests/codeception.yml ${TEST_FILE}\" - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - XDEBUG_MODE=\"off\" \ - $${COMMAND}; - else - DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'` - XDEBUG_MODE=\"debug,develop\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \ - $${COMMAND}; - fi - " cgl: - image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${CORE_ROOT}:${CORE_ROOT} - - ${HOST_HOME}:${HOST_HOME} - - /etc/passwd:/etc/passwd:ro - - /etc/group:/etc/group:ro - working_dir: ${CORE_ROOT} - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - php -v | grep '^PHP'; - ${CORE_ROOT}/.Build/bin/php-cs-fixer fix --config Build/php-cs-fixer.php - " - - composer_update: - image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + image: ${DOCKER_PHP_IMAGE} user: "${HOST_UID}" volumes: - ${CORE_ROOT}:${CORE_ROOT} @@ -123,11 +15,11 @@ services: set -x fi php -v | grep '^PHP'; - COMPOSER_HOME=${CORE_ROOT}/.Build/.composer composer update --no-progress --no-interaction; + ${CORE_ROOT}/.Build/bin/php-cs-fixer fix --config Build/php-cs-fixer.php --dry-run --stop-on-violation --using-cache=no " - composer_validate: - image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + composer_install: + image: ${DOCKER_PHP_IMAGE} user: "${HOST_UID}" volumes: - ${CORE_ROOT}:${CORE_ROOT} @@ -141,121 +33,34 @@ services: set -x fi php -v | grep '^PHP'; - composer validate; - " - - functional_mariadb10: - image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - links: - - mariadb10 - volumes: - - ${CORE_ROOT}:${CORE_ROOT} - - ${HOST_HOME}:${HOST_HOME} - - /etc/passwd:/etc/passwd:ro - - /etc/group:/etc/group:ro - environment: - typo3DatabaseName: func_test - typo3DatabaseUsername: root - typo3DatabasePassword: funcp - typo3DatabaseHost: mariadb10 - working_dir: ${CORE_ROOT}/.Build - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - echo Waiting for database start...; - while ! nc -z mariadb10 3306; do - sleep 1; - done; - echo Database is up; - php -v | grep '^PHP'; - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - XDEBUG_MODE=\"off\" \ - bin/phpunit -c Web/typo3conf/ext/form_custom_templates/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + if [ ${TYPO3} -eq 13 ]; then + COMPOSER_HOME=${CORE_ROOT}/.Build/.composer composer require typo3/cms-core:^13.4 --dev -W --no-progress --no-interaction --no-plugins; else - DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'` - XDEBUG_MODE=\"debug,develop\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \ - bin/phpunit -c Web/typo3conf/ext/form_custom_templates/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + COMPOSER_HOME=${CORE_ROOT}/.Build/.composer composer install --no-progress --no-interaction; fi - " - functional_postgres10: - image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - links: - - postgres10 - volumes: - - ${CORE_ROOT}:${CORE_ROOT} - - ${HOST_HOME}:${HOST_HOME} - - /etc/passwd:/etc/passwd:ro - - /etc/group:/etc/group:ro - environment: - typo3DatabaseDriver: pdo_pgsql - typo3DatabaseName: bamboo - typo3DatabaseUsername: ${HOST_USER} - typo3DatabaseHost: postgres10 - typo3DatabasePassword: funcp - working_dir: ${CORE_ROOT}/.Build - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - echo Waiting for database start...; - while ! nc -z postgres10 5432; do - sleep 1; - done; - echo Database is up; - php -v | grep '^PHP'; - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - XDEBUG_MODE=\"off\" \ - bin/phpunit -c Web/typo3conf/ext/form_custom_templates/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-postgres ${TEST_FILE}; - else - DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'` - XDEBUG_MODE=\"debug,develop\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \ - bin/phpunit -c Web/typo3conf/ext/form_custom_templates/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-postgres ${TEST_FILE}; - fi " - functional_sqlite: - image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + composer_validate: + image: ${DOCKER_PHP_IMAGE} user: "${HOST_UID}" volumes: - ${CORE_ROOT}:${CORE_ROOT} - ${HOST_HOME}:${HOST_HOME} - /etc/passwd:/etc/passwd:ro - /etc/group:/etc/group:ro - tmpfs: - - ${CORE_ROOT}/.Build/Web/typo3temp/var/tests/functional-sqlite-dbs/:rw,noexec,nosuid,uid=${HOST_UID} - environment: - typo3DatabaseDriver: pdo_sqlite - working_dir: ${CORE_ROOT}/.Build + working_dir: ${CORE_ROOT} command: > /bin/sh -c " if [ ${SCRIPT_VERBOSE} -eq 1 ]; then set -x fi php -v | grep '^PHP'; - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - XDEBUG_MODE=\"off\" \ - bin/phpunit -c Web/typo3conf/ext/form_custom_templates/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-sqlite ${TEST_FILE}; - else - DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'` - XDEBUG_MODE=\"debug,develop\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \ - bin/phpunit -c Web/typo3conf/ext/form_custom_templates/Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-sqlite ${TEST_FILE}; - fi + composer validate; " lint: - image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + image: ${DOCKER_PHP_IMAGE} user: "${HOST_UID}" volumes: - ${CORE_ROOT}:${CORE_ROOT} @@ -272,7 +77,7 @@ services: " phpstan: - image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + image: ${DOCKER_PHP_IMAGE} user: "${HOST_UID}" volumes: - ${CORE_ROOT}:${CORE_ROOT} @@ -288,30 +93,3 @@ services: php -v | grep '^PHP'; php -dxdebug.mode=off .Build/bin/phpstan analyze -c Build/phpstan.neon --no-progress --no-interaction " - - unit: - image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest - user: "${HOST_UID}" - volumes: - - ${CORE_ROOT}:${CORE_ROOT} - - ${HOST_HOME}:${HOST_HOME} - - /etc/passwd:/etc/passwd:ro - - /etc/group:/etc/group:ro - working_dir: ${CORE_ROOT}/.Build - command: > - /bin/sh -c " - if [ ${SCRIPT_VERBOSE} -eq 1 ]; then - set -x - fi - php -v | grep '^PHP'; - if [ ${PHP_XDEBUG_ON} -eq 0 ]; then - XDEBUG_MODE=\"off\" \ - bin/phpunit -c Web/typo3conf/ext/form_custom_templates/Build/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; - else - DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'` - XDEBUG_MODE=\"debug,develop\" \ - XDEBUG_TRIGGER=\"foo\" \ - XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \ - bin/phpunit -c Web/typo3conf/ext/form_custom_templates/Build/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; - fi - " diff --git a/Classes/Backend/Controller/Ajax/JobController.php b/Classes/Backend/Controller/Ajax/JobController.php index cad9897..cc0b06b 100644 --- a/Classes/Backend/Controller/Ajax/JobController.php +++ b/Classes/Backend/Controller/Ajax/JobController.php @@ -12,11 +12,13 @@ * of the License, or any later version. */ +use B13\ContentSync\Domain\Factory\StatusReportFactory; use B13\ContentSync\Domain\Model\Configuration; use B13\ContentSync\Domain\Model\Job; use B13\ContentSync\Domain\Repository\JobRepository; use B13\ContentSync\Domain\Validation\ConfigurationValidator; use B13\ContentSync\Exception; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\Attribute\AsController; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; @@ -36,9 +38,10 @@ public function __construct( private ExtensionConfiguration $extensionConfiguration, private ConfigurationValidator $validator, private JobRepository $jobRepository, + private StatusReportFactory $statusReportFactory, ) {} - public function create(ServerRequestInterface $request): Response + public function create(ServerRequestInterface $request): ResponseInterface { if (!$this->checkAccess()) { return (new Response())->withStatus(403); @@ -51,7 +54,7 @@ public function create(ServerRequestInterface $request): Response $this->jobRepository->add($job); $viewFactoryData = new ViewFactoryData( templateRootPaths: ['EXT:content_sync/Resources/Private/Templates/'], - partialRootPaths: [['EXT:content_sync/Resources/Private/Partials/']], + partialRootPaths: ['EXT:content_sync/Resources/Private/Partials/'], request: $request, ); $view = $this->viewFactory->create($viewFactoryData); @@ -77,7 +80,23 @@ public function create(ServerRequestInterface $request): Response return new JsonResponse($return); } - public function kill(ServerRequestInterface $request): Response + public function reload(ServerRequestInterface $request): ResponseInterface + { + $statusReport = $this->statusReportFactory->build(); + $viewFactoryData = new ViewFactoryData( + templateRootPaths: ['EXT:content_sync/Resources/Private/Templates/'], + partialRootPaths: ['EXT:content_sync/Resources/Private/Partials/'], + request: $request, + ); + $view = $this->viewFactory->create($viewFactoryData); + $view->assign('statusReport', $statusReport); + $return = [ + 'content' => $view->render('Ajax/Job/Reload'), + ]; + return new JsonResponse($return); + } + + public function kill(ServerRequestInterface $request): ResponseInterface { if (!$this->checkAccess()) { return (new Response())->withStatus(403); @@ -85,7 +104,7 @@ public function kill(ServerRequestInterface $request): Response $job = $this->jobRepository->findOneLast(); $viewFactoryData = new ViewFactoryData( templateRootPaths: ['EXT:content_sync/Resources/Private/Templates/'], - partialRootPaths: [['EXT:content_sync/Resources/Private/Partials/']], + partialRootPaths: ['EXT:content_sync/Resources/Private/Partials/'], request: $request, ); $view = $this->viewFactory->create($viewFactoryData); diff --git a/Classes/Command/CollectGarbageCommand.php b/Classes/Command/CollectGarbageCommand.php index 0449802..3821236 100644 --- a/Classes/Command/CollectGarbageCommand.php +++ b/Classes/Command/CollectGarbageCommand.php @@ -13,15 +13,17 @@ */ use B13\ContentSync\Domain\Repository\JobRepository; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[AsCommand(name: 'content-sync:collect-garbage')] final class CollectGarbageCommand extends Command { public function __construct( private readonly JobRepository $jobRepository, - string $name = null + ?string $name = null ) { parent::__construct($name); } @@ -33,6 +35,6 @@ public function execute(InputInterface $input, OutputInterface $output): int $job->fail('job too old'); $this->jobRepository->updateJob($job); } - return 0; + return Command::SUCCESS; } } diff --git a/Classes/Command/JobCreatorCommand.php b/Classes/Command/JobCreatorCommand.php index 44e3355..58c824f 100644 --- a/Classes/Command/JobCreatorCommand.php +++ b/Classes/Command/JobCreatorCommand.php @@ -16,18 +16,21 @@ use B13\ContentSync\Domain\Model\Job; use B13\ContentSync\Domain\Repository\JobRepository; use B13\ContentSync\Domain\Validation\ConfigurationValidator; +use B13\ContentSync\Exception; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; +#[AsCommand(name: 'content-sync:job:create')] final class JobCreatorCommand extends Command { public function __construct( private readonly ExtensionConfiguration $extensionConfiguration, private readonly ConfigurationValidator $validator, private readonly JobRepository $jobRepository, - string $name = null + ?string $name = null ) { parent::__construct($name); } @@ -38,7 +41,12 @@ public function execute(InputInterface $input, OutputInterface $output): int $this->validator->assertValid($configuration); $job = new Job(); $job->setConfiguration($configuration); - $this->jobRepository->add($job); - return 0; + try { + $this->jobRepository->add($job); + } catch (Exception $e) { + $output->writeln($e->getMessage()); + return Command::FAILURE; + } + return Command::SUCCESS; } } diff --git a/Classes/Command/JobKillerCommand.php b/Classes/Command/JobKillerCommand.php index 7669b5b..e60e403 100644 --- a/Classes/Command/JobKillerCommand.php +++ b/Classes/Command/JobKillerCommand.php @@ -13,15 +13,17 @@ */ use B13\ContentSync\Domain\Repository\JobRepository; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[AsCommand(name: 'content-sync:job:kill')] final class JobKillerCommand extends Command { public function __construct( private readonly JobRepository $jobRepository, - string $name = null + ?string $name = null ) { parent::__construct($name); } @@ -30,10 +32,10 @@ public function execute(InputInterface $input, OutputInterface $output): int { $job = $this->jobRepository->findOneLast(); if ($job === null) { - return 0; + return Command::SUCCESS; } $job->kill(); $this->jobRepository->updateJob($job); - return 0; + return Command::SUCCESS; } } diff --git a/Classes/Command/RunnerCommand.php b/Classes/Command/RunnerCommand.php index ada1674..2d2294d 100644 --- a/Classes/Command/RunnerCommand.php +++ b/Classes/Command/RunnerCommand.php @@ -16,17 +16,19 @@ use B13\ContentSync\Domain\Service\ProcessRunner; use B13\ContentSync\Domain\Validation\ConfigurationValidator; use B13\ContentSync\Exception; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[AsCommand(name: 'content-sync:runner')] class RunnerCommand extends Command { public function __construct( private readonly ProcessRunner $processRunner, private readonly JobRepository $jobRepository, private readonly ConfigurationValidator $validator, - string $name = null + ?string $name = null ) { parent::__construct($name); } @@ -35,11 +37,11 @@ public function execute(InputInterface $input, OutputInterface $output): int { $runningJob = $this->jobRepository->findOneRunning(); if ($runningJob !== null) { - return 0; + return Command::SUCCESS; } $job = $this->jobRepository->findOneWaiting(); if ($job === null) { - return 0; + return Command::SUCCESS; } $job->start(); $this->jobRepository->updateJob($job); @@ -55,11 +57,11 @@ public function execute(InputInterface $input, OutputInterface $output): int // @extensionScannerIgnoreLine $job->finish(); $this->jobRepository->updateJob($job); - return 0; + return Command::SUCCESS; } catch (Exception $e) { $job->fail($e->getCode() . ' - ' . $e->getMessage()); $this->jobRepository->updateJob($job); - return 1; + return Command::FAILURE; } } } diff --git a/Classes/Command/StatusReportCommand.php b/Classes/Command/StatusReportCommand.php index 0fe1561..16fa405 100644 --- a/Classes/Command/StatusReportCommand.php +++ b/Classes/Command/StatusReportCommand.php @@ -14,16 +14,18 @@ use B13\ContentSync\Domain\Factory\StatusReportFactory; use B13\ContentSync\Domain\Model\Job; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use TYPO3\CMS\Extbase\Utility\LocalizationUtility; +#[AsCommand(name: 'content-sync:status-report')] class StatusReportCommand extends Command { public function __construct( private readonly StatusReportFactory $statusReportFactory, - string $name = null + ?string $name = null ) { parent::__construct($name); } @@ -87,6 +89,6 @@ public function execute(InputInterface $input, OutputInterface $output): int } else { $output->writeln(LocalizationUtility::translate($llPrefix . 'no-jobs')); } - return 0; + return Command::SUCCESS; } } diff --git a/Classes/Domain/Model/Job.php b/Classes/Domain/Model/Job.php index 9d74e40..e468aaf 100644 --- a/Classes/Domain/Model/Job.php +++ b/Classes/Domain/Model/Job.php @@ -27,8 +27,8 @@ class Job protected int $status = self::STATUS_WAITING; protected Configuration $configuration; - protected \DateTime $startTime; - protected \DateTime $endTime; + protected ?\DateTime $startTime = null; + protected ?\DateTime $endTime = null; protected \DateTime $createdTime; protected string $error = ''; protected int $uid; @@ -36,8 +36,6 @@ class Job public function __construct() { $this->createdTime = new \DateTime(); - $this->startTime = new \DateTime(); - $this->endTime = new \DateTime(); } public function fail(string $error): void @@ -91,8 +89,8 @@ public function toDatabaseRow(): array 'status' => $this->status, 'json_configuration' => $json, 'created_time' => $this->createdTime->format('U'), - 'start_time' => $this->startTime->format('U'), - 'end_time' => $this->endTime->format('U'), + 'start_time' => $this->startTime ? $this->startTime->format('U') : 0, + 'end_time' => $this->endTime ? $this->endTime->format('U') : 0, 'error' => $this->error, ]; } diff --git a/Classes/Domain/Repository/JobRepository.php b/Classes/Domain/Repository/JobRepository.php index 5ee4943..386a463 100644 --- a/Classes/Domain/Repository/JobRepository.php +++ b/Classes/Domain/Repository/JobRepository.php @@ -13,6 +13,7 @@ */ use B13\ContentSync\Domain\Model\Job; +use B13\ContentSync\Exception; use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; @@ -26,6 +27,14 @@ public function __construct( public function add(Job $job): void { + $waitingJob = $this->findOneWaiting(); + if ($waitingJob !== null) { + throw new Exception('there is already a waiting job', 1780465898); + } + $runningJob = $this->findOneRunning(); + if ($runningJob !== null) { + throw new Exception('there is already a running job', 1780465899); + } $this->connectionPool->getConnectionForTable(self::TABLE)->insert(self::TABLE, $job->toDatabaseRow()); } diff --git a/Classes/Domain/Service/ProcessRunner.php b/Classes/Domain/Service/ProcessRunner.php index ddf927c..9609229 100644 --- a/Classes/Domain/Service/ProcessRunner.php +++ b/Classes/Domain/Service/ProcessRunner.php @@ -13,13 +13,18 @@ */ use B13\ContentSync\Domain\Model\Configuration; +use B13\ContentSync\Event\BeforeProcessRunnerExecutesCommandsEvent; use B13\ContentSync\Exception; +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\Process\Process; final readonly class ProcessRunner { public function __construct( - private DatabaseParameterBuilder $databaseParameterBuilder + private DatabaseParameterBuilder $databaseParameterBuilder, + private LoggerInterface $logger, + private EventDispatcherInterface $eventDispatcher ) {} public function localToRemote(Configuration $configuration): void @@ -40,8 +45,11 @@ public function localToRemote(Configuration $configuration): void $file = rtrim($file, '/'); $commands[] = 'rsync -a --delete --omit-dir-times --no-owner --no-group ' . $localNode->getBasePath() . $file . '/ ' . $remoteNode->getConnection() . ':' . $remoteNode->getBasePath() . $file; } + $beforeProcessRunnnerExecuteCommands = new BeforeProcessRunnerExecutesCommandsEvent($configuration, $commands); + $this->eventDispatcher->dispatch($beforeProcessRunnnerExecuteCommands); + $commands = $beforeProcessRunnnerExecuteCommands->commands; foreach ($commands as $command) { - $this->exec($command); + $this->exec($command, $beforeProcessRunnnerExecuteCommands->timeoutPerProcess); } } @@ -63,14 +71,18 @@ public function remoteToLocal(Configuration $configuration): void $file = rtrim($file, '/'); $commands[] = 'rsync -a --delete --omit-dir-times --no-owner --no-group ' . $remoteNode->getConnection() . ':' . $remoteNode->getBasePath() . $file . '/ ' . $localNode->getBasePath() . $file; } + $beforeProcessRunnnerExecuteCommands = new BeforeProcessRunnerExecutesCommandsEvent($configuration, $commands); + $this->eventDispatcher->dispatch($beforeProcessRunnnerExecuteCommands); + $commands = $beforeProcessRunnnerExecuteCommands->commands; foreach ($commands as $command) { - $this->exec($command); + $this->exec($command, $beforeProcessRunnnerExecuteCommands->timeoutPerProcess); } } - private function exec(string $cmd): void + private function exec(string $cmd, int $timeout): void { - $process = Process::fromShellCommandline($cmd); + $this->logger->debug($cmd); + $process = Process::fromShellCommandline(command: $cmd, timeout: $timeout); $process->run(); if (!$process->isSuccessful()) { throw new Exception('cannot exec command ' . $cmd . ' with error ' . $process->getErrorOutput(), 1600757440); diff --git a/Classes/Domain/Validation/ConfigurationValidator.php b/Classes/Domain/Validation/ConfigurationValidator.php index f7fbe74..b8ebb40 100644 --- a/Classes/Domain/Validation/ConfigurationValidator.php +++ b/Classes/Domain/Validation/ConfigurationValidator.php @@ -37,7 +37,7 @@ public function assertRemoteIsValid(Node $remoteNode): void $process = Process::fromShellCommandline('ssh -o BatchMode=yes -o ConnectTimeout=5 ' . $remoteNode->getConnection() . ' ls ' . $remoteNode->getBin()); $process->run(); if (!$process->isSuccessful()) { - throw new Exception('typo3cms bin not found at remote node', 1600765841); + throw new Exception('typo3 bin not found at remote node', 1600765841); } // remote basePath exists $process = Process::fromShellCommandline('ssh -o BatchMode=yes -o ConnectTimeout=5 ' . $remoteNode->getConnection() . ' ls -d ' . $remoteNode->getBasePath()); @@ -69,7 +69,7 @@ public function assertValid(Configuration $configuration): void $process = Process::fromShellCommandline('ls ' . $localNode->getBin()); $process->run(); if (!$process->isSuccessful()) { - throw new Exception('typo3cms bin not found at local node', 1600765843); + throw new Exception('typo3 bin not found at local node', 1600765843); } // remote basePath exists $process = Process::fromShellCommandline('ls -d ' . $localNode->getBasePath()); diff --git a/Classes/Event/BeforeProcessRunnerExecutesCommandsEvent.php b/Classes/Event/BeforeProcessRunnerExecutesCommandsEvent.php new file mode 100644 index 0000000..740351c --- /dev/null +++ b/Classes/Event/BeforeProcessRunnerExecutesCommandsEvent.php @@ -0,0 +1,25 @@ + '/ContentSync/job/kill', 'target' => B13\ContentSync\Backend\Controller\Ajax\JobController::class . '::kill', ], + 'content-sync_reload' => [ + 'path' => '/ContentSync/job/reload', + 'target' => B13\ContentSync\Backend\Controller\Ajax\JobController::class . '::reload', + ], 'content-sync_collect-garbage' => [ 'path' => '/ContentSync/collectGarbage', 'target' => B13\ContentSync\Backend\Controller\Ajax\JobController::class . '::collectGarbage', diff --git a/Configuration/Icons.php b/Configuration/Icons.php index 21ab63f..0796a78 100644 --- a/Configuration/Icons.php +++ b/Configuration/Icons.php @@ -1,5 +1,7 @@ [ 'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, diff --git a/Configuration/JavaScriptModules.php b/Configuration/JavaScriptModules.php index 5a3ef58..975d8ea 100644 --- a/Configuration/JavaScriptModules.php +++ b/Configuration/JavaScriptModules.php @@ -1,5 +1,7 @@ [ '@b13/content-sync/' => 'EXT:content_sync/Resources/Public/JavaScript/', diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index dd59c9b..ec19adc 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -6,29 +6,7 @@ services: B13\ContentSync\: resource: '../Classes/*' - - B13\ContentSync\Command\StatusReportCommand: - tags: - - name: 'console.command' - command: 'content-sync:status-report' - schedulable: true - B13\ContentSync\Command\RunnerCommand: - tags: - - name: 'console.command' - command: 'content-sync:runner' - schedulable: true - B13\ContentSync\Command\JobCreatorCommand: - tags: - - name: 'console.command' - command: 'content-sync:job:create' - schedulable: true - B13\ContentSync\Command\JobKillerCommand: - tags: - - name: 'console.command' - command: 'content-sync:job:kill' - schedulable: true - B13\ContentSync\Command\CollectGarbageCommand: - tags: - - name: 'console.command' - command: 'content-sync:collect-garbage' - schedulable: true + exclude: + - '../Classes/Domain/Model/*' + - '../Classes/Event/*' + - '../Classes/Exception.php' diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 94b03dc..08b9549 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -27,6 +27,9 @@ Kill Job + + Reload + Created at @@ -71,4 +74,4 @@ - \ No newline at end of file + diff --git a/Resources/Private/Partials/ToolbarItems/Job.html b/Resources/Private/Partials/ToolbarItems/Job.html index 5b2a92d..70e07c9 100644 --- a/Resources/Private/Partials/ToolbarItems/Job.html +++ b/Resources/Private/Partials/ToolbarItems/Job.html @@ -1,4 +1,4 @@ - + : @@ -37,6 +37,7 @@ {job.error} +

diff --git a/Resources/Private/Partials/ToolbarItems/NewJob.html b/Resources/Private/Partials/ToolbarItems/NewJob.html index c3dcd75..f0dd24e 100644 --- a/Resources/Private/Partials/ToolbarItems/NewJob.html +++ b/Resources/Private/Partials/ToolbarItems/NewJob.html @@ -1,4 +1,4 @@ - +