'' . 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( r( <<[ FAQ ] ------- # How run scheduled tasks? To run scheduled tasks, Do the following {cmd} {route} --run # How to force run specific task? You have to combine both [--run] and [--task task_name], For example: {cmd} {route} --task import --run Running task in force mode, bypass the task enabled check. # How to configure tasks? All Prebuilt tasks have 3 environment variables associated 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 variable 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]. Simply put, run help on the associated command, and you can use any Options listed there in this variable. ## {TASK} Replace {TASK} tag in environment variables which one of the following [ {tasksList} ] environment variables are in ALL CAPITAL LETTERS. HELP, [ 'cmd' => trim(commandContext()), 'route' => self::ROUTE, 'tasksList' => $tasksName, ] ) ); } /** * 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 { if ($input->getOption('run')) { return $this->runTasks($input, $output); } $list = []; $mode = $input->getOption('output'); foreach (self::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 = []; $tasks = self::getTasks(); if (null !== ($task = $input->getOption('task'))) { $task = strtolower($task); if (false === ag_exists($tasks, $task)) { $output->writeln( r('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( r('[{datetime}] No task scheduled to run at this time.', [ 'datetime' => makeDate(), ]), iOutput::VERBOSITY_VERBOSE ); } foreach ($run as $task) { $cmd = []; $cmd[] = ROOT_PATH . '/bin/console'; $cmd[] = ag($task, 'command'); if (null !== ($args = ag($task, 'args'))) { $cmd[] = $args; } $process = Process::fromShellCommandline(implode(' ', $cmd), timeout: null); $started = makeDate()->format('D, H:i:s T'); $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; } $ended = makeDate()->format('D, H:i:s T'); $this->write('--------------------------', $input, $output); $this->write( r('Task: {name} (Started: {startDate})', [ 'name' => $task['name'], 'startDate' => $started, ]), $input, $output ); $this->write(r('Command: {cmd}', ['cmd' => $process->getCommandLine()]), $input, $output); $this->write( r('Exit Code: {code} (Ended: {endDate})', [ 'code' => $process->getExitCode(), 'endDate' => $ended, ]), $input, $output ); $this->write('--------------------------', $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) { 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('Failed to open log file [{file}]. Error [{message}].', [ '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); $output->writeln($text, $level); if ($input->getOption('save-log')) { $this->logs[] = $output->getLastMessage(); } } /** * Get the list of tasks. * * @param string|null $name The name of the task to get. * * @return array The list of tasks. */ public static function getTasks(string|null $name = null): 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(); } } if (null !== $name) { return ag($list, $name, []); } 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); 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); } } }