38 Commits

Author SHA1 Message Date
Jean-Baptiste Nahan
26ab698a72 Merge pull request #21 from win32service/revert-20-issue_17_failed_message_never_retried
Revert "issue #17 add code to retry and PHPUnit for tests"
2024-10-23 14:51:46 +02:00
Jean-Baptiste Nahan
764337d0a8 Revert "issue #17 add code to retry and PHPUnit for tests" 2024-10-23 14:51:15 +02:00
Jean-Baptiste Nahan
fbfedf4205 Merge pull request #20 from jbcr/issue_17_failed_message_never_retried
issue #17 add code to retry and PHPUnit for tests
2024-10-23 14:48:10 +02:00
jb cr
896da203d1 update cache action version and cache key 2024-10-22 14:10:59 +02:00
jb cr
29480592cb add tests 2024-10-22 14:06:20 +02:00
jb cr
26dbef58d3 revert mock namespace 2024-10-22 11:20:39 +02:00
jb cr
b78326a847 fix mock 2024-10-22 11:15:16 +02:00
jb cr
5b279891af add symfony/doctrine-messenger 2024-10-22 11:11:13 +02:00
jb cr
0c0a7cc756 change db name and APP_ENV value 2024-10-22 11:08:54 +02:00
jb cr
2cd6704473 try fix mariadb port 2024-10-22 11:06:58 +02:00
jb cr
62e6137287 fix env config 2024-10-22 11:05:03 +02:00
jb cr
55fd2033ad use mariadb 2024-10-22 11:03:57 +02:00
jb cr
08c0b73107 fix database path 2024-10-22 10:59:24 +02:00
jb cr
ff661eea1b fix console path 2024-10-22 10:57:44 +02:00
jb cr
bcee3e0562 set PHP version as string instead of float 2024-10-22 10:57:20 +02:00
jb cr
2025d94208 fix matrix config 2024-10-22 10:54:52 +02:00
jb cr
8a9f74a606 move workflow config 2024-10-22 10:52:58 +02:00
jb cr
68b542430f add branch for run action 2024-10-22 10:52:04 +02:00
jb cr
6644c3410b issue #17 add code to retry and PHPUnit for tests 2024-10-22 10:50:48 +02:00
Jean-Baptiste Nahan
d08e424b1b Merge pull request #16 from jbcr/issue_15_limit_config_option
[issue #15] change word separator from dash to underscore for limit o…
2024-10-21 17:01:26 +02:00
Jean-Baptiste Nahan
3091b0beea Merge pull request #18 from jbcr/issue_18_error_on_stop_service
[issue #19] Remove "throw exception" on service stop request when the…
2024-10-21 17:00:55 +02:00
jb cr
b3bf4ff439 [issue #18] Remove "throw exception" on service stop request when the limit has been over 2024-10-21 15:45:34 +02:00
jb cr
81052cea72 [issue #15] change word separator from dash to underscore for limit option on messenger service configuration 2024-10-21 15:38:04 +02:00
Jean-Baptiste Nahan
09bf839709 Merge pull request #14 from jbcr/2.x
change recovery deffault settings
2024-07-26 14:14:13 +00:00
jb cr
1b99649020 change recovery deffault settings 2024-07-26 16:13:09 +02:00
Jean-Baptiste Nahan
9d41554ee8 Merge pull request #13 from jbcr/2.x
fix name without thread index
2024-07-26 13:09:54 +00:00
jb cr
5799c93164 fix name without thread index 2024-07-26 15:09:30 +02:00
Jean-Baptiste Nahan
f9810c243a Merge pull request #12 from jbcr/2.x
set env from current env on service registration
2024-07-26 12:52:18 +00:00
jb cr
985288b312 fix colision service name 2024-07-26 14:51:10 +02:00
jb cr
1833f89af2 set env from current env on service registration 2024-07-26 10:56:16 +02:00
Jean-Baptiste Nahan
a3e7e6038f Merge pull request #11 from jbcr/1.x
fix version required
2024-07-26 07:23:02 +00:00
jb cr
715e190f5d fix version required 2024-07-26 09:19:28 +02:00
Jean-Baptiste Nahan
6c68bb64a8 Merge pull request #10 from jbcr/1.x
Use Symfony Messenger in Win32Service
2024-07-26 07:17:39 +00:00
jb cr
4fcf4248e6 add Messenger worker 2024-07-25 18:52:32 +02:00
jb cr
e4d56cd3fb messenger work fine 2024-07-25 17:50:11 +02:00
jb cr
6ab7294d2a prepare for Messenger working 2024-07-25 17:28:42 +02:00
jb cr
27523837ef rewite to improve usage 2024-07-25 16:30:25 +02:00
Jean-Baptiste Nahan
926b725f4e allow Symfony 5.4 2024-07-25 10:43:45 +02:00
54 changed files with 1712 additions and 313 deletions

18
.gitignore vendored
View File

@@ -1,4 +1,18 @@
/vendor
/composer.lock
compose.override.yaml
/.php-cs-fixer.cache
###> symfony/framework-bundle ###
/.env.local
/tests/Application/.env.local
/.env.local.php
/tests/Application/.env.local.php
/.env.*.local
/tests/Application/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/tests/Application/public/bundles/
/var/
/tests/Application/var
/vendor/
###< symfony/framework-bundle ###

View File

@@ -20,6 +20,7 @@ win32_service:
channels: # The list of channels whom the processor add the thread number. If empty, the thread number is added for all channels.
- ref1
services:
# Prototype
-
service_id: "" # The service id. If `thread_count` > 1, you can use `%d` to define the place of the thread number
machine: "" # the machine name for this service.
@@ -47,6 +48,22 @@ win32_service:
reset_period: 86400 # The period before reset the fail count (in minutes)
dependencies: # The list of service depends
- Netman # An example of dependency.
messenger: # This configuration allow to set on Windows service the Symfony Messenger Consumer.
# Prototype
-
user: # The User set on the service
account: ~ # the account name
password: ~ # the password
receivers: [] # Symfony Messenger transport consumed by the service
machine: '' # the machine name for this service.
displayed_name: ~ # Required, The friendly name of the service. If `thread_count` > 1, you can use `%d` to define the place of the thread number
description: '' # the service description
thread_count: 1 # the number of this service need to register. Use `%d` into `service_id`, `displayed_name` and `script_params` for contains the service number.
delayed_start: false # If true the starting for the service is delayed
limit: 0 # Reboot service after processed X messages
failure-limit: 0 # Reboot service after X messages failure
time-limit: 0 # Reboot service after X seconds
```
# Define the runner

View File

@@ -20,6 +20,7 @@
},
"autoload-dev": {
"psr-4": {
"Win32ServiceBundle\\Tests\\Application\\": "tests/Application/src/",
"Win32ServiceBundle\\Tests\\": "tests/"
}
},
@@ -27,13 +28,36 @@
"require": {
"php": "^8.0",
"win32service/service-library": "^1.0||1.x-dev",
"symfony/http-kernel": "^6.0",
"symfony/console": "^6.0",
"symfony/config": "^6.0",
"symfony/dependency-injection": "^6.0",
"symfony/event-dispatcher": "^6.0"
"symfony/http-kernel": "^5.4|^6.0",
"symfony/console": "^5.4|^6.0",
"symfony/config": "^5.4|^6.0",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/event-dispatcher": "^5.4|^6.0",
"symfony/yaml": "^5.4|^6.0",
"symfony/framework-bundle": "^5.4|^6.0"
},
"require-dev": {
"rector/rector": "^0.14.2"
"rector/rector": "^0.14.2",
"symfony/dotenv": "^5.4|^6.0",
"symfony/runtime": "^5.4|^6.0",
"symfony/messenger": "^5.4|^6.0",
"symfony/monolog-bundle": "^3.10",
"doctrine/doctrine-bundle": "^2.12",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^2.19"
},
"suggest": {
"ext-win32service": "On Windows only, install this extension to run PHP Service on Windows Service Manager"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "^5.4"
}
},
"config": {
"allow-plugins": {
"symfony/runtime": true
}
}
}

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* @copy Win32Service (c) 2019
* Added by : macintoshplus at 19/02/19 13:59
@@ -6,112 +8,118 @@
namespace Win32ServiceBundle\Command;
use Exception;
use InvalidArgumentException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Win32Service\Model\ServiceIdentifier;
use Win32Service\Model\ServiceInformations;
use Win32Service\Service\ServiceStateManager;
use const atoum\atoum\phar\name;
use Win32ServiceBundle\Service\ServiceConfigurationManager;
#[AsCommand(name: 'win32service:action')]
class ActionServiceCommand extends Command
{
const ALL_SERVICE = 'All';
public const ALL_SERVICE = 'All';
/**
* @var array<string, mixed>
*/
private array $config = [];
protected function configure()
public function __construct(private ServiceConfigurationManager $serviceConfigurationManager)
{
$this->setDescription("Send the action at all service");
$this->addArgument('control', InputArgument::REQUIRED, "The action you want");
$this->addOption('service-name', 's', InputOption::VALUE_REQUIRED, 'Send the controle to the service with service_id. The value must be equal to the configuration.', self::ALL_SERVICE);
$this->addOption('custom-action', 'c', InputOption::VALUE_REQUIRED, 'The custom control send to the service.', null);
parent::__construct();
}
public function defineBundleConfig(array $config) {
$this->config = $config;
protected function configure(): void
{
$this->setDescription('Send the action at all service');
$this->addArgument('control', InputArgument::REQUIRED, 'The action you want');
$this->addOption(
'service-name',
's',
InputOption::VALUE_REQUIRED,
'Send the controle to the service with service_id. The value must be equal to the configuration.',
self::ALL_SERVICE
);
$this->addOption(
'custom-action',
'c',
InputOption::VALUE_REQUIRED,
'The custom control send to the service.',
null
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($this->config === []) {
throw new Exception('The configuration of win32Service is not defined into command');
}
$serviceToAction = $input->getOption('service-name');
$customAction = $input->getOption('custom-action');
$action = $input->getArgument('control');
$adminService = new ServiceStateManager();
$actions = ['start', 'stop', 'pause', 'continue', 'custom'];
if (!in_array($action, $actions)) {
throw new InvalidArgumentException('The value of action argument is invalid. Valid values : '.implode(', ', $actions));
if (!\in_array($action, $actions)) {
throw new \InvalidArgumentException('The value of action argument is invalid. Valid values : '.implode(', ', $actions));
}
if ($action === 'custom' && ($customAction < 128 || $customAction > 255)) {
throw new InvalidArgumentException("The custom control value must be between 128 and 255");
throw new \InvalidArgumentException('The custom control value must be between 128 and 255');
}
$services = $this->config['services'];
if ($serviceToAction !== self::ALL_SERVICE) {
$serviceInfos = $this->serviceConfigurationManager->getServiceInformations($serviceToAction);
$adminService = new ServiceStateManager();
$this->sendAction($adminService, $action, $serviceInfos, $customAction);
$output->writeln('Sending control to <info>'.$serviceInfos->serviceId().'</info> : OK');
return self::SUCCESS;
}
$nbService = 0;
foreach ($services as $service) {
$threadNumber = $service['thread_count'];
for ($i = 0; $i < $threadNumber; $i++) {
$serviceThreadId = sprintf($service['service_id'], $i);
if ($serviceToAction !== self::ALL_SERVICE && $serviceToAction !== $service['service_id'] && $serviceThreadId !== $serviceToAction) {
continue;
}
$nbService++;
//Init the service informations
$serviceInfos = ServiceIdentifier::identify($serviceThreadId, $service['machine']);
try {
switch ($action) {
case 'start':
$adminService->startService($serviceInfos);
break;
case 'stop':
$adminService->stopService($serviceInfos);
break;
case 'pause':
$adminService->pauseService($serviceInfos);
break;
case 'continue':
$adminService->continueService($serviceInfos);
break;
case 'custom':
$adminService->sendCustomControl($serviceInfos, $customAction);
break;
default:
break;
}
$output->writeln('Sending control to <info>' . $serviceInfos->serviceId() . '</info> : OK');
} catch (Exception $e) {
$output->writeln('<error> Error : ' . $serviceInfos->serviceId() . '(' . $e->getCode() . ') ' . $e->getMessage() . ' </error>');
}
foreach ($this->serviceConfigurationManager->getFullServiceList() as $serviceInfos) {
try {
++$nbService;
$this->sendAction($adminService, $action, $serviceInfos, $customAction);
$output->writeln('Sending control to <info>'.$serviceInfos->serviceId().'</info> : OK');
} catch (\Exception $e) {
$output->writeln('<error> Error : '.$serviceInfos->serviceId().'('.$e->getCode().') '.$e->getMessage().' </error>');
}
}
if ($nbService === 0) {
$output->writeln('<info>No signal sent</info>');
return self::FAILURE;
}
$output->writeln(sprintf('Signal sent to <info>%d</info> service(s)', $nbService));
return self::SUCCESS;
}
private function sendAction(
ServiceStateManager $adminService,
string $action,
ServiceInformations $serviceInfos,
?int $customAction
): void {
switch ($action) {
case 'start':
$adminService->startService($serviceInfos);
break;
case 'stop':
$adminService->stopService($serviceInfos);
break;
case 'pause':
$adminService->pauseService($serviceInfos);
break;
case 'continue':
$adminService->continueService($serviceInfos);
break;
case 'custom':
$adminService->sendCustomControl($serviceInfos, $customAction);
break;
default:
break;
}
}
}

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* @copyright Win32Service (c) 2019
* Added by : macintoshplus at 19/02/19 21:18
@@ -6,7 +8,7 @@
namespace Win32ServiceBundle\Command;
use Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
@@ -15,91 +17,62 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Win32Service\Model\ServiceIdentifier;
use Win32Service\Model\RunnerServiceInterface;
use Win32ServiceBundle\Logger\ThreadNumberEvent;
use Win32ServiceBundle\Service\RunnerManager;
use const atoum\atoum\phar\name;
use Win32ServiceBundle\Service\ServiceConfigurationManager;
#[AsCommand(name: 'win32service:run')]
class ExecuteServiceCommand extends Command
{
/**
* @var array<string, mixed>
*/
private array $config = [];
public function __construct(
private ServiceConfigurationManager $serviceConfigurationManager,
private RunnerManager $service,
private ?EventDispatcherInterface $eventDispatcher = null,
private ?LoggerInterface $logger = null
) {
parent::__construct();
}
private ?RunnerManager $service = null;
private ?EventDispatcherInterface $eventDispatcher = null;
protected function configure()
protected function configure(): void
{
$this->setDescription("Run the service");
$this->setDescription('Run the service');
$this->addArgument('service-name', InputArgument::REQUIRED, 'The service name.');
$this->addArgument('thread', InputArgument::REQUIRED, 'Thread number');
$this->addOption('max-run', 'r', InputOption::VALUE_REQUIRED, 'Set the max run');
}
public function defineBundleConfig(array $config) {
$this->config = $config;
}
public function setService(RunnerManager $service) {
$this->service = $service;
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher) {
$this->eventDispatcher = $eventDispatcher;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($this->config === []) {
throw new Exception('The configuration of win32Service is not defined into command');
}
if ($this->service === null) {
throw new Exception('The service runner manager is not defined into command');
}
$serviceName = $input->getArgument('service-name');
$threadNumber = $input->getArgument('thread');
$threadNumber = (int) $input->getArgument('thread') ?? 1;
$maxRun = $input->getOption('max-run');
$infos=$this->getServiceInformation($serviceName, $threadNumber);
if ($infos === null) {
throw new Exception(sprintf('The information for service %s is not found', $serviceName));
}
$infos = $this->serviceConfigurationManager->getServiceInformations($serviceName);
if ($maxRun === null) {
$maxRun = $infos['run_max'];
$maxRun = -1;
}
$runner = $this->service->getRunner($infos['service_id']);
$runner = $this->service->getRunner($this->serviceConfigurationManager->getRunnerAliasForServiceId($serviceName));
if ($runner === null) {
throw new Exception(sprintf('The runner for service "%1$s" is not found. Call method \'add\' on the RunnerManager with the runner instance and the alias "%1$s".', $infos['service_id']));
throw new \Win32ServiceException(sprintf('The runner for service "%1$s" is not found. Call method \'add\' on the RunnerManager with the runner instance and the alias "%1$s".', $infos['service_id']));
}
if ($this->eventDispatcher !== null) {
$event = new ThreadNumberEvent($threadNumber);
$this->eventDispatcher->dispatch($event,ThreadNumberEvent::NAME);
$this->eventDispatcher->dispatch($event, ThreadNumberEvent::NAME);
}
$runner->setServiceId(ServiceIdentifier::identify($serviceName, $infos['machine']));
$runner->setServiceId(ServiceIdentifier::identify($serviceName, $infos->machine()));
$rawConfig = $this->serviceConfigurationManager->getServiceRawConfiguration($serviceName);
$this->logger?->info(
'Configure exit graceful and code',
['exit_graceful' => $rawConfig['exit']['graceful'], 'exit_code' => $rawConfig['exit']['code']]
);
$runner->defineExitModeAndCode($rawConfig['exit']['graceful'], $rawConfig['exit']['code']);
$runner->defineExitModeAndCode($infos['exit']['graceful'], $infos['exit']['code']);
$runner->doRun(intval($maxRun), $threadNumber);
$runner->doRun((int) $maxRun, $threadNumber);
return self::SUCCESS;
}
private function getServiceInformation(string $serviceToRun, $threadNumber) {
foreach ($this->config['services'] as $service) {
if ($serviceToRun === sprintf($service['service_id'], $threadNumber)) {
return $service;
}
}
return null;
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\Command;
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 Symfony\Component\Console\Style\SymfonyStyle;
use Win32Service\Model\RunnerServiceInterface;
use Win32ServiceBundle\DependencyInjection\TagRunnerCompilerPass;
use Win32ServiceBundle\Service\RunnerManager;
use Win32ServiceBundle\Service\ServiceConfigurationManager;
#[AsCommand(name: 'win32service:list', description: 'List all services')]
final class ListServiceCommand extends Command
{
public function __construct(
private RunnerManager $runnerManager,
private ServiceConfigurationManager $serviceConfigurationManager
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('List all services');
$allAlias = array_keys($this->runnerManager->getRunners());
$data = [];
foreach ($this->serviceConfigurationManager->getFullServiceList() as $serviceInformations) {
try {
if (\function_exists('win32_query_service_status') === false) {
throw new \Win32ServiceException('Win32Service extension is not installed.');
}
$status = win32_query_service_status(
$serviceInformations->serviceId(),
$serviceInformations->machine()
);
$status = match ($status['CurrentState']) {
WIN32_SERVICE_CONTINUE_PENDING => 'continue pending',
WIN32_SERVICE_PAUSE_PENDING => 'pause pending',
WIN32_SERVICE_PAUSED => 'paused',
WIN32_SERVICE_RUNNING => 'running',
WIN32_SERVICE_START_PENDING => 'start pending',
WIN32_SERVICE_STOP_PENDING => 'stop pending',
WIN32_SERVICE_STOPPED => 'stopped',
default => 'Unknow',
};
} catch (\Win32ServiceException $exception) {
$status = match ($exception->getCode()) {
WIN32_ERROR_ACCESS_DENIED => 'Access denied',
WIN32_ERROR_SERVICE_DOES_NOT_EXIST => 'Not registred',
default => $exception->getMessage(),
};
}
$runnerTagAlias = $this->serviceConfigurationManager->getRunnerAliasForServiceId($serviceInformations->serviceId());
$data[] = [
empty($serviceInformations->machine()) ? 'localhost' : $serviceInformations->machine(),
$serviceInformations->serviceId(),
\in_array($runnerTagAlias, $allAlias) ? '<info>OK</info>' : sprintf(
'<error>No Symfony service implements "%s" with tag "name: \'%s\', alias: \'%s\'"</error>',
RunnerServiceInterface::class,
TagRunnerCompilerPass::WIN32SERVICE_RUNNER_TAG,
$runnerTagAlias,
),
$status,
$serviceInformations[WIN32_INFO_DISPLAY],
];
}
$io->table(['Machine', 'ServiceId', 'Runner config', 'State', 'Name'], $data);
return self::SUCCESS;
}
}

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* @copy Win32Service (c) 2019
* Added by : macintoshplus at 19/02/19 13:59
@@ -6,126 +8,70 @@
namespace Win32ServiceBundle\Command;
use Exception;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Win32Service\Model\ServiceIdentifier;
use Win32Service\Model\ServiceInformations;
use Win32Service\Service\ServiceAdminManager;
use Win32ServiceBundle\Service\ServiceConfigurationManager;
#[AsCommand(name: 'win32service:register')]
class RegisterServiceCommand extends Command
{
const ALL_SERVICE = 'All';
public const ALL_SERVICE = 'All';
/**
* @var array<string, mixed>
*/
private array $config = [];
private ?string $projectRoot = null;
protected function configure()
public function __construct(private ServiceConfigurationManager $serviceConfigurationManager)
{
$this->setDescription("Register all service into Windows Service Manager");
$this->addOption('service-name', 's', InputOption::VALUE_REQUIRED,
'Register the service with service_id. The value must be equal to the configuration.', self::ALL_SERVICE);
parent::__construct();
}
public function defineBundleConfig(array $config)
protected function configure(): void
{
$this->config = $config;
}
/**
* @required
*/
public function defineProjectRoot(string $projectRoot)
{
$this->projectRoot = $projectRoot;
$this->setDescription('Register all service into Windows Service Manager');
$this->addOption(
'service-name',
's',
InputOption::VALUE_REQUIRED,
'Register the service with service_id. The value must be equal to the configuration.',
self::ALL_SERVICE
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($this->config === []) {
throw new Exception('The configuration of win32Service is not defined into command');
}
$adminService = new ServiceAdminManager();
$serviceToRegister = $input->getOption('service-name');
$services = $this->config['services'];
if ($serviceToRegister !== self::ALL_SERVICE) {
$serviceInfos = $this->serviceConfigurationManager->getServiceInformations($serviceToRegister);
$adminService = new ServiceAdminManager();
$adminService->registerService($serviceInfos);
$output->writeln('Registration success for <info>'.$serviceInfos->serviceId().'</info>');
$windowsLocalEncoding = $this->config['windows_local_encoding'];
return self::SUCCESS;
}
$nbService = 0;
foreach ($services as $service) {
if ($serviceToRegister !== self::ALL_SERVICE && $serviceToRegister !== $service['service_id']) {
continue;
}
$threadNumber = $service['thread_count'];
for ($i = 0; $i < $threadNumber; $i++) {
$nbService++;
//Init the service informations
$serviceThreadId = sprintf($service['service_id'], $i);
$path = $service['script_path'];
$args = sprintf($service['script_params'], $i);
if ($path === null) {
$path = realpath($_SERVER['PHP_SELF']);
//$path = sprintf('%s\\bin\\console', $this->projectRoot);
$args = sprintf('%s %s %d', ExecuteServiceCommand::getDefaultName(), $serviceThreadId, $i);
}
$serviceInfos = new ServiceInformations(
ServiceIdentifier::identify($serviceThreadId, $service['machine']),
mb_convert_encoding(sprintf($service['displayed_name'], $i), $windowsLocalEncoding, 'UTF-8'),
mb_convert_encoding($service['description'], $windowsLocalEncoding, 'UTF-8'),
mb_convert_encoding($path, $windowsLocalEncoding, 'UTF-8'),
mb_convert_encoding($args, $windowsLocalEncoding, 'UTF-8')
);
$serviceInfos->defineIfStartIsDelayed($service['delayed_start']);
$recovery = $service['recovery'];
$serviceInfos->defineRecoverySettings(
$recovery['delay'],
$recovery['enable'],
$recovery['action1'],
$recovery['action2'],
$recovery['action3'],
$recovery['reboot_msg'],
$recovery['command'],
$recovery['reset_period']
);
if ($service['user']['account'] !== null) {
$serviceInfos->defineUserService($service['user']['account'], $service['user']['password']);
}
if (count($service['dependencies']) > 0) {
$serviceInfos->defineDependencies($service['dependencies']);
}
try {
$adminService->registerService($serviceInfos);
$output->writeln('Registration success for <info>' . $serviceInfos->serviceId() . '</info>');
} catch (Exception $e) {
$output->writeln('<error> Error : ' . $serviceInfos->serviceId() . '(' . $e->getCode() . ') ' . $e->getMessage() . ' </error>');
}
foreach ($this->serviceConfigurationManager->getFullServiceList() as $serviceInfos) {
try {
$adminService->registerService($serviceInfos);
++$nbService;
$output->writeln('Registration success for <info>'.$serviceInfos->serviceId().'</info>');
} catch (\Exception $e) {
$output->writeln('<error> Error : '.$serviceInfos->serviceId().'('.$e->getCode().') '.$e->getMessage().' </error>');
}
}
if ($nbService === 0) {
$output->writeln('<info>No service registred</info>');
return self::FAILURE;
}
$output->writeln(sprintf('<info>%d</info> service(s) processed', $nbService));
return self::SUCCESS;
}
}

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* @copy Win32Service (c) 2019
* Added by : macintoshplus at 19/02/19 15:59
@@ -6,77 +8,69 @@
namespace Win32ServiceBundle\Command;
use Exception;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Win32Service\Model\ServiceIdentifier;
use Win32Service\Service\ServiceAdminManager;
use Win32ServiceBundle\Service\ServiceConfigurationManager;
#[AsCommand(name: 'win32service:unregister')]
class UnregisterServiceCommand extends Command
{
const ALL_SERVICE = 'All';
public const ALL_SERVICE = 'All';
/**
* @var array<string, mixed>
*/
private array $config = [];
protected function configure()
public function __construct(private ServiceConfigurationManager $serviceConfigurationManager)
{
$this->setDescription("Unregister all service into Windows Service Manager");
$this->addOption('service-name', 's', InputOption::VALUE_REQUIRED,
'Register the service with service_id. The value must be equal to the configuration.', self::ALL_SERVICE);
parent::__construct();
}
public function defineBundleConfig(array $config)
protected function configure(): void
{
$this->config = $config;
$this->setDescription('Unregister all service into Windows Service Manager');
$this->addOption(
'service-name',
's',
InputOption::VALUE_REQUIRED,
'Register the service with service_id. The value must be equal to the configuration.',
self::ALL_SERVICE
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($this->config === []) {
throw new Exception('The configuration of win32Service is not defined into command');
}
$serviceToRegister = $input->getOption('service-name');
$services = $this->config['services'];
$serviceToUnregister = $input->getOption('service-name');
$adminService = new ServiceAdminManager();
if ($serviceToUnregister !== self::ALL_SERVICE) {
$serviceInfos = $this->serviceConfigurationManager->getServiceInformations($serviceToUnregister);
$adminService->unregisterService($serviceInfos);
$output->writeln('Unregistration success for <info>'.$serviceInfos->serviceId().'</info>');
return self::SUCCESS;
}
$nbService = 0;
foreach ($services as $service) {
if ($serviceToRegister !== self::ALL_SERVICE && $serviceToRegister !== $service['service_id']) {
continue;
}
$threadNumber = $service['thread_count'];
for ($i = 0; $i < $threadNumber; $i++) {
$nbService++;
//Init the service informations
$serviceInfos = ServiceIdentifier::identify(sprintf($service['service_id'], $i), $service['machine']);
try {
$adminService->unregisterService($serviceInfos);
$output->writeln('Unregistration success for <info>' . $serviceInfos->serviceId() . '</info>');
} catch (Exception $e) {
$output->writeln('<error> Error : ' . $serviceInfos->serviceId() . '(' . $e->getCode() . ') ' . $e->getMessage() . ' </error>');
}
foreach ($this->serviceConfigurationManager->getFullServiceList() as $serviceInfos) {
try {
$adminService->unregisterService($serviceInfos);
++$nbService;
$output->writeln('Unregistration success for <info>'.$serviceInfos->serviceId().'</info>');
} catch (\Exception $e) {
$output->writeln('<error> Error : '.$serviceInfos->serviceId().'('.$e->getCode().') '.$e->getMessage().' </error>');
}
}
if ($nbService === 0) {
$output->writeln('<info>No service unregistred</info>');
return self::FAILURE;
}
$output->writeln(sprintf('<info>%d</info> service(s) processed', $nbService));
return self::SUCCESS;
}
}

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* @copy Win32Service (c) 2019
* Added by : macintoshplus at 19/02/19 13:34
@@ -18,6 +20,13 @@ class Configuration implements ConfigurationInterface
$treeBuilder->getRootNode()
->children()
->scalarNode('windows_local_encoding')->defaultValue('ISO-8859-15')->end()
->scalarNode('project_code')->isRequired()->cannotBeEmpty()->info('Project specific code to distinguish service ID')
->validate()
->ifTrue(function ($value) {
return \is_string($value) === false || \strlen($value) > 5 || \strlen($value) < 2;
})->thenInvalid('Invalid project code (string length between 2 and 5 chars)')
->end()
->end()
->arrayNode('logging_extra')
->addDefaultsIfNotSet()
->children()
@@ -72,8 +81,8 @@ class Configuration implements ConfigurationInterface
->values([WIN32_SC_ACTION_NONE, WIN32_SC_ACTION_REBOOT, WIN32_SC_ACTION_RESTART, WIN32_SC_ACTION_RUN_COMMAND])
->defaultValue(WIN32_SC_ACTION_NONE)
->end()
->scalarNode('reboot_msg')->defaultValue("")->end()
->scalarNode('command')->defaultValue("")->end()
->scalarNode('reboot_msg')->defaultValue('')->end()
->scalarNode('command')->defaultValue('')->end()
->integerNode('reset_period')->defaultValue(86400)->min(1)->end()
->end()
->end()
@@ -85,6 +94,31 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->end()
->arrayNode('messenger')
->arrayPrototype()
->children()
->arrayNode('user')
->addDefaultsIfNotSet()
->children()
->scalarNode('account')->defaultNull()->end()
->scalarNode('password')->defaultNull()->end()
->end()
->end()
->arrayNode('receivers')
->scalarPrototype()->end()
->end()
->scalarNode('machine')->defaultValue('')->end()
->scalarNode('displayed_name')->isRequired()->cannotBeEmpty()->end()
->scalarNode('description')->defaultValue('')->end()
->integerNode('thread_count')->defaultValue(1)->min(1)->end()
->booleanNode('delayed_start')->defaultFalse()->end()
->integerNode('limit')->defaultValue(0)->min(0)->end()
->integerNode('failure_limit')->defaultValue(0)->min(0)->end()
->integerNode('time_limit')->defaultValue(0)->min(0)->end()
->scalarNode('memory_limit')->defaultValue('')->end()
->end()
->end()
->end()
->end()
;

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
use Symfony\Component\DependencyInjection\Reference;
final class MessengerPass implements CompilerPassInterface
{
private string $busTag = 'messenger.bus';
private string $receiverTag = 'messenger.receiver';
private string $win32ServiceRunnerTag = TagRunnerCompilerPass::WIN32SERVICE_RUNNER_TAG.'.messenger';
public function process(ContainerBuilder $container): void
{
$busIds = [];
foreach ($container->findTaggedServiceIds($this->busTag) as $busId => $tags) {
$busIds[] = $busId;
}
$receiverMapping = [];
foreach ($container->findTaggedServiceIds($this->receiverTag) as $id => $tags) {
$receiverMapping[$id] = new Reference($id);
foreach ($tags as $tag) {
if (isset($tag['alias'])) {
$receiverMapping[$tag['alias']] = $receiverMapping[$id];
}
}
}
$receiverNames = [];
foreach ($receiverMapping as $name => $reference) {
$receiverNames[(string) $reference] = $name;
}
foreach ($container->findTaggedServiceIds($this->win32ServiceRunnerTag) as $win32ServiceId => $tags) {
$serviceRunnerDefinition = $container->getDefinition($win32ServiceId);
$serviceRunnerDefinition->replaceArgument(1, new Reference('messenger.routable_message_bus'));
$serviceRunnerDefinition->replaceArgument(6, array_values($receiverNames));
try {
$serviceRunnerDefinition->replaceArgument(8, $busIds);
} catch (OutOfBoundsException $e) {
// ignore to preserve compatibility with symfony/framework-bundle < 5.4
}
}
}
}

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* @copyright Macintoshplus (c) 2019
* Added by : Macintoshplus at 19/02/19 23:09
@@ -6,14 +8,16 @@
namespace Win32ServiceBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Win32ServiceBundle\Service\RunnerManager;
class TagRunnerCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
public const WIN32SERVICE_RUNNER_TAG = 'win32service.runner';
public function process(ContainerBuilder $container): void
{
if (!$container->has(RunnerManager::class)) {
return;
@@ -22,17 +26,22 @@ class TagRunnerCompilerPass implements CompilerPassInterface
$definition = $container->findDefinition(RunnerManager::class);
// find all service IDs with the app.mail_transport tag
$taggedServices = $container->findTaggedServiceIds('win32service.runner');
$taggedServices = $container->findTaggedServiceIds(self::WIN32SERVICE_RUNNER_TAG);
foreach ($taggedServices as $id => $tags) {
$serviceDefinition = $container->findDefinition($id);
$class = $serviceDefinition->getClass();
$alias = null;
if (method_exists($class, 'getAlias')) {
$alias = $class::getAlias();
}
// a service could have the same tag twice
foreach ($tags as $attributes) {
$definition->addMethodCall('addRunner', [
new Reference($id),
$attributes["alias"]
$alias ?? $attributes['alias'],
]);
}
}
}
}
}

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* @copy Win32Service (c) 2019
* Added by : macintoshplus at 19/02/19 13:35
@@ -6,58 +8,135 @@
namespace Win32ServiceBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Argument\AbstractArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Win32ServiceBundle\Logger\ThreadNumberEvent;
use Win32ServiceBundle\Logger\ThreadNumberProcessor;
use Win32ServiceBundle\Model\MessengerServiceRunner;
class Win32ServiceExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$config = $this->processMessengerConfig($config);
$container->setParameter('win32service.config', $config);
$loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
$this->processMessenger($config['messenger'], $config['project_code'], $container);
$loader = new YamlFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config'));
$loader->load('services.yaml');
if (!$config['logging_extra']['enable']) {
return;
}
//New service definition
// New service definition
$definition = new Definition(ThreadNumberProcessor::class);
//If already register, get the current definition
// If already register, get the current definition
if ($container->hasDefinition(ThreadNumberProcessor::class)) {
$definition = $container->findDefinition(ThreadNumberProcessor::class);
}
//If no definition, add the definition into container
// If no definition, add the definition into container
if (!$container->hasDefinition(ThreadNumberProcessor::class)) {
$container->setDefinition(ThreadNumberProcessor::class, $definition);
}
//Add the tag for receive the thread number event
// Add the tag for receive the thread number event
if (!$definition->hasTag('kernel.event_listener')) {
$definition->addTag('kernel.event_listener', ['event'=>ThreadNumberEvent::NAME, 'method'=>'setThreadNumber']);
$definition->addTag(
'kernel.event_listener',
['event' => ThreadNumberEvent::NAME, 'method' => 'setThreadNumber']
);
}
//Add tags for each channel defined
// Add tags for each channel defined
$channels = $config['logging_extra']['channels'];
if (count($channels)>0) {
if (\count($channels) > 0) {
foreach ($channels as $channel) {
$definition->addTag('monolog.processor', ['channel' => $channel]);
}
return;
}
//If no channels defined, the processor is enable for all
// If no channels defined, the processor is enable for all
$definition->addTag('monolog.processor');
}
public function processMessenger(array $messengerConfig, string $projectCode, ContainerBuilder $container): void
{
foreach ($messengerConfig as $service) {
$name = sprintf(
MessengerServiceRunner::SERVICE_TAG_PATTERN,
$projectCode,
implode('_', $service['receivers'])
);
$arguments = [
$service,
new AbstractArgument('Routable message bus'),
new Reference('messenger.receiver_locator'),
new Reference('event_dispatcher'),
new Reference('messenger.bus.default'),
new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE),
[],
new Reference('messenger.listener.reset_services', ContainerInterface::NULL_ON_INVALID_REFERENCE),
[],
];
$serviceDefinition = new Definition(MessengerServiceRunner::class, $arguments);
$serviceDefinition->addTag(TagRunnerCompilerPass::WIN32SERVICE_RUNNER_TAG, ['alias' => $name]);
$serviceDefinition->addTag(TagRunnerCompilerPass::WIN32SERVICE_RUNNER_TAG.'.messenger');
$container->setDefinition($name, $serviceDefinition);
}
}
private function processMessengerConfig(array $config): array
{
foreach ($config['messenger'] as $service) {
$strlen = \strlen((string) ($service['thread_count'] - 1)) - 2;
$templatedName = sprintf(
MessengerServiceRunner::SERVICE_TAG_PATTERN,
$config['project_code'],
implode('_', $service['receivers'])
);
if (($totalLength = $strlen + \strlen($templatedName)) > 80) {
throw new \Win32ServiceException(sprintf('The future service identity length "%s" is over 80 chars (%d). Reduce the project code or "receivers" number or name length to keep less than 80 chars.', sprintf($templatedName, $service['thread_count'] - 1), $totalLength));
}
$config['services'][] = [
'machine' => $service['machine'],
'displayed_name' => $service['displayed_name'],
'description' => $service['description'],
'delayed_start' => $service['delayed_start'],
'user' => $service['user'],
'thread_count' => $service['thread_count'],
'script_path' => null,
'script_params' => '',
'service_id' => $templatedName,
'recovery' => [
'enable' => true,
'delay' => 60_000,
'action1' => WIN32_SC_ACTION_RESTART,
'action2' => WIN32_SC_ACTION_RESTART,
'action3' => WIN32_SC_ACTION_RESTART,
'reboot_msg' => '',
'command' => '',
'reset_period' => 1,
],
'exit' => [
'graceful' => false,
'code' => 1,
],
'dependencies' => [],
];
}
return $config;
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\Event;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\AbstractWorkerMessageEvent;
final class MessengerWorkerMessageFailedEvent extends AbstractWorkerMessageEvent
{
private \Throwable $throwable;
private bool $willRetry = false;
public function __construct(Envelope $envelope, string $receiverName, \Throwable $error)
{
$this->throwable = $error;
parent::__construct($envelope, $receiverName);
}
public function getThrowable(): \Throwable
{
return $this->throwable;
}
public function willRetry(): bool
{
return $this->willRetry;
}
public function setForRetry(): void
{
$this->willRetry = true;
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\Event;
use Symfony\Component\Messenger\Event\AbstractWorkerMessageEvent;
final class MessengerWorkerMessageHandledEvent extends AbstractWorkerMessageEvent
{
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\Event;
use Win32ServiceBundle\Model\MessengerServiceRunner;
final class MessengerWorkerRunningEvent
{
public function __construct(private MessengerServiceRunner $messengerServiceRunner, private bool $isWorkerIdle)
{
}
public function getMessengerServiceRunner(): MessengerServiceRunner
{
return $this->messengerServiceRunner;
}
public function isWorkerIdle(): bool
{
return $this->isWorkerIdle;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\Event;
use Win32ServiceBundle\Model\MessengerServiceRunner;
final class MessengerWorkerStartedEvent
{
public function __construct(
private MessengerServiceRunner $messengerServiceRunner
) {
}
public function getMessengerServiceRunner(): MessengerServiceRunner
{
return $this->messengerServiceRunner;
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\MessengerSubscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Win32ServiceBundle\Event\MessengerWorkerMessageFailedEvent;
use Win32ServiceBundle\Event\MessengerWorkerRunningEvent;
final class StopWorkerOnFailureLimitListener implements EventSubscriberInterface
{
private int $maximumNumberOfFailures;
private ?LoggerInterface $logger;
private int $failedMessages = 0;
public function __construct(int $maximumNumberOfFailures, ?LoggerInterface $logger = null)
{
$this->maximumNumberOfFailures = $maximumNumberOfFailures;
$this->logger = $logger;
if ($maximumNumberOfFailures <= 0) {
throw new InvalidArgumentException('Failure limit must be greater than zero.');
}
}
public function onMessageFailed(MessengerWorkerMessageFailedEvent $event): void
{
++$this->failedMessages;
}
public function onWorkerRunning(MessengerWorkerRunningEvent $event): void
{
if (!$event->isWorkerIdle() && $this->failedMessages >= $this->maximumNumberOfFailures) {
$this->failedMessages = 0;
$event->getMessengerServiceRunner()->stop();
if ($this->logger !== null) {
$this->logger->info('Worker stopped due to limit of {count} failed message(s) is reached', ['count' => $this->maximumNumberOfFailures]);
}
}
}
public static function getSubscribedEvents(): array
{
return [
MessengerWorkerMessageFailedEvent::class => 'onMessageFailed',
MessengerWorkerRunningEvent::class => 'onWorkerRunning',
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\MessengerSubscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Win32ServiceBundle\Event\MessengerWorkerRunningEvent;
final class StopWorkerOnMemoryLimitListener implements EventSubscriberInterface
{
private int $memoryLimit;
private ?LoggerInterface $logger;
private $memoryResolver;
public function __construct(int $memoryLimit, ?LoggerInterface $logger = null, ?callable $memoryResolver = null)
{
$this->memoryLimit = $memoryLimit;
$this->logger = $logger;
$this->memoryResolver = $memoryResolver ?: static function () {
return memory_get_usage(true);
};
}
public function onWorkerRunning(MessengerWorkerRunningEvent $event): void
{
$memoryResolver = $this->memoryResolver;
$usedMemory = $memoryResolver();
if ($usedMemory > $this->memoryLimit) {
$event->getMessengerServiceRunner()->stop();
if ($this->logger !== null) {
$this->logger->info('Worker stopped due to memory limit of {limit} bytes exceeded ({memory} bytes used)', ['limit' => $this->memoryLimit, 'memory' => $usedMemory]);
}
}
}
public static function getSubscribedEvents(): array
{
return [
MessengerWorkerRunningEvent::class => 'onWorkerRunning',
];
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\MessengerSubscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Win32ServiceBundle\Event\MessengerWorkerRunningEvent;
final class StopWorkerOnMessageLimitListener implements EventSubscriberInterface
{
private int $maximumNumberOfMessages;
private ?LoggerInterface $logger;
private int $receivedMessages = 0;
public function __construct(int $maximumNumberOfMessages, ?LoggerInterface $logger = null)
{
$this->maximumNumberOfMessages = $maximumNumberOfMessages;
$this->logger = $logger;
if ($maximumNumberOfMessages <= 0) {
throw new InvalidArgumentException('Message limit must be greater than zero.');
}
}
public function onWorkerRunning(MessengerWorkerRunningEvent $event): void
{
if (!$event->isWorkerIdle() && ++$this->receivedMessages >= $this->maximumNumberOfMessages) {
$this->receivedMessages = 0;
$event->getMessengerServiceRunner()->stop();
if ($this->logger !== null) {
$this->logger->info('Worker stopped due to maximum count of {count} messages processed', ['count' => $this->maximumNumberOfMessages]);
}
}
}
public static function getSubscribedEvents(): array
{
return [
MessengerWorkerRunningEvent::class => 'onWorkerRunning',
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\MessengerSubscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Exception\InvalidArgumentException;
use Win32ServiceBundle\Event\MessengerWorkerRunningEvent;
use Win32ServiceBundle\Event\MessengerWorkerStartedEvent;
final class StopWorkerOnTimeLimitListener implements EventSubscriberInterface
{
private int $timeLimitInSeconds;
private ?LoggerInterface $logger;
private $endTime;
public function __construct(int $timeLimitInSeconds, ?LoggerInterface $logger = null)
{
$this->timeLimitInSeconds = $timeLimitInSeconds;
$this->logger = $logger;
if ($timeLimitInSeconds <= 0) {
throw new InvalidArgumentException('Time limit must be greater than zero.');
}
}
public function onWorkerStarted(): void
{
$startTime = microtime(true);
$this->endTime = $startTime + $this->timeLimitInSeconds;
}
public function onWorkerRunning(MessengerWorkerRunningEvent $event): void
{
if ($this->endTime < microtime(true)) {
$event->getMessengerServiceRunner()->stop();
if ($this->logger !== null) {
$this->logger->info('Worker stopped due to time limit of {timeLimit}s exceeded', ['timeLimit' => $this->timeLimitInSeconds]);
}
}
}
public static function getSubscribedEvents(): array
{
return [
MessengerWorkerStartedEvent::class => 'onWorkerStarted',
MessengerWorkerRunningEvent::class => 'onWorkerRunning',
];
}
}

View File

@@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\Model;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
use Symfony\Component\Messenger\EventListener\ResetServicesListener;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\RejectRedeliveredMessageException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\RoutableMessageBus;
use Symfony\Component\Messenger\Stamp\AckStamp;
use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp;
use Symfony\Component\Messenger\Stamp\FlushBatchHandlersStamp;
use Symfony\Component\Messenger\Stamp\NoAutoAckStamp;
use Symfony\Component\Messenger\Stamp\ReceivedStamp;
use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface;
use Win32Service\Model\AbstractServiceRunner;
use Win32ServiceBundle\Event\MessengerWorkerMessageFailedEvent;
use Win32ServiceBundle\Event\MessengerWorkerMessageHandledEvent;
use Win32ServiceBundle\Event\MessengerWorkerRunningEvent;
use Win32ServiceBundle\Event\MessengerWorkerStartedEvent;
use Win32ServiceBundle\MessengerSubscriber\StopWorkerOnFailureLimitListener;
use Win32ServiceBundle\MessengerSubscriber\StopWorkerOnMemoryLimitListener;
use Win32ServiceBundle\MessengerSubscriber\StopWorkerOnMessageLimitListener;
use Win32ServiceBundle\MessengerSubscriber\StopWorkerOnTimeLimitListener;
final class MessengerServiceRunner extends AbstractServiceRunner
{
public const SERVICE_TAG_PATTERN = 'win32service.%s.messenger.%s.%%s';
/**
* @var array<string, ReceiverInterface>
*/
private array $receivers;
private bool $shouldStop = false;
private array $acks = [];
private \SplObjectStorage $unacks;
public function __construct(
private array $config,
private RoutableMessageBus $routableBus,
private ContainerInterface $receiverLocator,
private EventDispatcherInterface $eventDispatcher,
private MessageBusInterface $bus,
private ?LoggerInterface $logger = null,
private array $receiverNames = [],
private ?ResetServicesListener $resetServicesListener = null,
private array $busIds = [],
) {
$this->unacks = new \SplObjectStorage();
}
protected function setup(): void
{
$this->config['sleep'] = $this->config['sleep'] ?? 1000000;
$this->eventDispatcher->addSubscriber($this->resetServicesListener);
$limit = (int) $this->config['limit'];
if ($limit > 0) {
$this->eventDispatcher->addSubscriber(new StopWorkerOnMessageLimitListener($limit, $this->logger));
}
$failureLimit = (int) $this->config['failure_limit'];
if ($failureLimit > 0) {
$this->eventDispatcher->addSubscriber(new StopWorkerOnFailureLimitListener($failureLimit, $this->logger));
}
$timeLimit = (int) $this->config['time_limit'];
if ($timeLimit > 0) {
$this->eventDispatcher->addSubscriber(new StopWorkerOnTimeLimitListener($timeLimit, $this->logger));
}
$memoryLimit = (string) $this->config['memory_limit'];
if ($memoryLimit > 0) {
$this->eventDispatcher->addSubscriber(new StopWorkerOnMemoryLimitListener(
$this->convertToBytes($memoryLimit),
$this->logger
));
}
$this->receivers = [];
foreach ($this->config['receivers'] as $receiverName) {
if (!$this->receiverLocator->has($receiverName)) {
$message = \sprintf('The receiver "%s" does not exist.', $receiverName);
if ($this->receiverNames) {
$message .= \sprintf(' Valid receivers are: %s.', implode(', ', $this->receiverNames));
}
throw new RuntimeException($message);
}
$this->receivers[$receiverName] = $this->receiverLocator->get($receiverName);
}
}
public function stop(): void
{
if ($this->logger !== null) {
$this->logger->info('Stopping worker.', ['transport_names' => array_keys($this->receivers)]);
}
$this->shouldStop = true;
$this->requestStop();
}
protected function beforeContinue(): void
{
// TODO: Implement beforeContinue() method.
}
protected function beforePause(): void
{
// TODO: Implement beforePause() method.
}
protected function run(int $control): void
{
$this->eventDispatcher->dispatch(new MessengerWorkerStartedEvent($this));
$envelopeHandled = false;
$envelopeHandledStart = microtime(true);
foreach ($this->receivers as $transportName => $receiver) {
$envelopes = $receiver->get();
foreach ($envelopes as $envelope) {
$envelopeHandled = true;
$this->handleMessage($envelope, $transportName);
$this->eventDispatcher->dispatch(new MessengerWorkerRunningEvent($this, false));
if ($this->shouldStop) {
break 2;
}
}
if ($envelopeHandled) {
break;
}
}
if (!$envelopeHandled && $this->flush(false)) {
return;
}
if (!$envelopeHandled) {
$this->eventDispatcher->dispatch(new MessengerWorkerRunningEvent($this, true));
if (0 < $sleep = (int) ($this->config['sleep'] - 1e6 * (microtime(true) - $envelopeHandledStart))) {
usleep($sleep);
}
}
}
protected function lastRunIsTooSlow(float $duration): void
{
$this->logger->info('Last run is too low. Max 30s.', ['duration' => $duration]);
}
protected function beforeStop(): void
{
// TODO: Implement beforeStop() method.
}
private function handleMessage(Envelope $envelope, string $transportName): void
{
$event = new WorkerMessageReceivedEvent($envelope, $transportName);
$this->eventDispatcher->dispatch($event);
$envelope = $event->getEnvelope();
if (!$event->shouldHandle()) {
return;
}
$acked = false;
$ack = function (Envelope $envelope, ?\Throwable $e = null) use ($transportName, &$acked) {
$acked = true;
$this->acks[] = [$transportName, $envelope, $e];
};
try {
$e = null;
$envelope = $this->bus->dispatch($envelope->with(
new ReceivedStamp($transportName),
new ConsumedByWorkerStamp(),
new AckStamp($ack)
));
} catch (\Throwable $e) {
}
$noAutoAckStamp = $envelope->last(NoAutoAckStamp::class);
if (!$acked && !$noAutoAckStamp) {
$this->acks[] = [$transportName, $envelope, $e];
} elseif ($noAutoAckStamp) {
$this->unacks[$noAutoAckStamp->getHandlerDescriptor()->getBatchHandler()] = [
$envelope->withoutAll(AckStamp::class),
$transportName,
];
}
$this->ack();
}
private function ack(): bool
{
$acks = $this->acks;
$this->acks = [];
foreach ($acks as [$transportName, $envelope, $e]) {
$receiver = $this->receivers[$transportName];
if ($e !== null) {
if ($rejectFirst = $e instanceof RejectRedeliveredMessageException) {
// redelivered messages are rejected first so that continuous failures in an event listener or while
// publishing for retry does not cause infinite redelivery loops
$receiver->reject($envelope);
}
if ($e instanceof HandlerFailedException) {
$envelope = $e->getEnvelope();
}
$failedEvent = new MessengerWorkerMessageFailedEvent($envelope, $transportName, $e);
$this->eventDispatcher->dispatch($failedEvent);
$envelope = $failedEvent->getEnvelope();
if (!$rejectFirst) {
$receiver->reject($envelope);
}
continue;
}
$handledEvent = new MessengerWorkerMessageHandledEvent($envelope, $transportName);
$this->eventDispatcher->dispatch($handledEvent);
$envelope = $handledEvent->getEnvelope();
if ($this->logger !== null) {
$message = $envelope->getMessage();
$context = [
'class' => $message::class,
];
$this->logger->info('{class} was handled successfully (acknowledging to transport).', $context);
}
$receiver->ack($envelope);
}
return (bool) $acks;
}
private function flush(bool $force): bool
{
$unacks = $this->unacks;
if (!$unacks->count()) {
return false;
}
$this->unacks = new \SplObjectStorage();
foreach ($unacks as $batchHandler) {
[$envelope, $transportName] = $unacks[$batchHandler];
try {
$this->bus->dispatch($envelope->with(new FlushBatchHandlersStamp($force)));
$envelope = $envelope->withoutAll(NoAutoAckStamp::class);
unset($unacks[$batchHandler], $batchHandler);
} catch (\Throwable $e) {
$this->acks[] = [$transportName, $envelope, $e];
}
}
return $this->ack();
}
private function convertToBytes(string $memoryLimit): int
{
$memoryLimit = strtolower($memoryLimit);
$max = ltrim($memoryLimit, '+');
if (str_starts_with($max, '0x')) {
$max = \intval($max, 16);
} elseif (str_starts_with($max, '0')) {
$max = \intval($max, 8);
} else {
$max = (int) $max;
}
switch (substr(rtrim($memoryLimit, 'b'), -1)) {
case 't':
$max *= 1024;
// no break
case 'g':
$max *= 1024;
// no break
case 'm':
$max *= 1024;
// no break
case 'k':
$max *= 1024;
}
return $max;
}
}

View File

@@ -3,29 +3,12 @@ services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
$projectRoot: '%kernel.project_dir%'
Win32ServiceBundle\:
resource: '../../*'
exclude: '../../{DependencyInjection,Entity,Logger,Migrations,Model,Tests,Win32ServiceBundle.php}'
exclude: '../../{DependencyInjection,Entity,Logger,MessengerSubscriber,Migrations,Model,Tests,Win32ServiceBundle.php}'
Win32ServiceBundle\Command\RegisterServiceCommand:
autowire: true
calls:
- ['defineBundleConfig', ['%win32service.config%']]
Win32ServiceBundle\Command\UnregisterServiceCommand:
autowire: true
calls:
- ['defineBundleConfig', ['%win32service.config%']]
Win32ServiceBundle\Command\ActionServiceCommand:
autowire: true
calls:
- ['defineBundleConfig', ['%win32service.config%']]
Win32ServiceBundle\Command\ExecuteServiceCommand:
autowire: true
calls:
- ['defineBundleConfig', ['%win32service.config%']]
- ['setService', ['@Win32ServiceBundle\Service\RunnerManager']]
- ['setEventDispatcher', ['@event_dispatcher']]
Win32ServiceBundle\Service\ServiceConfigurationManager:
arguments:
- '%win32service.config%'
- '%kernel.environment%'

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* @copyright Macintoshplus (c) 2019
* Added by : Macintoshplus at 19/02/19 23:06
@@ -13,25 +15,21 @@ class RunnerManager
/**
* @var RunnerServiceInterface[]
*/
private $runner;
private array $runner = [];
public function __construct()
public function addRunner(RunnerServiceInterface $runner, string $alias): void
{
$this->runner = [];
}
public function addRunner(RunnerServiceInterface $runner, string $alias) {
$this->runner[$alias] = $runner;
}
/**
* @return RunnerServiceInterface|null
*/
public function getRunner(string $alias) {
if (!isset($this->runner[$alias])) {
return null;
}
return $this->runner[$alias];
public function getRunner(string $alias): ?RunnerServiceInterface
{
return $this->runner[$alias] ?? null;
}
}
/** @return array<string, RunnerServiceInterface> */
public function getRunners(): array
{
return $this->runner;
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\Service;
use Win32Service\Model\ServiceIdentifier;
use Win32Service\Model\ServiceInformations;
use Win32ServiceBundle\Command\ExecuteServiceCommand;
final class ServiceConfigurationManager
{
private array $serviceIds = [];
private array $serviceIdsToRunnerAlias = [];
public function __construct(private array $configuration, string $environment)
{
if ($configuration === []) {
throw new \Win32ServiceException('The configuration of win32Service is not defined');
}
$services = $configuration['services'];
foreach ($services as $service) {
$threadNumber = $service['thread_count'];
$runnerAlias = $service['service_id'];
$runnerName = $service['displayed_name'];
$scriptParams = $service['script_params'];
$scriptPath = $service['script_path'];
for ($i = 0; $i < $threadNumber; ++$i) {
$serviceThreadId = sprintf($runnerAlias, $i);
$runnerNameId = sprintf($runnerName, $i);
$path = $service['script_path'];
$args = sprintf($scriptParams, $i);
if ($scriptPath === null) {
$path = realpath($_SERVER['PHP_SELF']);
$args = sprintf('-e %s %s %s %d', $environment, ExecuteServiceCommand::getDefaultName(), $serviceThreadId, $i);
}
$service['service_id'] = $serviceThreadId;
$service['displayed_name'] = $runnerNameId;
$service['script_path'] = $path;
$service['script_params'] = $args;
if (isset($this->serviceIds[$serviceThreadId]) === true) {
throw new \Win32ServiceException(sprintf('The Win32Service "%s" is already defined. if the parameter "thread_count" is greater than 1, please add "%d" in "service_id" parameter. Otherwise, check if no other service have same name.', $serviceThreadId));
}
$this->serviceIds[$serviceThreadId] = $service;
$this->serviceIdsToRunnerAlias[$serviceThreadId] = $runnerAlias;
}
}
}
/** @return \Generator<int, ServiceInformations> */
public function getFullServiceList(): \Generator
{
foreach ($this->serviceIds as $serviceId => $service) {
yield $this->getServiceInformations($serviceId);
}
}
public function getServiceRawConfiguration(string $serviceId): array
{
if (isset($this->serviceIds[$serviceId]) === false) {
throw new \Win32ServiceException(sprintf('The Win32Service "%s" is not defined.', $serviceId));
}
return $this->serviceIds[$serviceId];
}
public function getServiceInformations(string $serviceId): ServiceInformations
{
if (isset($this->serviceIds[$serviceId]) === false) {
throw new \Win32ServiceException(sprintf('The Win32Service "%s" is not defined.', $serviceId));
}
$service = $this->serviceIds[$serviceId];
$windowsLocalEncoding = $this->configuration['windows_local_encoding'];
$serviceInfos = new ServiceInformations(
ServiceIdentifier::identify($serviceId, $service['machine']),
mb_convert_encoding($service['displayed_name'], $windowsLocalEncoding, 'UTF-8'),
mb_convert_encoding($service['description'], $windowsLocalEncoding, 'UTF-8'),
mb_convert_encoding($service['script_path'], $windowsLocalEncoding, 'UTF-8'),
mb_convert_encoding($service['script_params'], $windowsLocalEncoding, 'UTF-8')
);
$serviceInfos->defineIfStartIsDelayed($service['delayed_start']);
$recovery = $service['recovery'];
$serviceInfos->defineRecoverySettings(
$recovery['delay'],
$recovery['enable'],
$recovery['action1'],
$recovery['action2'],
$recovery['action3'],
$recovery['reboot_msg'],
$recovery['command'],
$recovery['reset_period']
);
if ($service['user']['account'] !== null) {
$serviceInfos->defineUserService($service['user']['account'], $service['user']['password']);
}
if (\count($service['dependencies']) > 0) {
$serviceInfos->defineDependencies($service['dependencies']);
}
return $serviceInfos;
}
public function getRunnerAliasForServiceId(string $serviceId): string
{
return $this->serviceIdsToRunnerAlias[$serviceId] ?? throw new \Win32ServiceException('The Win32Service "'.$serviceId.'" have no alias defined.');
}
}

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* @copy Win32Service (c) 2019
* Added by : macintoshplus at 19/02/19 13:30
@@ -6,14 +8,20 @@
namespace Win32ServiceBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Win32Service\Model\RunnerServiceInterface;
use Win32ServiceBundle\DependencyInjection\MessengerPass;
use Win32ServiceBundle\DependencyInjection\TagRunnerCompilerPass;
class Win32ServiceBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$autoconfig = $container->registerForAutoconfiguration(RunnerServiceInterface::class);
$autoconfig->addTag(TagRunnerCompilerPass::WIN32SERVICE_RUNNER_TAG);
$container->addCompilerPass(new TagRunnerCompilerPass());
$container->addCompilerPass(new MessengerPass());
}
}

32
tests/Application/.env Normal file
View File

@@ -0,0 +1,32 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=625318e78d9582a45c1483b809a1e780
###< symfony/framework-bundle ###
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###
#DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
#DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
DATABASE_URL="mysql://user:nopassword@127.0.0.1:3306/app?serverVersion=mariadb-10.11.2&charset=utf8mb4"
#DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"

10
tests/Application/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###

View File

@@ -0,0 +1 @@
8.0

25
tests/Application/bin/console Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env php
<?php
use Win32ServiceBundle\Tests\Application\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_dir(dirname(__DIR__, 3) . '/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
}
if (!is_file(dirname(__DIR__, 3) . '/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
$_SERVER['APP_RUNTIME_OPTIONS'] = [
'project_dir' => dirname(__DIR__),
];
require_once dirname(__DIR__, 3) . '/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool)$context['APP_DEBUG']);
return new Application($kernel);
};

View File

@@ -0,0 +1,4 @@
services:
db:
ports:
- "3306:3306"

View File

@@ -0,0 +1,13 @@
services:
db:
image: mariadb:10.11
environment:
MYSQL_DATABASE: app
MYSQL_ROOT_PASSWORD: nopassword
MYSQL_USER: user
MYSQL_PASSWORD: nopassword
volumes:
- win32service_bundle_mariadb_data:/var/lib/mysql
volumes:
win32service_bundle_mariadb_data: ~

View File

@@ -0,0 +1,4 @@
{
"type": "project",
"license": "proprietary"
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Win32ServiceBundle\Win32ServiceBundle::class => ['all' => true],
];

View File

@@ -0,0 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null

View File

@@ -0,0 +1,52 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: false # set true when minimal version of Symfony is 6.2 + composer req --dev symfony/var-exporter
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
controller_resolver:
auto_mapping: false
when@test:
doctrine:
dbal:
# "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false

View File

@@ -0,0 +1,24 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
#csrf_protection: true
http_method_override: false
# Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support.
session:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
#esi: true
#fragments: true
php_errors:
log: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file

View File

@@ -0,0 +1,18 @@
framework:
messenger:
# reset services after consuming messages
reset_on_message: true
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async: '%env(MESSENGER_TRANSPORT_DSN)%'
failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
routing:
# Route your messages to the transports
# 'App\Message\YourMessage': async
'Win32ServiceBundle\Tests\Application\Event\TestMessage': async

View File

@@ -0,0 +1,62 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event", "!doctrine"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: stream
path: php://stderr
level: debug
formatter: monolog.formatter.json
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: stream
channels: [deprecation]
path: php://stderr
formatter: monolog.formatter.json

View File

@@ -0,0 +1,12 @@
framework:
router:
utf8: true
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost
when@prod:
framework:
router:
strict_requirements: null

View File

@@ -0,0 +1,10 @@
win32_service:
project_code: demo
messenger:
-
receivers: [async]
limit: 10
displayed_name: Demo Messenger Consumer Async %d
thread_count: 2
memory_limit: 3600

View File

@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
if (file_exists(\dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require \dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}

View File

@@ -0,0 +1,3 @@
#index:
# path: /
# controller: App\Controller\DefaultController::index

View File

@@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

View File

@@ -0,0 +1,25 @@
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
Win32ServiceBundle\Tests\Application\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../Event/'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240725152658 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE messenger_messages (id BIGINT AUTO_INCREMENT NOT NULL, body LONGTEXT NOT NULL, headers LONGTEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL, available_at DATETIME NOT NULL, delivered_at DATETIME DEFAULT NULL, INDEX IDX_75EA56E0FB7336F0 (queue_name), INDEX IDX_75EA56E0E3BD61CE (available_at), INDEX IDX_75EA56E016BA31DB (delivered_at), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE messenger_messages');
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
use App\Kernel;
require_once \dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\Tests\Application\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Win32ServiceBundle\Tests\Application\Event\TestMessage;
#[AsCommand('test:send-message', 'Send a Test Message')]
final class SendMessageCommand extends Command
{
public function __construct(private MessageBusInterface $messageBus)
{
parent::__construct();
}
protected function configure(): void
{
$this->addArgument('message', InputArgument::REQUIRED, 'Your test message');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$message = $input->getArgument('message');
$this->messageBus->dispatch(new TestMessage($message));
return self::SUCCESS;
}
}

View File

View File

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\Tests\Application\Event;
final class TestMessage
{
public function __construct(public string $message)
{
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\Tests\Application\Handler;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Win32ServiceBundle\Tests\Application\Event\TestMessage;
#[AsMessageHandler(fromTransport: 'async')]
final class TestMessageHandler
{
public function __construct(private LoggerInterface $logger)
{
}
public function __invoke(TestMessage $message): void
{
$this->logger->info(__METHOD__.' - message : '.$message->message);
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Win32ServiceBundle\Tests\Application;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}