Documented Commands.State & Commands.System

This commit is contained in:
Abdulmhsen B. A. A
2023-12-16 20:54:20 +03:00
parent e893c33f39
commit 781c7530d7
18 changed files with 623 additions and 97 deletions

31
FAQ.md
View File

@@ -275,22 +275,21 @@ $ docker exec -ti watchstate console system:env
These environment variables relates to the tool itself, you can load them via the recommended methods.
| Key | Type | Description | Default |
|--------------------------|---------|-------------------------------------------------------------------------|--------------------|
| WS_DATA_PATH | string | Where to store main data. (config, db). | `${BASE_PATH}/var` |
| WS_TMP_DIR | string | Where to store temp data. (logs, cache) | `${WS_DATA_PATH}` |
| WS_TZ | string | Set timezone. | `UTC` |
| WS_CRON_{TASK} | bool | Enable {task} task. Value casted to bool. | `false` |
| WS_CRON_{TASK}_AT | string | When to run {task} task. Valid Cron Expression Expected. | `*/1 * * * *` |
| WS_CRON_{TASK}_ARGS | string | Flags to pass to the {task} command. | `-v` |
| WS_LOGS_CONTEXT | bool | Add context to console output messages. | `false` |
| WS_LOGGER_FILE_ENABLE | bool | Save logs to file. | `true` |
| WS_LOGGER_FILE_LEVEL | string | File Logger Level. | `ERROR` |
| WS_WEBHOOK_DUMP_REQUEST | bool | If enabled, will dump all received requests. | `false` |
| WS_EPISODES_DISABLE_GUID | bool | Disable external id parsing for episodes and rely on relative ids. | `true` |
| WS_TRUST_PROXY | bool | Trust `WS_TRUST_HEADER` ip. Value casted to bool. | `false` |
| WS_TRUST_HEADER | string | Which header contain user true IP. | `X-Forwarded-For` |
| WS_LIBRARY_SEGMENT | integer | Paginate backend library items request. Per request get total X number. | `1000` |
| Key | Type | Description | Default |
|-------------------------|---------|-------------------------------------------------------------------------|--------------------|
| WS_DATA_PATH | string | Where to store main data. (config, db). | `${BASE_PATH}/var` |
| WS_TMP_DIR | string | Where to store temp data. (logs, cache) | `${WS_DATA_PATH}` |
| WS_TZ | string | Set timezone. | `UTC` |
| WS_CRON_{TASK} | bool | Enable {task} task. Value casted to bool. | `false` |
| WS_CRON_{TASK}_AT | string | When to run {task} task. Valid Cron Expression Expected. | `*/1 * * * *` |
| WS_CRON_{TASK}_ARGS | string | Flags to pass to the {task} command. | `-v` |
| WS_LOGS_CONTEXT | bool | Add context to console output messages. | `false` |
| WS_LOGGER_FILE_ENABLE | bool | Save logs to file. | `true` |
| WS_LOGGER_FILE_LEVEL | string | File Logger Level. | `ERROR` |
| WS_WEBHOOK_DUMP_REQUEST | bool | If enabled, will dump all received requests. | `false` |
| WS_TRUST_PROXY | bool | Trust `WS_TRUST_HEADER` ip. Value casted to bool. | `false` |
| WS_TRUST_HEADER | string | Which header contain user true IP. | `X-Forwarded-For` |
| WS_LIBRARY_SEGMENT | integer | Paginate backend library items request. Per request get total X number. | `1000` |
> [!IMPORTANT]
> for environment variables that has `{TASK}` tag, you **MUST** replace it with one

View File

@@ -17,13 +17,19 @@ use App\Libs\Routable;
use App\Libs\Stream;
use Monolog\Logger;
use Psr\Log\LoggerInterface as iLogger;
use RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;
use Throwable;
/**
* Class ExportCommand
*
* Command for exporting play state to backends.
*
* @package App\Console\Commands\State
*/
#[Routable(command: self::ROUTE)]
class ExportCommand extends Command
{
@@ -31,6 +37,14 @@ class ExportCommand extends Command
public const TASK_NAME = 'export';
/**
* Class Constructor.
*
* @param iDB $db The instance of the iDB class.
* @param DirectMapper $mapper The instance of the DirectMapper class.
* @param QueueRequests $queue The instance of the QueueRequests class.
* @param iLogger $logger The instance of the iLogger class.
*/
public function __construct(
private iDB $db,
private DirectMapper $mapper,
@@ -43,6 +57,9 @@ class ExportCommand extends Command
parent::__construct();
}
/**
* Configure the command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -100,15 +117,30 @@ class ExportCommand extends Command
);
}
/**
* Make sure the command is not running in parallel.
*
* @param InputInterface $input The input object containing the command data.
* @param OutputInterface $output The output object for displaying command output.
*
* @return int The exit code of the command execution.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
return $this->single(fn(): int => $this->process($input, $output), $output);
}
/**
* Process the command by pulling and comparing status and then pushing.
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function process(InputInterface $input, OutputInterface $output): int
{
if (null !== ($logfile = $input->getOption('logfile')) && true === ($this->logger instanceof Logger)) {
$this->logger->pushHandler(new StreamLogHandler(new Stream(fopen($logfile, 'a')), $output));
$this->logger->pushHandler(new StreamLogHandler(new Stream($logfile, 'a'), $output));
}
// -- Use Custom servers.yaml file.
@@ -116,7 +148,7 @@ class ExportCommand extends Command
try {
$custom = true;
Config::save('servers', Yaml::parseFile($this->checkCustomBackendsFile($config)));
} catch (RuntimeException $e) {
} catch (\App\Libs\Exceptions\RuntimeException $e) {
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
return self::FAILURE;
}
@@ -448,13 +480,22 @@ class ExportCommand extends Command
copy($config, $config . '.bak');
}
file_put_contents($config, Yaml::dump(Config::get('servers', []), 8, 2));
$stream = new Stream($config, 'w');
$stream->write(Yaml::dump(Config::get('servers', []), 8, 2));
$stream->close();
}
return self::SUCCESS;
}
/**
* Push entities to backends if applicable.
*
* @param array $backends An array of backends.
* @param array $entities An array of entities to be pushed.
*
* @return int The success status code.
*/
protected function push(array $backends, array $entities): int
{
$this->logger->notice('Push mode start.', [
@@ -477,10 +518,10 @@ class ExportCommand extends Command
}
/**
* Pull and compare status and then push.
* Fallback to export mode if push mode is not supported for the backend.
*
* @param array $backends
* @param InputInterface $input
* @param array $backends An array of backends to export data to.
* @param InputInterface $input The input containing export options.
*/
protected function export(array $backends, InputInterface $input): void
{

View File

@@ -20,7 +20,6 @@ use App\Libs\Routable;
use App\Libs\Stream;
use Monolog\Logger;
use Psr\Log\LoggerInterface as iLogger;
use RuntimeException;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface;
@@ -30,6 +29,11 @@ use Symfony\Component\Yaml\Yaml;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Throwable;
/**
* Class ImportCommand
*
* This command imports metadata and play state of items from backends and updates the local database.
*/
#[Routable(command: self::ROUTE)]
class ImportCommand extends Command
{
@@ -37,6 +41,13 @@ class ImportCommand extends Command
public const TASK_NAME = 'import';
/**
* Class Constructor.
*
* @param iDB $db The database interface object.
* @param iImport $mapper The import interface object.
* @param iLogger $logger The logger interface object.
*/
public function __construct(private iDB $db, private iImport $mapper, private iLogger $logger)
{
set_time_limit(0);
@@ -45,6 +56,9 @@ class ImportCommand extends Command
parent::__construct();
}
/**
* Configure the method.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -201,15 +215,31 @@ class ImportCommand extends Command
);
}
/**
* Make sure the command is not running in parallel.
*
* @param InputInterface $input The input interface object.
* @param OutputInterface $output The output interface object.
*
* @return int The status code of the command execution.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
return $this->single(fn(): int => $this->process($input, $output), $output);
}
/**
* Import the state from the backends.
*
* @param InputInterface $input The input interface object.
* @param OutputInterface $output The output interface object.
*
* @return int The return status code.
*/
protected function process(InputInterface $input, OutputInterface $output): int
{
if (null !== ($logfile = $input->getOption('logfile')) && true === ($this->logger instanceof Logger)) {
$this->logger->pushHandler(new StreamLogHandler(new Stream(fopen($logfile, 'a')), $output));
$this->logger->pushHandler(new StreamLogHandler(new Stream($logfile, 'a'), $output));
}
// -- Use Custom servers.yaml file.
@@ -217,7 +247,7 @@ class ImportCommand extends Command
try {
$custom = true;
Config::save('servers', Yaml::parseFile($this->checkCustomBackendsFile($config)));
} catch (RuntimeException $e) {
} catch (\App\Libs\Exceptions\RuntimeException $e) {
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
return self::FAILURE;
}
@@ -482,7 +512,9 @@ class ImportCommand extends Command
copy($config, $config . '.bak');
}
file_put_contents($config, Yaml::dump(Config::get('servers', []), 8, 2));
$stream = new Stream($config, 'w');
$stream->write(Yaml::dump(Config::get('servers', []), 8, 2));
$stream->close();
}
if ($input->getOption('show-messages')) {

View File

@@ -13,11 +13,21 @@ use App\Libs\QueueRequests;
use App\Libs\Routable;
use Psr\Log\LoggerInterface as iLogger;
use Psr\SimpleCache\CacheInterface as iCache;
use Psr\SimpleCache\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
/**
* Class ProgressCommand
*
* This command is used to push user watch progress to export enabled backends.
* It should not be run manually and should be scheduled to run as a task.
*
* This command requires the watch progress metadata to be already saved in the database.
* If no metadata is available for a backend,
* the watch progress update won't be sent to that backend
*/
#[Routable(command: self::ROUTE)]
class ProgressCommand extends Command
{
@@ -25,6 +35,14 @@ class ProgressCommand extends Command
public const TASK_NAME = 'progress';
/**
* Class Constructor.
*
* @param iLogger $logger The logger instance.
* @param iCache $cache The cache instance.
* @param iDB $db The database instance.
* @param QueueRequests $queue The queue requests instance.
*/
public function __construct(
private iLogger $logger,
private iCache $cache,
@@ -37,6 +55,9 @@ class ProgressCommand extends Command
parent::__construct();
}
/**
* Configure the command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -69,10 +90,12 @@ class ProgressCommand extends Command
}
/**
* Make sure the command is not running in parallel.
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int
* @throws InvalidArgumentException
* @throws \Psr\Cache\InvalidArgumentException if the cache key is not a legal value
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
@@ -80,7 +103,12 @@ class ProgressCommand extends Command
}
/**
* @throws InvalidArgumentException
* Run the command.
*
* @param InputInterface $input The input interface.
* @param OutputInterface $output The output interface.
* @return int Returns the status code.
* @throws \Psr\Cache\InvalidArgumentException if the cache key is not a legal value
*/
protected function process(InputInterface $input, OutputInterface $output): int
{
@@ -219,7 +247,7 @@ class ProgressCommand extends Command
...$context,
'status_code' => $response->getStatusCode(),
]);
} catch (\Throwable $e) {
} catch (Throwable $e) {
$this->logger->error(
message: 'SYSTEM: Exception [{error.kind}] was thrown unhandled during [{backend}] request to change watch progress of {item.type} [{item.title}]. Error [{error.message} @ {error.file}:{error.line}].',
context: [
@@ -266,12 +294,13 @@ class ProgressCommand extends Command
}
/**
* List Items.
* Renders and displays a list of items based on the specified output mode.
*
* @param InputInterface $input
* @param OutputInterface $output
* @param array<iState> $items
* @return int
* @param InputInterface $input The input interface object.
* @param OutputInterface $output The output interface object.
* @param array $items An array of items to be listed.
*
* @return int The status code indicating the success of the method execution.
*/
private function listItems(InputInterface $input, OutputInterface $output, array $items): int
{

View File

@@ -14,11 +14,17 @@ use App\Libs\QueueRequests;
use App\Libs\Routable;
use Psr\Log\LoggerInterface as iLogger;
use Psr\SimpleCache\CacheInterface as iCache;
use Psr\SimpleCache\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
/**
* Class PushCommand
*
* This class represents a command that pushes webhook queued events.
* It sends change play state requests to the supported backends.
*/
#[Routable(command: self::ROUTE)]
class PushCommand extends Command
{
@@ -26,6 +32,16 @@ class PushCommand extends Command
public const TASK_NAME = 'push';
/**
* Constructor for the given class.
*
* @param iLogger $logger The logger instance.
* @param iCache $cache The cache instance.
* @param iDB $db The database instance.
* @param QueueRequests $queue The queue instance.
*
* @return void
*/
public function __construct(
private iLogger $logger,
private iCache $cache,
@@ -38,6 +54,9 @@ class PushCommand extends Command
parent::__construct();
}
/**
* Configure command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -68,10 +87,13 @@ class PushCommand extends Command
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
* @throws InvalidArgumentException
* Make sure the command is not running in parallel.
*
* @param InputInterface $input The input interface.
* @param OutputInterface $output The output interface.
*
* @return int Returns the process result status code.
* @throws \Psr\SimpleCache\InvalidArgumentException if the cache key is not a legal value.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
@@ -79,7 +101,12 @@ class PushCommand extends Command
}
/**
* @throws InvalidArgumentException
* Process the queue items and send change play state requests to the supported backends.
*
* @param InputInterface $input The input interface.
*
* @return int Returns the process result status code.
* @throws \Psr\SimpleCache\InvalidArgumentException if the cache key is not a legal value.
*/
protected function process(InputInterface $input): int
{
@@ -197,7 +224,7 @@ class PushCommand extends Command
}
$this->logger->notice('SYSTEM: Marked [{backend}] [{item.title}] as [{play_state}].', $context);
} catch (\Throwable $e) {
} catch (Throwable $e) {
$this->logger->error(
message: 'SYSTEM: Exception [{error.kind}] was thrown unhandled during [{backend}] request to change play state of {item.type} [{item.title}]. Error [{error.message} @ {error.file}:{error.line}].',
context: [

View File

@@ -12,13 +12,19 @@ use App\Libs\Options;
use App\Libs\Routable;
use Psr\Log\LoggerInterface as iLogger;
use Psr\SimpleCache\CacheInterface as iCache;
use Psr\SimpleCache\InvalidArgumentException;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class RequestsCommand
*
* This class is responsible for processing queued HTTP requests.
*
* @package Your\Namespace
*/
#[Routable(command: self::ROUTE)]
class RequestsCommand extends Command
{
@@ -26,6 +32,13 @@ class RequestsCommand extends Command
public const TASK_NAME = 'requests';
/**
* Class constructor.
*
* @param iLogger $logger The logger object.
* @param iCache $cache The cache object.
* @param DirectMapper $mapper The DirectMapper object.
*/
public function __construct(private iLogger $logger, private iCache $cache, private DirectMapper $mapper)
{
set_time_limit(0);
@@ -34,6 +47,9 @@ class RequestsCommand extends Command
parent::__construct();
}
/**
* Configure the command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -46,10 +62,13 @@ class RequestsCommand extends Command
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
* @throws InvalidArgumentException
* Make sure the command is not running in parallel.
*
* @param InputInterface $input The input interface.
* @param OutputInterface $output The output interface.
*
* @return int The exit code of the command.
* @throws \Psr\Cache\InvalidArgumentException if the $key string is not a legal value
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
@@ -57,7 +76,13 @@ class RequestsCommand extends Command
}
/**
* @throws InvalidArgumentException
* Run the command.
*
* @param InputInterface $input The input interface.
* @param OutputInterface $output The output interface.
*
* @return int The exit code of the command.
* @throws \Psr\Cache\InvalidArgumentException if the $key string is not a legal value
*/
protected function process(InputInterface $input, OutputInterface $output): int
{
@@ -155,12 +180,13 @@ class RequestsCommand extends Command
}
/**
* List Items.
* Lists items based on the provided input and output.
*
* @param InputInterface $input
* @param OutputInterface $output
* @param array $requests
* @return int
* @param InputInterface $input The input interface object.
* @param OutputInterface $output The output interface object.
* @param array $requests The array of requests.
*
* @return int Returns the success status code.
*/
private function listItems(InputInterface $input, OutputInterface $output, array $requests): int
{

View File

@@ -10,11 +10,19 @@ use App\Libs\Routable;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class EnvCommand
*
* This command displays the environment variables that were loaded during the execution of the tool.
*/
#[Routable(command: self::ROUTE)]
final class EnvCommand extends Command
{
public const ROUTE = 'system:env';
/**
* Configure the command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -78,13 +86,21 @@ final class EnvCommand extends Command
);
}
/**
* Run the command.
*
* @param InputInterface $input The input interface.
* @param OutputInterface $output The output interface.
*
* @return int The exit code.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
$mode = $input->getOption('output');
$keys = [];
foreach (getenv() as $key => $val) {
if (false === str_starts_with($key, 'WS_')) {
if (false === str_starts_with($key, 'WS_') && $key !== 'HTTP_PORT') {
continue;
}

View File

@@ -12,6 +12,11 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class IndexCommand
*
* This command ensures that the database has correct indexes.
*/
#[Routable(command: self::ROUTE)]
final class IndexCommand extends Command
{
@@ -19,11 +24,21 @@ final class IndexCommand extends Command
public const TASK_NAME = 'indexes';
/**
* Class constructor.
*
* @param iDB $db An instance of the iDB class.
*/
public function __construct(private iDB $db)
{
parent::__construct();
}
/**
* Configure the command.
*
* @return void
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -56,6 +71,14 @@ final class IndexCommand extends Command
);
}
/**
* Run a command.
*
* @param InputInterface $input An instance of the InputInterface interface.
* @param OutputInterface $output An instance of the OutputInterface interface.
*
* @return int The status code indicating the success or failure of the command execution.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
$this->db->ensureIndex([

View File

@@ -6,8 +6,8 @@ namespace App\Commands\System;
use App\Command;
use App\Libs\Config;
use App\Libs\Exceptions\InvalidArgumentException;
use App\Libs\Routable;
use Exception;
use LimitIterator;
use SplFileObject;
use Symfony\Component\Console\Completion\CompletionInput;
@@ -17,19 +17,33 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
/**
* Class LogsCommand.
*
* This class is used to view and clear log files.
*/
#[Routable(command: self::ROUTE)]
final class LogsCommand extends Command
{
public const ROUTE = 'system:logs';
/**
* @var array Constant array containing names of supported log files.
*/
private const LOG_FILES = [
'app',
'access',
'task'
];
/**
* @var int The default limit of how many lines to show.
*/
public const DEFAULT_LIMIT = 50;
/**
* Configure the command.
*/
protected function configure(): void
{
$defaultDate = makeDate()->format('Ymd');
@@ -121,7 +135,15 @@ final class LogsCommand extends Command
}
/**
* @throws Exception
* Run the command.
*
* @param InputInterface $input The input interface.
* @param OutputInterface $output The output interface.
*
* @return int The exit code of the command.
*
* @throws InvalidArgumentException If the log type is not one of the supported log files.
* @throws InvalidArgumentException If the log date is not in the correct format.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
@@ -132,7 +154,7 @@ final class LogsCommand extends Command
$type = $input->getOption('type');
if (false === in_array($type, self::LOG_FILES)) {
throw new \RuntimeException(
throw new InvalidArgumentException(
sprintf('Log type has to be one of the supported log files [%s].', implode(', ', self::LOG_FILES))
);
}
@@ -140,12 +162,15 @@ final class LogsCommand extends Command
$date = $input->getOption('date');
if (1 !== preg_match('/^\d{8}$/', $date)) {
throw new \RuntimeException('Log date must be in [YYYYMMDD] format. For example [20220622].');
throw new InvalidArgumentException('Log date must be in [YYYYMMDD] format. For example [20220622].');
}
$limit = (int)$input->getOption('limit');
$file = sprintf(Config::get('tmpDir') . '/logs/%s.%s.log', $type, $date);
$file = r(text: Config::get('tmpDir') . '/logs/{type}.{date}.log', context: [
'type' => $type,
'date' => $date
]);
if (false === file_exists($file) || filesize($file) < 1) {
$output->writeln(
@@ -226,6 +251,14 @@ final class LogsCommand extends Command
return self::SUCCESS;
}
/**
* Lists the logs.
*
* @param InputInterface $input The input interface.
* @param OutputInterface $output The output interface.
*
* @return int The exit code.
*/
private function listLogs(InputInterface $input, OutputInterface $output): int
{
$path = fixPath(Config::get('tmpDir') . '/logs');
@@ -259,6 +292,15 @@ final class LogsCommand extends Command
return self::SUCCESS;
}
/**
* Clears the contents of a log file.
*
* @param SplFileObject $file The log file object.
* @param InputInterface $input The input interface.
* @param OutputInterface $output The output interface.
*
* @return int The exit code.
*/
private function handleClearLog(SplFileObject $file, InputInterface $input, OutputInterface $output): int
{
$logfile = after($file->getRealPath(), Config::get('tmpDir') . '/');
@@ -310,6 +352,12 @@ final class LogsCommand extends Command
return self::SUCCESS;
}
/**
* Complete the suggestions for the given input.
*
* @param CompletionInput $input The completion input.
* @param CompletionSuggestions $suggestions The completion suggestions.
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
parent::complete($input, $suggestions);
@@ -329,6 +377,11 @@ final class LogsCommand extends Command
}
}
/**
* Retrieve the types of log files.
*
* @return array The array of available log file types.
*/
public static function getTypes(): array
{
return self::LOG_FILES;

View File

@@ -10,16 +10,29 @@ use App\Libs\Routable;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class MaintenanceCommand
*
* Runs maintenance tasks on the database.
*/
#[Routable(command: self::ROUTE)]
final class MaintenanceCommand extends Command
{
public const ROUTE = 'system:db:maintenance';
/**
* Class constructor.
*
* @param iDB $db The database connection object.
*/
public function __construct(private iDB $db)
{
parent::__construct();
}
/**
* Configure the command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -35,6 +48,14 @@ final class MaintenanceCommand extends Command
);
}
/**
* Runs the command.
*
* @param InputInterface $input The input interface object.
* @param OutputInterface $output The output interface object.
*
* @return int Returns the exit code.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
$this->db->maintenance();

View File

@@ -11,16 +11,29 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class MakeCommand
*
* This class represents a command to create a database schema migration file.
*/
#[Routable(command: self::ROUTE)]
final class MakeCommand extends Command
{
public const ROUTE = 'system:db:make';
/**
* Class Constructor.
*
* @param iDB $db The iDB object used for database operations.
*/
public function __construct(private iDB $db)
{
parent::__construct();
}
/**
* Configure the command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -46,16 +59,21 @@ final class MakeCommand extends Command
);
}
/**
* Executes a command.
*
* @param InputInterface $input The input object containing command arguments and options.
* @param OutputInterface $output The output object used for displaying messages.
*
* @return int The exit code of the command execution. Returns "SUCCESS" constant value.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
$file = $this->db->makeMigration($input->getArgument('filename'));
$output->writeln(
sprintf(
'<info>Created new migration at \'%s\'.</info>',
after(realpath($file), ROOT_PATH),
)
);
$output->writeln(r(text: "<info>Created new migration file at '{file}'.</info>", context: [
'file' => after(realpath($file), ROOT_PATH),
]));
return self::SUCCESS;
}

View File

@@ -11,16 +11,30 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class MigrationsCommand
*
* Database migrations runner.
*/
#[Routable(command: self::ROUTE)]
final class MigrationsCommand extends Command
{
public const ROUTE = 'system:db:migrations';
/**
* Class Constructor.
*
* @param iDB $db The database connection object.
*
*/
public function __construct(private iDB $db)
{
parent::__construct();
}
/**
* Configures the command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -37,7 +51,27 @@ final class MigrationsCommand extends Command
);
}
/**
* Make sure the command is not running in parallel.
*
* @param InputInterface $input The input object containing the command data.
* @param OutputInterface $output The output object for displaying command output.
*
* @return int The exit code of the command execution.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
return $this->single(fn(): int => $this->process($input), $output);
}
/**
* Run the command to migrate the database.
*
* @param InputInterface $input The input object representing the command inputs.
*
* @return int The exit code of the command execution.
*/
protected function process(InputInterface $input): int
{
$opts = [];

View File

@@ -11,11 +11,22 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class PHPCommand
*
* This command is used to generate expected values for php.ini and fpm pool worker.
* To generate fpm values, use the "--fpm" option.
*/
#[Routable(command: self::ROUTE)]
final class PHPCommand extends Command
{
public const ROUTE = 'system:php';
/**
* Configures the command.
*
* @return void
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -40,11 +51,25 @@ final class PHPCommand extends Command
);
}
/**
* Runs the command based on the input options.
*
* @param InputInterface $input The input options.
* @param OutputInterface $output The output interface for displaying messages.
* @return int The exit code of the command execution.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
return $input->getOption('fpm') ? $this->makeFPM($output) : $this->makeConfig($output);
}
/**
* Print the php.ini configuration.
*
* @param OutputInterface $output The OutputInterface object to write the configuration to.
*
* @return int The status code indicating the success of the method.
*/
protected function makeConfig(OutputInterface $output): int
{
$config = Config::get('php.ini', []);
@@ -56,6 +81,13 @@ final class PHPCommand extends Command
return self::SUCCESS;
}
/**
* Print the PHP-FPM configuration.
*
* @param OutputInterface $output The OutputInterface object to write the configuration to.
*
* @return int The status code indicating the success of the method.
*/
protected function makeFPM(OutputInterface $output): int
{
$config = Config::get('php.fpm', []);
@@ -70,6 +102,13 @@ final class PHPCommand extends Command
return self::SUCCESS;
}
/**
* Escape the given value.
*
* @param mixed $val The value to escape.
*
* @return mixed The escaped value.
*/
private function escapeValue(mixed $val): mixed
{
if (is_bool($val) || is_int($val)) {

View File

@@ -13,6 +13,12 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class PruneCommand
*
* This command removes automatically generated files like logs and backups.
* It provides an option to run in dry-run mode to see what files will be removed without actually removing them.
*/
#[Routable(command: self::ROUTE)]
final class PruneCommand extends Command
{
@@ -20,11 +26,19 @@ final class PruneCommand extends Command
public const TASK_NAME = 'prune';
/**
* Class Constructor.
*
* @param LoggerInterface $logger The logger implementation used for logging.
*/
public function __construct(private LoggerInterface $logger)
{
parent::__construct();
}
/**
* Configure the command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -49,6 +63,14 @@ final class PruneCommand extends Command
);
}
/**
* Executes the command.
*
* @param InputInterface $input The input interface.
* @param OutputInterface $output The output interface.
*
* @return int The exit status code.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
$time = time();
@@ -139,5 +161,4 @@ final class PruneCommand extends Command
return self::SUCCESS;
}
}

View File

@@ -11,8 +11,8 @@ use App\Libs\Entity\StateEntity;
use App\Libs\Extends\Date;
use App\Libs\Options;
use App\Libs\Routable;
use App\Libs\Stream;
use Cron\CronExpression;
use Exception;
use LimitIterator;
use SplFileObject;
use Symfony\Component\Console\Input\InputInterface as iInput;
@@ -20,16 +20,31 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface as iOutput;
use Throwable;
/**
* Class ReportCommand
*
* Show basic information for diagnostics.
*/
#[Routable(command: self::ROUTE)]
final class ReportCommand extends Command
{
public const ROUTE = 'system:report';
/**
* Class Constructor.
*
* @param iDB $db An instance of the iDB class used for database operations.
*
* @return void
*/
public function __construct(private iDB $db)
{
parent::__construct();
}
/**
* Configure the command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -54,6 +69,14 @@ final class ReportCommand extends Command
);
}
/**
* Display basic information for diagnostics.
*
* @param iInput $input An instance of the iInput class used for command input.
* @param iOutput $output An instance of the iOutput class used for command output.
*
* @return int Returns the command execution status code.
*/
protected function runCommand(iInput $input, iOutput $output): int
{
$output->writeln('<info>[ Basic Report ]</info>' . PHP_EOL);
@@ -82,7 +105,7 @@ final class ReportCommand extends Command
}
try {
$pid = trim(file_get_contents($pidFile));
$pid = trim((string)(new Stream($pidFile)));
} catch (Throwable $e) {
return $e->getMessage();
}
@@ -111,6 +134,14 @@ final class ReportCommand extends Command
return self::SUCCESS;
}
/**
* Get backends and display information about each backend.
*
* @param iInput $input An instance of the iInput class used for input operations.
* @param iOutput $output An instance of the iOutput class used for output operations.
*
* @return void
*/
private function getBackends(iInput $input, iOutput $output): void
{
$includeSample = (bool)$input->getOption('include-db-sample');
@@ -239,6 +270,13 @@ final class ReportCommand extends Command
}
}
/**
* Retrieves the tasks and displays information about each task.
*
* @param iOutput $output An instance of the iOutput class used for displaying output.
*
* @return void
*/
private function getTasks(iOutput $output): void
{
foreach (Config::get('tasks.list', []) as $task) {
@@ -274,7 +312,7 @@ final class ReportCommand extends Command
'answer' => gmdate(Date::ATOM, $timer->getNextRunDate()->getTimestamp()),
])
);
} catch (Exception $e) {
} catch (Throwable $e) {
$output->writeln(
r('Next Run scheduled failed. <error>{answer}</error>', [
'answer' => $e->getMessage(),
@@ -288,6 +326,12 @@ final class ReportCommand extends Command
}
}
/**
* Get logs.
*
* @param iInput $input An instance of the iInput class used for input operations.
* @param iOutput $output An instance of the iOutput class used for output operations.
*/
private function getLogs(iInput $input, iOutput $output): void
{
$todayAffix = makeDate()->format('Ymd');
@@ -307,6 +351,16 @@ final class ReportCommand extends Command
}
}
/**
* Get last X lines from log file.
*
* @param iOutput $output An instance of the iOutput class used for displaying output.
* @param string $type The type of the log.
* @param string|int $date The date of the log file.
* @param int|string $limit The maximum number of lines to display.
*
* @return void
*/
private function handleLog(iOutput $output, string $type, string|int $date, int|string $limit): void
{
$logFile = Config::get('tmpDir') . '/logs/' . r(

View File

@@ -9,11 +9,19 @@ use App\Libs\Routable;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class RoutesCommand
*
* This command is used to generate routes for commands. It is automatically run on container startup.
*/
#[Routable(command: self::ROUTE)]
final class RoutesCommand extends Command
{
public const ROUTE = 'system:routes';
/**
* Configures the command.
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -28,6 +36,14 @@ final class RoutesCommand extends Command
);
}
/**
* Executes the command to generate routes.
*
* @param InputInterface $input The input interface object.
* @param OutputInterface $output The output interface object.
*
* @return int The exit code of the command execution.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
generateRoutes();

View File

@@ -11,16 +11,35 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class ServerCommand
*
* This class represents a command that starts a minimal HTTP server.
*
* @package YourPackage
*/
#[Routable(command: self::ROUTE)]
final class ServerCommand extends Command
{
public const ROUTE = 'system:server';
/**
* Class Constructor.
*
* @param Server $server The server object to be injected.
*
* @return void
*/
public function __construct(private Server $server)
{
parent::__construct();
}
/**
* Configure the command options and description.
*
* @return void
*/
protected function configure(): void
{
$this->setName(self::ROUTE)
@@ -37,6 +56,14 @@ final class ServerCommand extends Command
);
}
/**
* Runs Server.
*
* @param InputInterface $input The input interface object containing command input options.
* @param OutputInterface $output The output interface object used to display command output.
*
* @return int Returns the exit code of the command.
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
$host = $input->getOption('interface');

View File

@@ -8,6 +8,7 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Extends\ConsoleOutput;
use App\Libs\Routable;
use App\Libs\Stream;
use Cron\CronExpression;
use Exception;
use Symfony\Component\Console\Completion\CompletionInput;
@@ -17,7 +18,13 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface as iOutput;
use Symfony\Component\Process\Process;
use Throwable;
/**
* Class TasksCommand
*
* Automates the runs of scheduled tasks.
*/
#[Routable(command: self::ROUTE)]
final class TasksCommand extends Command
{
@@ -26,6 +33,9 @@ final class TasksCommand extends Command
private array $logs = [];
private array $taskOutput = [];
/**
* Class Constructor.
*/
public function __construct()
{
set_time_limit(0);
@@ -34,6 +44,9 @@ final class TasksCommand extends Command
parent::__construct();
}
/**
* Configure the command.
*/
protected function configure(): void
{
$tasksName = implode(
@@ -109,7 +122,12 @@ final class TasksCommand extends Command
}
/**
* @throws Exception
* If the run option is set, run the tasks, otherwise list available tasks.
*
* @param iInput $input The input instance.
* @param iOutput $output The output instance.
*
* @return int Returns the exit code of the command.
*/
protected function runCommand(iInput $input, iOutput $output): int
{
@@ -117,10 +135,34 @@ final class TasksCommand extends Command
return $this->runTasks($input, $output);
}
$this->listTasks($input, $output);
$list = [];
$mode = $input->getOption('output');
foreach ($this->getTasks() as $task) {
$list[] = [
'name' => $task['name'],
'command' => $task['command'],
'options' => $task['args'] ?? '',
'timer' => $task['timer']->getExpression(),
'description' => $task['description'] ?? '',
'NextRun' => $task['next'],
];
}
$this->displayContent($list, $output, $mode);
return self::SUCCESS;
}
/**
* Runs the tasks.
*
* @param iInput $input The input object.
* @param iOutput $output The output object.
*
* @return int The exit code of the command.
*/
private function runTasks(iInput $input, iOutput $output): int
{
$run = [];
@@ -232,15 +274,35 @@ final class TasksCommand extends Command
}
if ($input->getOption('save-log') && count($this->logs) >= 1) {
if (false !== ($fp = @fopen(Config::get('tasks.logfile'), 'a'))) {
fwrite($fp, preg_replace('#\R+#', PHP_EOL, implode(PHP_EOL, $this->logs)) . PHP_EOL . PHP_EOL);
fclose($fp);
try {
$stream = new Stream(Config::get('tasks.logfile'), 'a');
$stream->write(preg_replace('#\R+#', PHP_EOL, implode(PHP_EOL, $this->logs)) . PHP_EOL . PHP_EOL);
$stream->close();
} catch (Throwable $e) {
$this->write(r('<error>Failed to open log file [{file}]. Error [{message}].</error>', [
'file' => Config::get('tasks.logfile'),
'message' => $e->getMessage(),
]), $input, $output);
return self::INVALID;
}
}
return self::SUCCESS;
}
/**
* Write method.
*
* Writes a given text to the output with the specified level.
* Optionally if the 'save-log' option is set to true, the output will be saved to the logs array.
* The logs array will be saved to the log file at the end of the command execution.
*
* @param string $text The text to write to output.
* @param iInput $input The input object.
* @param iOutput $output The output object.
* @param int $level The level of the output (default: iOutput::OUTPUT_NORMAL).
*/
private function write(string $text, iInput $input, iOutput $output, int $level = iOutput::OUTPUT_NORMAL): void
{
assert($output instanceof ConsoleOutput);
@@ -252,28 +314,10 @@ final class TasksCommand extends Command
}
/**
* @throws Exception
* Get the list of tasks.
*
* @return array<string, array{name: string, command: string, args: string, description: string, enabled: bool, timer: CronExpression, next: string }> The list of tasks.
*/
private function listTasks(iInput $input, iOutput $output): void
{
$list = [];
$mode = $input->getOption('output');
foreach ($this->getTasks() as $task) {
$list[] = [
'name' => $task['name'],
'command' => $task['command'],
'options' => $task['args'] ?? '',
'timer' => $task['timer']->getExpression(),
'description' => $task['description'] ?? '',
'NextRun' => $task['next'],
];
}
$this->displayContent($list, $output, $mode);
}
private function getTasks(): array
{
$list = [];
@@ -302,6 +346,12 @@ final class TasksCommand extends Command
return $list;
}
/**
* Complete the input with suggestions if necessary.
*
* @param CompletionInput $input The completion input object.
* @param CompletionSuggestions $suggestions The completion suggestions object.
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
parent::complete($input, $suggestions);