Replaced our old task runner with new one.

This commit is contained in:
Abdulmhsen B. A. A
2022-07-05 17:06:06 +03:00
parent fe58d088c3
commit 6330224a4f
9 changed files with 345 additions and 1183 deletions

View File

@@ -0,0 +1,302 @@
<?php
declare(strict_types=1);
namespace App\Commands\System;
use App\Command;
use App\Libs\Config;
use App\Libs\Extends\ConsoleOutput;
use App\Libs\Routable;
use Cron\CronExpression;
use Exception;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface as iInput;
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;
#[Routable(command: self::ROUTE)]
final class TasksCommand extends Command
{
public const ROUTE = 'system:tasks';
private array $logs = [];
private array $taskOutput = [];
public function __construct()
{
set_time_limit(0);
ini_set('memory_limit', '-1');
parent::__construct();
}
protected function configure(): void
{
$cmdContext = trim(commandContext()) . ' ' . self::ROUTE;
$tasksName = implode(
', ',
array_map(fn($val) => '<comment>' . strtoupper($val) . '</comment>',
array_keys(Config::get('tasks.list', [])))
);
$this->setName(self::ROUTE)
->addOption('run', null, InputOption::VALUE_NONE, 'Run scheduled tasks.')
->addOption('task', 't', InputOption::VALUE_REQUIRED, 'Run the specified task only.')
->addOption('save-log', null, InputOption::VALUE_NONE, 'Save tasks output to file.')
->addOption('live', null, InputOption::VALUE_NONE, 'See output in real time.')
->setDescription('List & Run scheduled tasks.')
->setHelp(
<<<HELP
--------------------------
<comment># How run scheduled tasks?</comment>
--------------------------
To run scheduled tasks, Do the following
{$cmdContext} --run
---------------------------------
<comment># How to force run specific task?</comment>
---------------------------------
You have to combine both <info>[--run]</info> and <info>[--task task_name]</info>, For example:
{$cmdContext} <info>--run --task</info> <comment>import</comment>
Running task in force mode, bypass the task enabled check.
-------------------------
<comment># How to configure tasks?</comment>
-------------------------
All Prebuilt tasks have 3 environment variables assoicated with them.
## <info>WS_CRON_<comment>{TASK}</comment>:</info>
This environment variable control whether the task is enabled or not, it auto cast the value to bool. For example,
to enable <comment>import</comment> task simply add new environment varaible called <info>WS_CRON_</info><comment>IMPORT</comment> with value of <info>true</info> or <info>1</info>.
## <info>WS_CRON_<comment>{TASK}</comment>_AT:</info>
This environment variable control when the task should run, it accepts valid cron expression timer. For example,
to run <comment>import</comment> every two hours add new environment variable called <info>WS_CRON_<comment>IMPORT</comment>_AT</info> with value of <info>0 */2 * * *</info>.
## <info>WS_CRON_<comment>{TASK}</comment>_ARGS</info>:
This environment variable control the options passed to the executed command, For example to expand the information
logged during <comment>import</comment> run, add new environment variable called <info>WS_CRON_<comment>IMPORT</comment>_ARGS</info> with value of <info>-vvv --context --trace</info>.
Simply put, run help on the assoicated command, and you can use any <comment>Options</comment> listed there in this variable.
-------------------------------------
Replace <comment>{TASK}</comment> which one of the following [ $tasksName ]
environment variables are in ALL CAPITAL LETTERS
HELP
);
}
/**
* @throws Exception
*/
protected function runCommand(iInput $input, iOutput $output): int
{
if ($input->getOption('run')) {
return $this->runTasks($input, $output);
}
$this->listTasks($input, $output);
return self::SUCCESS;
}
private function runTasks(iInput $input, iOutput $output): int
{
$run = [];
$tasks = $this->getTasks();
if (null !== ($task = $input->getOption('task'))) {
$task = strtolower($task);
if (false === ag_exists($tasks, $task)) {
$output->writeln(
replacer('<error>There are no task named [{task}].</error>', [
'task' => $task
])
);
return self::FAILURE;
}
$run[] = ag($tasks, $task);
} else {
foreach ($tasks as $task) {
if (false === (bool)ag($task, 'enabled')) {
continue;
}
assert($task['timer'] instanceof CronExpression);
if ($task['timer']->isDue('now')) {
$run[] = $task;
}
}
}
if (count($run) < 1) {
$output->writeln(
replacer('<info>[{datetime}] No task scheduled to run at this time.</info>', [
'datetime' => makeDate(),
]),
iOutput::VERBOSITY_VERBOSE
);
}
foreach ($run as $task) {
$cmd = [];
$cmd[] = env('IN_DOCKER') ? 'console' : 'php ' . ROOT_PATH . '/console';
$cmd[] = ag($task, 'command');
if (null !== ($args = ag($task, 'args'))) {
$cmd[] = $args;
}
$process = Process::fromShellCommandline(implode(' ', $cmd), timeout: null);
$process->start(function ($std, $out) use ($input, $output) {
assert($output instanceof ConsoleOutputInterface);
if (empty($out)) {
return;
}
$this->taskOutput[] = trim($out);
if (!$input->getOption('live')) {
return;
}
('err' === $std ? $output->getErrorOutput() : $output)->writeln(trim($out));
});
if ($process->isRunning()) {
$process->wait();
}
if (count($this->taskOutput) < 1) {
continue;
}
$this->write('--------------------------', $input, $output);
$this->write(replacer('Task: {name}', ['name' => $task['name']]), $input, $output);
$this->write(
replacer('Date: {datetime}', ['datetime' => makeDate()->format('Y-m-d H:i:s T')]),
$input,
$output
);
$this->write(replacer('Command: {cmd}', ['cmd' => $process->getCommandLine()]), $input, $output);
$this->write(replacer('Exit Code: {code}', ['code' => $process->getExitCode()]), $input, $output);
$this->write('--------------------------' . PHP_EOL, $input, $output);
foreach ($this->taskOutput as $line) {
$this->write($line, $input, $output);
}
$this->taskOutput = [];
}
if ($input->getOption('save-log') && count($this->logs) >= 1) {
file_put_contents(
Config::get('tasks.logfile'),
preg_replace('#\R+#', PHP_EOL, implode(PHP_EOL, $this->logs)),
FILE_APPEND
);
}
return self::SUCCESS;
}
private function write(string $text, iInput $input, iOutput $output, int $level = iOutput::OUTPUT_NORMAL): void
{
assert($output instanceof ConsoleOutput);
$output->writeln($text, $level);
if ($input->getOption('save-log')) {
$this->logs[] = $output->getLastMessage();
}
}
/**
* @throws Exception
*/
private function listTasks(iInput $input, iOutput $output): void
{
$list = [];
foreach ($this->getTasks() as $task) {
$list[] = [
'name' => $task['name'],
'command' => $task['command'],
'Options' => $task['args'] ?? '',
'description' => $task['description'] ?? '',
'NextRun' => $task['next'],
];
}
$this->displayContent($list, $output, $input->getOption('output'));
}
private function getTasks(): array
{
$list = [];
foreach (Config::get('tasks.list', []) as $task) {
$timer = new CronExpression($task['timer'] ?? '5 * * * *');
$list[$task['name']] = [
'name' => $task['name'],
'command' => $task['command'],
'args' => $task['args'] ?? '',
'description' => $task['info'] ?? '',
'enabled' => (bool)$task['enabled'],
'timer' => $timer,
];
try {
$list[$task['name']]['next'] = $task['enabled'] ? $timer->getNextRunDate('now')->format(
'Y-m-d H:i:s T'
) : 'Disabled';
} catch (Exception $e) {
$list[$task['name']]['next'] = $e->getMessage();
}
}
return $list;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
parent::complete($input, $suggestions);
if ($input->mustSuggestOptionValuesFor('task')) {
$currentValue = $input->getCompletionValue();
$suggest = [];
foreach (array_keys(Config::get('tasks.list', [])) as $name) {
if (empty($currentValue) || str_starts_with($name, $currentValue)) {
$suggest[] = $name;
}
}
$suggestions->suggestValues($suggest);
}
}
}