diff --git a/config/config.php b/config/config.php
index 957a9e1d..eb9e6e84 100644
--- a/config/config.php
+++ b/config/config.php
@@ -9,7 +9,6 @@ use App\Commands\State\PushCommand;
use App\Commands\System\IndexCommand;
use App\Commands\System\PruneCommand;
use App\Libs\Mappers\Import\MemoryMapper;
-use App\Libs\Scheduler\Task;
use App\Libs\Servers\EmbyServer;
use App\Libs\Servers\JellyfinServer;
use App\Libs\Servers\PlexServer;
@@ -185,47 +184,54 @@ return (function () {
$config['tasks'] = [
'logfile' => ag($config, 'tmpDir') . '/logs/task.' . $logDateFormat . '.log',
- 'commands' => [
+ 'list' => [
ImportCommand::TASK_NAME => [
- Task::NAME => ImportCommand::TASK_NAME,
- Task::ENABLED => (bool)env('WS_CRON_IMPORT', false),
- Task::RUN_AT => (string)env('WS_CRON_IMPORT_AT', '0 */1 * * *'),
- Task::COMMAND => '@' . ImportCommand::ROUTE,
- Task::ARGS => env('WS_CRON_IMPORT_ARGS', '-v'),
+ 'command' => ImportCommand::ROUTE,
+ 'name' => ImportCommand::TASK_NAME,
+ 'info' => 'Import play state and metadata from backends.',
+ 'enabled' => (bool)env('WS_CRON_IMPORT', false),
+ 'timer' => (string)env('WS_CRON_IMPORT_AT', '0 */1 * * *'),
+ 'args' => env('WS_CRON_IMPORT_ARGS', '-v'),
],
ExportCommand::TASK_NAME => [
- Task::NAME => ExportCommand::TASK_NAME,
- Task::ENABLED => (bool)env('WS_CRON_EXPORT', false),
- Task::RUN_AT => (string)env('WS_CRON_EXPORT_AT', '30 */1 * * *'),
- Task::COMMAND => '@' . ExportCommand::ROUTE,
- Task::ARGS => env('WS_CRON_EXPORT_ARGS', '-v'),
+ 'command' => ExportCommand::ROUTE,
+ 'name' => ExportCommand::TASK_NAME,
+ 'info' => 'Export play state to backends.',
+ 'enabled' => (bool)env('WS_CRON_EXPORT', false),
+ 'timer' => (string)env('WS_CRON_EXPORT_AT', '30 */1 * * *'),
+ 'args' => env('WS_CRON_EXPORT_ARGS', '-v'),
],
PushCommand::TASK_NAME => [
- Task::NAME => PushCommand::TASK_NAME,
- Task::ENABLED => (bool)env('WS_CRON_PUSH', false),
- Task::RUN_AT => (string)env('WS_CRON_PUSH_AT', '*/10 * * * *'),
- Task::COMMAND => '@' . PushCommand::ROUTE,
- Task::ARGS => env('WS_CRON_PUSH_ARGS', '-v'),
- ],
- PruneCommand::TASK_NAME => [
- Task::NAME => PruneCommand::TASK_NAME,
- Task::ENABLED => (bool)env('WS_CRON_PRUNE', true),
- Task::RUN_AT => (string)env('WS_CRON_PRUNE_AT', '0 */12 * * *'),
- Task::COMMAND => '@' . PruneCommand::ROUTE,
- Task::ARGS => '-v',
- ],
- IndexCommand::TASK_NAME => [
- Task::NAME => IndexCommand::TASK_NAME,
- Task::ENABLED => true,
- Task::RUN_AT => '0 3 * * 3',
- Task::COMMAND => '@' . IndexCommand::ROUTE,
+ 'command' => PushCommand::ROUTE,
+ 'name' => PushCommand::TASK_NAME,
+ 'info' => 'Push Webhook play states to backends.',
+ 'enabled' => (bool)env('WS_CRON_PUSH', false),
+ 'timer' => (string)env('WS_CRON_PUSH_AT', '*/10 * * * *'),
+ 'args' => env('WS_CRON_PUSH_ARGS', '-v'),
],
BackupCommand::TASK_NAME => [
- Task::NAME => BackupCommand::TASK_NAME,
- Task::ENABLED => (bool)env('WS_CRON_BACKUP', false),
- Task::RUN_AT => (string)env('WS_CRON_BACKUP_AT', '0 6 */3 * *'),
- Task::COMMAND => '@' . BackupCommand::ROUTE,
- Task::ARGS => env('WS_CRON_EXPORT_ARGS', '-v'),
+ 'command' => BackupCommand::ROUTE,
+ 'name' => BackupCommand::TASK_NAME,
+ 'info' => 'Backup backends play states.',
+ 'enabled' => (bool)env('WS_CRON_BACKUP', false),
+ 'timer' => (string)env('WS_CRON_BACKUP_AT', '0 6 */3 * *'),
+ 'args' => env('WS_CRON_BACKUP_ARGS', '-v'),
+ ],
+ PruneCommand::TASK_NAME => [
+ 'command' => PruneCommand::ROUTE,
+ 'name' => PruneCommand::TASK_NAME,
+ 'info' => 'Delete old logs and backups.',
+ 'enabled' => (bool)env('WS_CRON_PRUNE', true),
+ 'timer' => (string)env('WS_CRON_PRUNE_AT', '0 */12 * * *'),
+ 'args' => env('WS_CRON_PRUNE_ARGS', '-v'),
+ ],
+ IndexCommand::TASK_NAME => [
+ 'command' => IndexCommand::ROUTE,
+ 'name' => IndexCommand::TASK_NAME,
+ 'info' => 'Check database for optimal indexes.',
+ 'enabled' => (bool)env('WS_CRON_INDEXES', true),
+ 'timer' => (string)env('WS_CRON_INDEXES_AT', '0 3 * * 3'),
+ 'args' => env('WS_CRON_INDEXES_ARGS', '-v'),
],
],
];
diff --git a/docker/files/cron.sh b/docker/files/cron.sh
index 8e5d660c..df7620ca 100755
--- a/docker/files/cron.sh
+++ b/docker/files/cron.sh
@@ -1,10 +1,9 @@
#!/usr/bin/env sh
UID=$(id -u)
-WS_CRON_DEBUG=${WS_CRON_DEBUG:-v}
if [ 0 == "${UID}" ]; then
- runuser -u www-data -- /usr/bin/console scheduler:run --save-log -${WS_CRON_DEBUG}
+ runuser -u www-data -- /usr/bin/console system:tasks --save-log
else
- /usr/bin/console scheduler:run --save-log -${WS_CRON_DEBUG}
+ /usr/bin/console system:tasks --save-log
fi
diff --git a/src/Commands/Scheduler/ListCommand.php b/src/Commands/Scheduler/ListCommand.php
deleted file mode 100644
index b71c4ba3..00000000
--- a/src/Commands/Scheduler/ListCommand.php
+++ /dev/null
@@ -1,105 +0,0 @@
-setName(self::ROUTE)
- ->addOption('timezone', 't', InputOption::VALUE_REQUIRED, 'Set Timezone.', Config::get('tz', 'UTC'))
- ->setDescription('List Scheduled Tasks.');
- }
-
- /**
- * @throws Exception
- */
- protected function runCommand(InputInterface $input, OutputInterface $output): int
- {
- $list = [];
-
- $table = new Table($output);
- $table->setHeaders(
- [
- 'Name',
- 'Command',
- 'Run As',
- 'Run At',
- 'In Background'
- ]
- );
-
- foreach (RunCommand::getTasks() as $task) {
- $task = RunCommand::fixTask($task);
-
- $timer = $task[Task::RUN_AT] ?? TaskTimer::everyMinute(5);
- if ((!$timer instanceof CronExpression)) {
- $timer = TaskTimer::at($timer);
- }
-
- $task[Task::RUN_AT] = $timer->getNextRunDate('now');
- $task[Task::RUN_IN_FOREGROUND] = (bool)($task[Task::RUN_IN_FOREGROUND] ?? false);
-
- if (false === $task[Task::USE_CLOSURE_AS_COMMAND] && ($task[Task::COMMAND] instanceof Closure)) {
- $task['Type'] = 'PHP Sub Process';
- $task[Task::COMMAND] = 'Closure';
- } else {
- if (($task[Task::COMMAND] instanceof Closure)) {
- $task[Task::COMMAND] = RunClosureCommand::runClosure($task[Task::COMMAND], $task[Task::CONFIG]);
- }
-
- if (!is_string($task[Task::COMMAND])) {
- throw new RuntimeException(
- sprintf('Task \'%s\' Command did not evaluated to a string.', $task['name'])
- );
- }
-
- if (str_starts_with($task[Task::COMMAND], '@')) {
- $task[Task::COMMAND] = substr($task[Task::COMMAND], 1);
- $task['Type'] = 'App Command';
- } else {
- $task['Type'] = 'Raw Shell';
- }
- }
-
- $list[] = [
- $task[Task::NAME],
- $task[Task::COMMAND],
- $task['Type'],
- $task[Task::ENABLED] ? $task[Task::RUN_AT]->setTimezone(
- new DateTimeZone($input->getOption('timezone'))
- )->format(
- DateTimeInterface::ATOM
- ) : 'Disabled via config',
- $task[Task::RUN_IN_FOREGROUND] ? 'No' : 'Yes',
- ];
- }
-
- $table->setRows($list);
-
- $table->render();
-
- return self::SUCCESS;
- }
-}
diff --git a/src/Commands/Scheduler/RunClosureCommand.php b/src/Commands/Scheduler/RunClosureCommand.php
deleted file mode 100644
index 7a80ab67..00000000
--- a/src/Commands/Scheduler/RunClosureCommand.php
+++ /dev/null
@@ -1,88 +0,0 @@
-setName(self::ROUTE)
- ->addArgument('task', InputArgument::REQUIRED, 'Run task closure.', null)
- ->setDescription('Run task closure.');
- }
-
- protected function runCommand(InputInterface $input, OutputInterface $output): int
- {
- $runTask = $input->getArgument('task');
-
- if (!is_string($runTask)) {
- throw new RuntimeException('Invalid Task name was given.');
- }
-
- $tasks = RunCommand::getTasks();
-
- if (!array_key_exists($runTask, $tasks)) {
- throw new RuntimeException(sprintf('Task \'%s\' was not found in Tasks config file.', $runTask));
- }
-
- $task = RunCommand::fixTask($tasks[$runTask]);
-
- $isRunnable = ($task[Task::COMMAND] instanceof Closure) && false === $task[Task::USE_CLOSURE_AS_COMMAND];
-
- if (false === $isRunnable) {
- throw new RuntimeException(
- sprintf(
- 'Was Expecting Command to be \'Closure\' for \'%s\'. But got \'%s\' instead.',
- $runTask,
- gettype($task[Task::COMMAND])
- )
- );
- }
-
- try {
- $commandOutput = (string)self::runClosure($task[Task::COMMAND], $task[Task::CONFIG]);
- } catch (Throwable $e) {
- $commandOutput = sprintf(
- 'Task \'%s\' has thrown unhandled exception. \'%s\'. (%s:%d)',
- $task[Task::NAME],
- $e->getMessage(),
- $e->getFile(),
- $e->getLine()
- );
-
- $this->logger->error($e->getMessage(), $e->getTrace());
- }
-
- $output->write($commandOutput);
-
- return self::SUCCESS;
- }
-
- public static function runClosure(Closure $fn, array $config = []): mixed
- {
- return Container::get(ReflectionContainer::class)->call($fn, $config);
- }
-}
diff --git a/src/Commands/Scheduler/RunCommand.php b/src/Commands/Scheduler/RunCommand.php
deleted file mode 100644
index f94200ce..00000000
--- a/src/Commands/Scheduler/RunCommand.php
+++ /dev/null
@@ -1,253 +0,0 @@
-scheduler = $scheduler;
-
- parent::__construct();
- }
-
- protected function configure(): void
- {
- $this->setName(self::ROUTE)
- ->addOption('no-headers', 'g', InputOption::VALUE_NONE, 'Do not prefix output with headers.')
- ->addOption('save-log', null, InputOption::VALUE_NONE, 'Save Tasks Output to file.')
- ->addArgument('task_name', InputArgument::OPTIONAL, 'Run specific task.', null)
- ->setDescription('Run Scheduled Tasks.');
- }
-
- protected function runCommand(InputInterface $input, OutputInterface $output): int
- {
- $runSpecificTask = $input->getArgument('task_name');
-
- $tasks = self::getTasks();
-
- if (is_string($runSpecificTask)) {
- if (!array_key_exists($runSpecificTask, $tasks)) {
- throw new RuntimeException(
- sprintf('Task \'%s\' was not found in Tasks config file.', $runSpecificTask)
- );
- }
-
- $tasks[$runSpecificTask][Task::RUN_AT] = TaskTimer::everyMinute();
-
- $tasks = [$tasks[$runSpecificTask]];
- }
-
- foreach ($tasks as $task) {
- $newTask = $this->makeTask($task);
-
- if (true !== $task[Task::ENABLED] && $task[Task::NAME] !== $runSpecificTask) {
- continue;
- }
-
- $this->scheduler->queueTask($newTask);
- }
-
- $this->scheduler->run(new DateTimeImmutable('now'));
-
- $executedTasks = $this->scheduler->getExecutedTasks();
-
- $count = count($executedTasks);
-
- if (0 === $count) {
- $this->write(
- sprintf('[%s] No Tasks Scheduled to run at this time.', makeDate()),
- $input,
- $output,
- OutputInterface::VERBOSITY_VERY_VERBOSE
- );
- }
-
- $tasks = array_reverse($executedTasks);
- $noHeaders = (bool)$input->getOption('no-headers');
-
- foreach ($tasks as $task) {
- $taskOutput = trim($task->getOutput());
-
- if (empty($taskOutput)) {
- continue;
- }
-
- if (false === $noHeaders) {
- $this->write('--------------------------', $input, $output);
- $this->write('Command: ' . $task->getCommand() . ' ' . $task->getArgs(), $input, $output);
- $this->write('Date: ' . makeDate(), $input, $output);
- $this->write('--------------------------', $input, $output);
- $this->write(sprintf('Task [%s] Output.', $task->getName()), $input, $output);
- $this->write('--------------------------', $input, $output);
- $this->write('', $input, $output);
- }
-
- $this->write($taskOutput, $input, $output);
-
- if (false === $noHeaders) {
- $this->write('', $input, $output);
- }
- }
-
- if ($input->getOption('save-log')) {
- 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,
- InputInterface $input,
- OutputInterface $output,
- int $level = OutputInterface::OUTPUT_NORMAL
- ): void {
- assert($output instanceof ConsoleOutput);
- $output->writeln($text, $level);
-
- if ($input->getOption('save-log')) {
- $this->logs[] = $output->getLastMessage();
- }
- }
-
- public static function getTasks(): array
- {
- $tasks = [];
-
- foreach (Config::get('tasks.commands', []) as $i => $task) {
- $task[Task::NAME] = $task[Task::NAME] ?? 'task_' . ((int)($i) + 1);
- $tasks[$task[Task::NAME]] = $task;
- }
-
- return $tasks;
- }
-
- private function makeTask(array $task): Task
- {
- $task = self::fixTask($task);
- $cli = env('IN_DOCKER') ? 'console' : 'php ' . ROOT_PATH . '/console';
-
- if (null === $task[Task::COMMAND]) {
- throw new RuntimeException(sprintf('Task \'%s\' does not have any execute command.', $task[Task::NAME]));
- }
-
- if (false === $task[Task::USE_CLOSURE_AS_COMMAND] && ($task[Task::COMMAND] instanceof Closure)) {
- $task[Task::COMMAND] = $cli . ' scheduler:closure ' . escapeshellarg($task[Task::NAME]);
- } else {
- if (($task[Task::COMMAND] instanceof Closure)) {
- $task[Task::COMMAND] = RunClosureCommand::runClosure($task[Task::COMMAND], $task[Task::CONFIG]);
- }
-
- if (!is_string($task[Task::COMMAND])) {
- throw new RuntimeException(
- sprintf('Task \'%s\' Command did not evaluate to a string.', $task[Task::NAME])
- );
- }
-
- if (str_starts_with($task[Task::COMMAND], '@')) {
- $task[Task::COMMAND] = substr($task[Task::COMMAND], 1);
- $task[Task::COMMAND] = $cli . ' ' . $task[Task::COMMAND];
- }
- }
-
- if (in_array($task[Task::NAME], $this->registered)) {
- throw new RuntimeException(
- sprintf('There are already task registered with the same name \'%s\'.', $task[Task::NAME])
- );
- }
-
- $this->registered[] = $task[Task::NAME];
-
- $obj = Task::newTask($task[Task::NAME], $task[Task::COMMAND], $task[Task::ARGS], $task[Task::CONFIG]);
-
- $timer = $task[Task::RUN_AT] ?? TaskTimer::everyMinute(5);
-
- if ((!$timer instanceof CronExpression)) {
- $timer = TaskTimer::at($timer);
- }
-
- $obj->runAt($timer);
-
- if (true === $task[Task::RUN_IN_FOREGROUND]) {
- $obj->inForeground();
- }
-
- if (null !== $task[Task::BEFORE_CALL]) {
- $obj->setBeforeCall($task[Task::BEFORE_CALL]);
- }
-
- if (null !== $task[Task::WHEN_OVER_LAPPING_CALL]) {
- $obj->whenOverlapping($task[Task::WHEN_OVER_LAPPING_CALL]);
- }
-
- return $obj;
- }
-
- public static function fixTask(array $task): array
- {
- $task[Task::CONFIG] = $task[Task::CONFIG] ?? [];
-
- return [
- Task::NAME => (string)$task[Task::NAME],
- Task::COMMAND => $task[Task::COMMAND] ?? null,
- Task::ARGS => $task[Task::ARGS] ?? [],
- Task::RUN_AT => $task[Task::RUN_AT] ?? null,
- Task::USE_CLOSURE_AS_COMMAND => (bool)($task[Task::USE_CLOSURE_AS_COMMAND] ?? false),
- Task::BEFORE_CALL => $task[Task::BEFORE_CALL] ?? null,
- Task::RUN_IN_FOREGROUND => (bool)($task[Task::RUN_IN_FOREGROUND] ?? false),
- Task::WHEN_OVER_LAPPING_CALL => $task[Task::WHEN_OVER_LAPPING_CALL] ?? null,
- Task::CONFIG => $task[Task::CONFIG],
- Task::ENABLED => $task[Task::ENABLED],
- ];
- }
-
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- parent::complete($input, $suggestions);
-
- if ($input->mustSuggestArgumentValuesFor('task_name')) {
- $currentValue = $input->getCompletionValue();
-
- $suggest = [];
-
- foreach (array_keys(self::getTasks()) as $name) {
- if (empty($currentValue) || str_starts_with($name, $currentValue)) {
- $suggest[] = $name;
- }
- }
-
- $suggestions->suggestValues($suggest);
- }
- }
-}
diff --git a/src/Commands/System/TasksCommand.php b/src/Commands/System/TasksCommand.php
new file mode 100644
index 00000000..78a30790
--- /dev/null
+++ b/src/Commands/System/TasksCommand.php
@@ -0,0 +1,302 @@
+ '' . strtoupper($val) . '',
+ 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(
+ <<# How run scheduled tasks?
+--------------------------
+
+To run scheduled tasks, Do the following
+
+{$cmdContext} --run
+
+---------------------------------
+# How to force run specific task?
+---------------------------------
+
+You have to combine both [--run] and [--task task_name], For example:
+
+{$cmdContext} --run --task import
+
+Running task in force mode, bypass the task enabled check.
+
+-------------------------
+# How to configure tasks?
+-------------------------
+
+All Prebuilt tasks have 3 environment variables assoicated with them.
+
+## WS_CRON_{TASK}:
+
+This environment variable control whether the task is enabled or not, it auto cast the value to bool. For example,
+to enable import task simply add new environment varaible called WS_CRON_IMPORT with value of true or 1.
+
+## WS_CRON_{TASK}_AT:
+
+This environment variable control when the task should run, it accepts valid cron expression timer. For example,
+to run import every two hours add new environment variable called WS_CRON_IMPORT_AT with value of 0 */2 * * *.
+
+
+## WS_CRON_{TASK}_ARGS:
+
+This environment variable control the options passed to the executed command, For example to expand the information
+logged during import run, add new environment variable called WS_CRON_IMPORT_ARGS with value of -vvv --context --trace.
+Simply put, run help on the assoicated command, and you can use any Options listed there in this variable.
+
+-------------------------------------
+
+Replace {TASK} 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('There are no task named [{task}].', [
+ '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('[{datetime}] No task scheduled to run at this time.', [
+ '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);
+ }
+ }
+}
diff --git a/src/Libs/Scheduler/Scheduler.php b/src/Libs/Scheduler/Scheduler.php
deleted file mode 100644
index 8e460ff0..00000000
--- a/src/Libs/Scheduler/Scheduler.php
+++ /dev/null
@@ -1,77 +0,0 @@
-
- */
- private array $queuedTasks = [];
-
- /**
- * @return array
- */
- private array $executedTasks = [];
-
- /**
- * Queue a task for execution in the correct queue.
- */
- public function queueTask(Task $task): void
- {
- $this->queuedTasks[] = $task;
- }
-
- /**
- * Prioritise tasks in background.
- *
- * @return array
- */
- private function getQueuedTasks(): array
- {
- $background = [];
- $foreground = [];
-
- foreach ($this->queuedTasks as $task) {
- if ($task->canRunInBackground()) {
- $background[] = $task;
- } else {
- $foreground[] = $task;
- }
- }
-
- return array_merge($background, $foreground);
- }
-
- /**
- * Run the scheduler.
- *
- * @param DateTimeInterface $runTime Run at specific moment.
- * @return array Executed tasks
- */
- public function run(DateTimeInterface $runTime): array
- {
- foreach ($this->getQueuedTasks() as $task) {
- if (!$task->isDue($runTime)) {
- continue;
- }
- $task->run();
- $this->executedTasks[$task->getName()] = $task;
- }
-
- return $this->getExecutedTasks();
- }
-
- /**
- * @return array
- * @psalm-return array
- */
- public function getExecutedTasks(): array
- {
- return $this->executedTasks;
- }
-}
diff --git a/src/Libs/Scheduler/Task.php b/src/Libs/Scheduler/Task.php
deleted file mode 100644
index 3d3ec0da..00000000
--- a/src/Libs/Scheduler/Task.php
+++ /dev/null
@@ -1,366 +0,0 @@
-command = $command;
- $this->name = $name;
- $this->config = $config;
- $this->args = $args;
-
- $this->creationTime = new DateTimeImmutable('now');
- $this->whenOverlappingCall = fn(string $Lockfile = ''): bool => false;
-
- if (array_key_exists(self::CONFIG_TMP_DIR, $config) && is_dir($config[self::CONFIG_TMP_DIR])
- && is_writable($config[self::CONFIG_TMP_DIR])) {
- $tempDir = $config[self::CONFIG_TMP_DIR];
- } else {
- $tempDir = sys_get_temp_dir();
- }
-
- $this->lockFile = rtrim($tempDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . "{$name}.lock";
- }
-
- public static function newTask(string $name, string $command, array|string $args = [], array $config = []): self
- {
- return new self($name, $command, $args, $config);
- }
-
- public function runAt(CronExpression $at): self
- {
- $this->executionTime = $at;
-
- return $this;
- }
-
- public function getName(): string
- {
- return $this->name;
- }
-
- /**
- * Check if the task is due to run.
- * It accepts as input a DateTimeInterface used to check if
- * the task is due. Defaults to task creation time.
- * It also defaults the execution time if not previously defined.
- */
- public function isDue(DateTimeInterface|null $date = null): bool
- {
- $date = $date ?? $this->creationTime;
-
- return null !== $this->executionTime && $this->executionTime->isDue($date);
- }
-
- public function isOverlapping(): bool
- {
- if (array_key_exists(self::CONFIG_NO_LOCK, $this->config) && true === $this->config[self::CONFIG_NO_LOCK]) {
- return false;
- }
-
- $func = $this->whenOverlappingCall;
-
- return $this->lockFile &&
- file_exists($this->lockFile) &&
- (null !== $func && false === $func($this->lockFile));
- }
-
- public function inForeground(): self
- {
- $this->runInBackground = false;
-
- return $this;
- }
-
- public function canRunInBackground(): bool
- {
- return true === $this->runInBackground;
- }
-
- public function getCommand(): string
- {
- return $this->command;
- }
-
- public function getArgs(): string
- {
- if (false === is_array($this->args)) {
- return trim($this->args);
- }
-
- $args = '';
-
- foreach ($this->args as $key => $value) {
- $args .= ' ' . $key;
- if ($value !== null) {
- $args .= ' ' . escapeshellarg($value);
- }
- }
-
- return trim($args);
- }
-
- public function run(): bool
- {
- // If overlapping, don't run
- if ($this->isOverlapping()) {
- return false;
- }
-
- try {
- if (null !== $this->beforeCall) {
- $fn = $this->beforeCall;
- if (true !== $fn($this)) {
- $this->output = 'Task did not execute as beforeCall returned value other than true.';
- return false;
- }
- }
-
- $cmd = $this->getCommand();
-
- $args = $this->getArgs();
- if (!empty($args)) {
- $cmd .= ' ' . $args;
- }
-
- $this->process = Process::fromShellCommandline(
- command: $cmd,
- cwd: $this->config[self::CONFIG_CWD] ?? null,
- env: $this->config[self::CONFIG_ENV] ?? null,
- input: $this->config[self::CONFIG_INPUT] ?? null,
- timeout: $this->config[self::CONFIG_TIMEOUT] ?? null,
- );
-
- if (array_key_exists('tty', $this->config) && true === $this->config['tty']) {
- $this->process->setTty(true);
- }
-
- $this->acquireLock();
-
- if (!($this->process instanceof Process)) {
- throw new RuntimeException(sprintf('Unable to create child process for \'%s\'.', $this->getName()));
- }
-
- if ($this->canRunInBackground()) {
- $this->process->start();
- } else {
- $this->process->run();
- $this->output = $this->getOutput();
- }
-
- return true;
- } catch (Throwable $e) {
- $this->output .= sprintf(
- 'Task \'%s\' has thrown unhandled exception. (%s). (%s:%d)',
- 'Task-' . $this->getName(),
- $e->getMessage(),
- $e->getFile(),
- $e->getLine()
- );
- $this->logger?->error($e->getMessage(), $e->getTrace());
- }
-
- return false;
- }
-
- public function getLockFile(): string
- {
- if (array_key_exists(self::CONFIG_NO_LOCK, $this->config) && true !== $this->config[self::CONFIG_NO_LOCK]) {
- return '';
- }
-
- return $this->lockFile;
- }
-
- public function acquireLock(): void
- {
- if (array_key_exists(self::CONFIG_NO_LOCK, $this->config) && true !== $this->config[self::CONFIG_NO_LOCK]) {
- return;
- }
-
- if (!$this->lockFile) {
- return;
- }
-
- file_put_contents($this->getLockFile(), $this->getName());
- }
-
- /**
- * Remove the task lock file.
- */
- public function releaseLock(): void
- {
- if (array_key_exists(self::CONFIG_NO_LOCK, $this->config) && true !== $this->config[self::CONFIG_NO_LOCK]) {
- return;
- }
-
- if ($this->lockFile && file_exists($this->lockFile)) {
- unlink($this->lockFile);
- }
- }
-
- /**
- * Get the task output.
- */
- public function getOutput(): mixed
- {
- if (null === $this->process) {
- return $this->output;
- }
-
- if (array_key_exists(self::CONFIG_NO_OUTPUT, $this->config) && true === $this->config[self::CONFIG_NO_OUTPUT]) {
- return '';
- }
-
- if ($this->process->isRunning()) {
- $this->process->wait();
- }
-
- $this->exitCode = $this->process->getExitCode();
- $this->exitCodeText = $this->process->getExitCodeText();
-
- $stdout = $this->process->getOutput();
- $stderr = $this->process->getErrorOutput();
-
- if (!empty($stderr)) {
- $this->output .= $stderr;
- }
-
- if (!empty($stdout)) {
- if (!empty($stderr)) {
- $this->output .= PHP_EOL;
- }
- $this->output .= $stdout;
- }
-
- $this->process = null;
-
- return $this->output;
- }
-
- /**
- * Set function to be called if task is overlapping.
- */
- public function whenOverlapping(Closure $fn): self
- {
- $this->whenOverlappingCall = $fn;
-
- return $this;
- }
-
- public function setBeforeCall(?Closure $fn = null): self
- {
- $this->beforeCall = $fn;
-
- return $this;
- }
-
- public function getExitCode(): ?int
- {
- return $this->exitCode;
- }
-
- public function getExitCodeText(): ?string
- {
- return $this->exitCodeText;
- }
-
- public function __destruct()
- {
- $this->releaseLock();
- }
-}
diff --git a/src/Libs/Scheduler/TaskTimer.php b/src/Libs/Scheduler/TaskTimer.php
deleted file mode 100644
index 6beb9498..00000000
--- a/src/Libs/Scheduler/TaskTimer.php
+++ /dev/null
@@ -1,256 +0,0 @@
-format('i')} {$date->format('H')} {$date->format('d')} {$date->format('m')} *");
- }
-
- /**
- * Set the execution time to every minute.
- *
- * @param int|string|null $minute When set, specifies that the Task will be run every $minute minutes
- */
- public static function everyMinute(int|string|null $minute = null): CronExpression
- {
- $minuteExpression = '*';
- if ($minute !== null) {
- $c = self::validateCronSequence($minute);
- $minuteExpression = '*/' . $c['minute'];
- }
-
- return self::at($minuteExpression . ' * * * *');
- }
-
- /**
- * Set the execution time to every hour.
- *
- * @param int|string $minute default [0].
- */
- public static function hourly(int|string $minute = 0): CronExpression
- {
- $c = self::validateCronSequence($minute);
-
- return self::at("{$c['minute']} * * * *");
- }
-
- /**
- * Set the execution time to once a day.
- *
- * @param int|string $hour Default [0]
- * @param int|string $minute Default [0]
- */
- public static function daily(string|int $hour = 0, string|int $minute = 0): CronExpression
- {
- if (is_string($hour)) {
- $parts = explode(':', $hour);
- $hour = $parts[0];
- $minute = $parts[1] ?? '0';
- }
-
- $c = self::validateCronSequence($minute, $hour);
-
- return self::at("{$c['minute']} {$c['hour']} * * *");
- }
-
- /**
- * Set the execution time to once a week.
- *
- * @param int|string $weekday Default [0]
- * @param int|string $hour Default [0]
- * @param int|string $minute Default [0]
- */
- public static function weekly(int|string $weekday = 0, int|string $hour = 0, int|string $minute = 0): CronExpression
- {
- if (is_string($hour)) {
- $parts = explode(':', $hour);
- $hour = $parts[0];
- $minute = $parts[1] ?? '0';
- }
-
- $c = self::validateCronSequence($minute, $hour, null, null, $weekday);
-
- return self::at("{$c['minute']} {$c['hour']} * * {$c['weekday']}");
- }
-
- /**
- * Set the execution time to once a month.
- *
- * @param int|string $month Default [*]
- * @param int|string $day Default [1]
- * @param int|string $hour Default [0]
- * @param int|string $minute Default [0]
- */
- public static function monthly(
- int|string $month = '*',
- int|string $day = 1,
- int|string $hour = 0,
- int|string $minute = 0
- ): CronExpression {
- if (is_string($hour)) {
- $parts = explode(':', $hour);
- $hour = $parts[0];
- $minute = $parts[1] ?? '0';
- }
- $c = self::validateCronSequence($minute, $hour, $day, $month);
- return self::at("{$c['minute']} {$c['hour']} {$c['day']} {$c['month']} *");
- }
-
- /**
- * Set the execution time to every Sunday.
- *
- * @param int|string $hour Default [0]
- * @param int|string $minute Default [0]
- */
- public static function sunday(int|string $hour = 0, int|string $minute = 0): CronExpression
- {
- return self::weekly(0, $hour, $minute);
- }
-
- /**
- * Set the execution time to every Monday.
- *
- * @param int|string $hour Default [0]
- * @param int|string $minute Default [0]
- */
- public static function monday(int|string $hour = 0, int|string $minute = 0): CronExpression
- {
- return self::weekly(1, $hour, $minute);
- }
-
- /**
- * Set the execution time to every Tuesday.
- *
- * @param int|string $hour Default [0]
- * @param int|string $minute Default [0]
- */
- public static function tuesday(int|string $hour = 0, int|string $minute = 0): CronExpression
- {
- return self::weekly(2, $hour, $minute);
- }
-
- /**
- * Set the execution time to every Wednesday.
- *
- * @param int|string $hour Default [0]
- * @param int|string $minute Default [0]
- */
- public static function wednesday(int|string $hour = 0, int|string $minute = 0): CronExpression
- {
- return self::weekly(3, $hour, $minute);
- }
-
- /**
- * Set the execution time to every Thursday.
- *
- * @param int|string $hour Default [0]
- * @param int|string $minute Default [0]
- */
- public static function thursday(int|string $hour = 0, int|string $minute = 0): CronExpression
- {
- return self::weekly(4, $hour, $minute);
- }
-
- /**
- * Set the execution time to every Friday.
- *
- * @param int|string $hour Default [0]
- * @param int|string $minute Default [0]
- */
- public static function friday(int|string $hour = 0, int|string $minute = 0): CronExpression
- {
- return self::weekly(5, $hour, $minute);
- }
-
- /**
- * Set the execution time to every Saturday.
- *
- * @param int|string $hour Default [0]
- * @param int|string $minute Default [0]
- */
- public static function saturday(int|string $hour = 0, int|string $minute = 0): CronExpression
- {
- return self::weekly(6, $hour, $minute);
- }
-
- /**
- * Validate sequence of cron expression.
- *
- * @param int|string|null $minute
- * @param int|string|null $hour
- * @param int|string|null $day
- * @param int|string|null $month
- * @param int|string|null $weekday
- * @return array
- */
- private static function validateCronSequence(
- int|string|null $minute = null,
- int|string|null $hour = null,
- int|string|null $day = null,
- int|string|null $month = null,
- int|string|null $weekday = null
- ): array {
- return [
- 'minute' => self::validateCronRange($minute, 0, 59),
- 'hour' => self::validateCronRange($hour, 0, 23),
- 'day' => self::validateCronRange($day, 1, 31),
- 'month' => self::validateCronRange($month, 1, 12),
- 'weekday' => self::validateCronRange($weekday, 0, 6),
- ];
- }
-
- /**
- * Validate sequence of cron expression.
- *
- * @param int|string|null $value
- * @param int $min
- * @param int $max
- * @return int|string
- */
- private static function validateCronRange(int|string|null $value, int $min, int $max): int|string
- {
- if (null === $value || '*' === $value) {
- return '*';
- }
-
- if (!is_numeric($value) || !($value >= $min && $value <= $max)) {
- throw new InvalidArgumentException(
- "Invalid value: it should be '*' or between {$min} and {$max}."
- );
- }
-
- return (int)$value;
- }
-
- private function __construct()
- {
- }
-}