[ Mini Kiebo ]
Server: Windows NT DESKTOP-5B8S0D4 6.2 build 9200 (Windows 8 Professional Edition) i586
Path:
D:
/
Backup
/
05122024
/
htdocs
/
jurnal-kesmas
/
lib
/
pkp
/
tools
/
[
Home
]
File: jobs.php
<?php declare(strict_types=1); /** * @file tools/jobs.php * * Copyright (c) 2014-2022 Simon Fraser University * Copyright (c) 2003-2022 John Willinsky * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. * * @class commandJobs * * @ingroup tools * * @brief CLI tool to list, iterate and purge queued jobs on database */ namespace PKP\tools; use APP\core\Application; use APP\facades\Repo; use Carbon\Carbon; use Illuminate\Console\Concerns\InteractsWithIO; use Illuminate\Console\OutputStyle; use Illuminate\Contracts\Queue\Job; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; use PKP\cliTool\CommandLineTool; use PKP\config\Config; use PKP\job\models\Job as PKPJobModel; use PKP\jobs\testJobs\TestJobFailure; use PKP\jobs\testJobs\TestJobSuccess; use PKP\queue\WorkerConfiguration; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\InvalidArgumentException as CommandInvalidArgumentException; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Helper\TableCellStyle; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\StreamOutput; use Throwable; define('APP_ROOT', dirname(__FILE__, 4)); require_once APP_ROOT . '/tools/bootstrap.php'; class commandInterface { use InteractsWithIO; public function __construct() { $output = new OutputStyle( new StringInput(''), new StreamOutput(fopen('php://stdout', 'w')) ); $this->setOutput($output); } public function errorBlock(array $messages = [], ?string $title = null): void { $this->getOutput()->block( $messages, $title, 'fg=white;bg=red', ' ', true ); } } class commandJobs extends CommandLineTool { protected const AVAILABLE_OPTIONS = [ 'list' => 'admin.cli.tool.jobs.available.options.list.description', 'purge' => 'admin.cli.tool.jobs.available.options.purge.description', 'test' => 'admin.cli.tool.jobs.available.options.test.description', 'total' => 'admin.cli.tool.jobs.available.options.total.description', 'help' => 'admin.cli.tool.jobs.available.options.help.description', 'run' => 'admin.cli.tool.jobs.available.options.run.description', 'work' => 'admin.cli.tool.jobs.available.options.work.description', 'failed' => 'admin.cli.tool.jobs.available.options.failed.description', 'restart' => 'admin.cli.tool.jobs.available.options.restart.description', 'usage' => 'admin.cli.tool.jobs.available.options.usage.description', ]; protected const CURRENT_PAGE = 'current'; protected const PREVIOUS_PAGE = 'previous'; protected const NEXT_PAGE = 'next'; /** * @var null|string Which option will be call? */ protected $option = null; /** * @var null|array Parameters and arguments from CLI */ protected $parameterList = null; /** * CLI interface, this object should extends InteractsWithIO */ protected $commandInterface = null; /** * Constructor */ public function __construct($argv = []) { parent::__construct($argv); array_shift($argv); $this->setParameterList($argv); if (!isset($this->getParameterList()[0])) { throw new CommandNotFoundException( __('admin.cli.tool.jobs.empty.option'), array_keys(self::AVAILABLE_OPTIONS) ); } $this->option = $this->getParameterList()[0]; $this->setCommandInterface(new commandInterface()); } public function setCommandInterface(commandInterface $commandInterface): self { $this->commandInterface = $commandInterface; return $this; } public function getCommandInterface(): commandInterface { return $this->commandInterface; } /** * Save the parameter list passed on CLI * * @param array $items Array with parameters and arguments passed on CLI * */ public function setParameterList(array $items): self { $parameters = []; foreach ($items as $param) { if (strpos($param, '=')) { [$key, $value] = explode('=', ltrim($param, '-')); $parameters[$key] = $value; continue; } $parameters[] = $param; } $this->parameterList = $parameters; return $this; } /** * Get the parameter list passed on CLI * */ public function getParameterList(): ?array { return $this->parameterList; } /** * Get the value of a specific parameter * * @param mixed $default * */ protected function getParameterValue(string $parameter, mixed $default = null): mixed { if (!isset($this->getParameterList()[$parameter])) { return $default; } return $this->getParameterList()[$parameter]; } /** * Print command usage information. */ public function usage() { $this->getCommandInterface()->line('<comment>' . __('admin.cli.tool.usage.title') . '</comment>'); $this->getCommandInterface()->line(__('admin.cli.tool.usage.parameters') . PHP_EOL); $this->getCommandInterface()->line('<comment>' . __('admin.cli.tool.available.commands', ['namespace' => 'jobs']) . '</comment>'); $this->printUsage(self::AVAILABLE_OPTIONS); } /** * Alias for usage command */ public function help(): void { $this->usage(); } /** * Retrieve the columnWidth based on the commands text size */ protected function getColumnWidth(array $commands): int { $widths = []; foreach ($commands as $command) { $widths[] = Helper::width($command); } return $widths ? max($widths) + 2 : 0; } /** * Failed jobs list/redispatch/remove */ protected function failed(): void { $parameterList = $this->getParameterList(); if (in_array('--redispatch', $parameterList) || ($jobIds = $this->getParameterValue('redispatch'))) { $jobsCount = Repo::failedJob()->redispatchToQueue( $this->getParameterValue('queue'), collect(explode(',', $jobIds ?? '')) ->filter() ->map(fn ($item) => (int)$item) ->toArray() ); $this->getCommandInterface()->getOutput()->success(__('admin.cli.tool.jobs.failed.redispatch.successful', ['jobsCount' => $jobsCount])); return; } if (in_array('--clear', $parameterList) || ($jobIds = $this->getParameterValue('clear'))) { $jobsCount = Repo::failedJob()->deleteJobs( $this->getParameterValue('queue'), collect(explode(',', $jobIds ?? '')) ->filter() ->map(fn ($item) => (int)$item) ->toArray() ); $this->getCommandInterface()->getOutput()->success(__('admin.cli.tool.jobs.failed.clear.successful', ['jobsCount' => $jobsCount])); return; } array_push($this->parameterList, '--failed'); $this->list(); } /** * Signal the queue worker to quit gracefully */ protected function restart(): void { $cache = app()->get("cache.store"); /** @var \Illuminate\Contracts\Cache\Repository $cache */ $cache->forever('illuminate:queue:restart', Carbon::now()->getTimestamp()); $this ->getCommandInterface() ->getOutput() ->info(__('admin.cli.tool.jobs.available.options.restart.confirm')); } /** * List all queued jobs */ protected function list(): void { $perPage = $this->getParameterValue('perPage', '10'); $page = $this->getParameterValue('page', '1'); $parameterList = $this->getparameterList(); $repository = in_array('--failed', $parameterList) ? Repo::failedJob() : Repo::job(); $data = $repository ->setOutputFormat($repository::OUTPUT_CLI) ->perPage((int) $perPage) ->setPage((int) $page) ->showJobs(); $this->total(); $this->getCommandInterface()->table( $this->getListTableFormat(), $data ->map(fn(JsonResource $job) => $job->toArray(app('request'))) ->toArray() ); $pagination = [ 'pagination' => [ self::CURRENT_PAGE => $data->currentPage(), self::PREVIOUS_PAGE => ($data->currentPage() - 1) > 0 ? $data->currentPage() - 1 : 1, self::NEXT_PAGE => $data->currentPage(), ], ]; if ($data->hasMorePages()) { $pagination['pagination'][self::NEXT_PAGE] = $data->currentPage() + 1; } $this->getCommandInterface() ->table( [ [ new TableCell( __('admin.cli.tool.jobs.pagination'), [ 'colspan' => 3, 'style' => new TableCellStyle(['align' => 'center']) ] ) ], [ __('admin.cli.tool.jobs.pagination.current'), __('admin.cli.tool.jobs.pagination.previous'), __('admin.cli.tool.jobs.pagination.next'), ] ], $pagination ); } /** * Get table format for list view */ protected function getListTableFormat(): array { $listForFailedJobs = in_array('--failed', $this->getParameterList()); return [ [ new TableCell( $listForFailedJobs ? __('admin.cli.tool.jobs.queued.jobs.failed.title') : __('admin.cli.tool.jobs.queued.jobs.title'), [ 'colspan' => $listForFailedJobs ? 6 : 7, 'style' => new TableCellStyle(['align' => 'center']) ] ) ], array_merge([ __('admin.cli.tool.jobs.queued.jobs.fields.id'), __('admin.cli.tool.jobs.queued.jobs.fields.queue'), __('admin.cli.tool.jobs.queued.jobs.fields.job.display.name'), ], $listForFailedJobs ? [ __('admin.cli.tool.jobs.queued.jobs.fields.connection'), __('admin.cli.tool.jobs.queued.jobs.fields.failed.at'), __('admin.cli.tool.jobs.queued.jobs.fields.exception'), ] : [ __('admin.cli.tool.jobs.queued.jobs.fields.attempts'), __('admin.cli.tool.jobs.queued.jobs.fields.reserved.at'), __('admin.cli.tool.jobs.queued.jobs.fields.available.at'), __('admin.cli.tool.jobs.queued.jobs.fields.created.at') ]) ]; } /** * Run daemon worker process to continue handle jobs */ protected function work(): void { $parameterList = $this->getParameterList(); if (in_array('--help', $parameterList)) { $this->workerOptionsHelp(); return; } if (Application::isUnderMaintenance()) { $this->getCommandInterface()->getOutput()->error(__('admin.cli.tool.jobs.maintenance.message')); return; } if (Config::getVar('general', 'sandbox', false)) { $this->getCommandInterface()->getOutput()->error(__('admin.cli.tool.jobs.sandbox.message')); error_log(__('admin.cli.tool.jobs.sandbox.message')); return; } $connection = $parameterList['connection'] ?? Config::getVar('queues', 'default_connection', 'database'); $queue = $parameterList['queue'] ?? Config::getVar('queues', 'default_queue', 'queue'); if (in_array('--test', $parameterList)) { $queue = PKPJobModel::TESTING_QUEUE; } $this->listenForEvents(); app('pkpJobQueue')->runJobsViaDaemon( $connection, $queue, $this->gatherWorkerOptions($parameterList) ); } /** * Dispatch jobs into the queue */ protected function run(): void { if (Application::isUnderMaintenance()) { $this->getCommandInterface()->getOutput()->error(__('admin.cli.tool.jobs.maintenance.message')); return; } if (Config::getVar('general', 'sandbox', false)) { $this->getCommandInterface()->getOutput()->error(__('admin.cli.tool.jobs.sandbox.message')); error_log(__('admin.cli.tool.jobs.sandbox.message')); return; } $parameterList = $this->getParameterList(); $queue = $parameterList['queue'] ?? Config::getVar('queues', 'default_queue', 'queue'); if (in_array('--test', $parameterList)) { $queue = PKPJobModel::TESTING_QUEUE; } $jobQueue = app('pkpJobQueue'); if ($queue && is_string($queue)) { $jobQueue = $jobQueue->forQueue($queue); } $jobBuilder = $jobQueue->getJobModelBuilder(); if (($jobCount = $jobBuilder->count()) <= 0) { $this->getCommandInterface()->getOutput()->info( __( 'admin.cli.tool.jobs.available.options.run.empty.description', ['queueName' => $queue,] ) ); return; } $this->listenForEvents(); while ($jobBuilder->count()) { $jobQueue->runJobInQueue(); if (in_array('--once', $parameterList)) { $jobCount = 1; break; } } $this->getCommandInterface()->getOutput()->success( __( 'admin.cli.tool.jobs.available.options.run.completed.description', ['jobCount' => $jobCount, 'queueName' => $queue,] ) ); } /** * Purge queued jobs */ protected function purge(): void { if (!isset($this->getParameterList()['queue']) && !isset($this->getParameterList()[1])) { throw new CommandInvalidArgumentException(__('admin.cli.tool.jobs.purge.without.id')); } $parameterList = $this->getParameterList(); if (in_array('--all', $parameterList) || ($queue = $this->getParameterValue('queue'))) { if (!Repo::job()->deleteJobs($queue ?? null)) { $this->getCommandInterface()->getOutput()->warning(__('admin.cli.tool.jobs.purge.impossible.to.purge.empty')); return; } $this->getCommandInterface()->getOutput()->success(__('admin.cli.tool.jobs.purge.successful.all')); return; } $deleted = Repo::job()->delete((int) $this->getParameterList()[1]); if (!$deleted) { throw new CommandInvalidArgumentException(__('admin.cli.tool.jobs.purge.invalid.id')); } $this->getCommandInterface()->getOutput()->success(__('admin.cli.tool.jobs.purge.successful')); } /** * Create a test queued job */ protected function test(): void { $queue = PKPJobModel::TESTING_QUEUE; $runnableJob = $this->getParameterList()['only'] ?? null; if ($runnableJob && !in_array($runnableJob, ['failed', 'success'])) { throw new CommandInvalidArgumentException(__('admin.cli.tool.jobs.test.invalid.option')); } if (!$runnableJob || $runnableJob === 'failed') { dispatch(new TestJobFailure()); $this->getCommandInterface()->getOutput()->success(__('admin.cli.tool.jobs.test.job.failed.dispatch.message', ['queueName' => $queue])); } if (!$runnableJob || $runnableJob === 'success') { dispatch(new TestJobSuccess()); $this->getCommandInterface()->getOutput()->success(__('admin.cli.tool.jobs.test.job.success.dispatch.message', ['queueName' => $queue])); } } /** * Gather worker daemon options * */ protected function gatherWorkerOptions(array $parameters = []): array { $workerConfig = new WorkerConfiguration(); return [ 'name' => $this->getParameterValue('name', $workerConfig->getName()), 'backoff' => $this->getParameterValue('backoff', $workerConfig->getBackoff()), 'memory' => $this->getParameterValue('memory', $workerConfig->getMemory()), 'timeout' => $this->getParameterValue('timeout', $workerConfig->getTimeout()), 'sleep' => $this->getParameterValue('sleep', $workerConfig->getSleep()), 'maxTries' => $this->getParameterValue('tries', $workerConfig->getMaxTries()), 'force' => $this->getParameterValue('force', in_array('force', $parameters) ? true : $workerConfig->getForce()), 'stopWhenEmpty' => $this->getParameterValue('stop-when-empty', in_array('stop-when-empty', $parameters) ? true : $workerConfig->getStopWhenEmpty()), 'maxJobs' => $this->getParameterValue('max-jobs', $workerConfig->getMaxJobs()), 'maxTime' => $this->getParameterValue('max-time', $workerConfig->getMaxTime()), 'rest' => $this->getParameterValue('rest', $workerConfig->getRest()), ]; } /** * Listen for the queue events in order to update the console output. * */ protected function listenForEvents(): void { $events = app()['events']; $events->listen(JobProcessing::class, function ($event) { $this->writeOutput($event->job, 'starting'); }); $events->listen(JobProcessed::class, function ($event) { $this->writeOutput($event->job, 'success'); }); $events->listen(JobFailed::class, function ($event) { $this->writeOutput($event->job, 'failed'); }); } /** * Write the status output for the queue worker. * * @param string $status * */ protected function writeOutput(Job $job, $status): void { match ($status) { 'starting' => $this->writeStatus($job, 'Processing', 'comment'), 'success' => $this->writeStatus($job, 'Processed', 'info'), 'failed' => $this->writeStatus($job, 'Failed', 'error'), }; } /** * Format the status output for the queue worker. * * @param string $status * @param string $type */ protected function writeStatus(Job $job, $status, $type): void { $this->getCommandInterface()->getOutput()->writeln(sprintf( "<{$type}>[%s][%s] %s</{$type}> %s", Carbon::now()->format('Y-m-d H:i:s'), $job->getJobId(), str_pad("{$status}:", 11), $job->resolveName() )); } /** * Print work command options information. */ protected function workerOptionsHelp(): void { $this->getCommandInterface()->line('<comment>' . __('admin.cli.tool.jobs.work.options.title') . '</comment>'); $this->getCommandInterface()->line(__('admin.cli.tool.jobs.work.options.usage') . PHP_EOL); $this->getCommandInterface()->line('<comment>' . __('admin.cli.tool.jobs.work.options.description') . '</comment>'); $workerConfig = new WorkerConfiguration(); $options = [ '--connection[=CONNECTION]' => __('admin.cli.tool.jobs.work.option.connection.description', ['default' => Config::getVar('queue', 'default_connection', 'database')]), '--queue[=QUEUE]' => __('admin.cli.tool.jobs.work.option.queue.description', ['default' => Config::getVar('queue', 'default_queue', 'queue')]), '--name[=NAME]' => __('admin.cli.tool.jobs.work.option.name.description', ['default' => $workerConfig->getName()]), '--backoff[=BACKOFF]' => __('admin.cli.tool.jobs.work.option.backoff.description', ['default' => $workerConfig->getBackoff()]), '--memory[=MEMORY]' => __('admin.cli.tool.jobs.work.option.memory.description', ['default' => $workerConfig->getMemory()]), '--timeout[=TIMEOUT]' => __('admin.cli.tool.jobs.work.option.timeout.description', ['default' => $workerConfig->getTimeout()]), '--sleep[=SLEEP]' => __('admin.cli.tool.jobs.work.option.sleep.description', ['default' => $workerConfig->getSleep()]), '--tries[=TRIES]' => __('admin.cli.tool.jobs.work.option.tries.description', ['default' => $workerConfig->getMaxTries()]), '--force' => __('admin.cli.tool.jobs.work.option.force.description', ['default' => $workerConfig->getForce() ? 'true' : 'false']), '--stop-when-empty' => __('admin.cli.tool.jobs.work.option.stopWhenEmpty.description', ['default' => $workerConfig->getStopWhenEmpty() ? 'true' : 'false']), '--max-jobs[=MAX-JOBS]' => __('admin.cli.tool.jobs.work.option.maxJobs.description', ['default' => $workerConfig->getMaxJobs()]), '--max-time[=MAX-TIME]' => __('admin.cli.tool.jobs.work.option.maxTime.description', ['default' => $workerConfig->getMaxTime()]), '--rest[=REST]' => __('admin.cli.tool.jobs.work.option.rest.description', ['default' => $workerConfig->getRest()]), '--test' => __('admin.cli.tool.jobs.work.option.test.description'), ]; $this->printUsage($options, false); } /** * Print given options in a pretty way. */ protected function printUsage(array $options, bool $shouldTranslate = true): void { $width = $this->getColumnWidth(array_keys($options)); foreach ($options as $commandName => $description) { $spacingWidth = $width - Helper::width($commandName); $this->getCommandInterface()->line( sprintf( ' <info>%s</info>%s%s', $commandName, str_repeat(' ', $spacingWidth), $shouldTranslate ? __($description) : $description ) ); } } /** * Display the queued/failed jobs quantity */ protected function total(): void { $parameterList = $this->getParameterList(); $total = in_array('--failed', $parameterList) ? Repo::failedJob()->total() : Repo::job()->total(); $outputInterface = $this->getCommandInterface()->getOutput(); if (in_array('--failed', $parameterList)) { $method = $total > 0 ? 'error' : 'success'; $outputInterface->{$method}(__('admin.cli.tool.jobs.total.failed.jobs', ['total' => $total])); return; } $outputInterface->warning(__('admin.cli.tool.jobs.total.jobs', ['total' => $total])); } /** * Parse and execute the command */ public function execute() { if (!isset(self::AVAILABLE_OPTIONS[$this->option])) { throw new CommandNotFoundException( __('admin.cli.tool.jobs.option.doesnt.exists', ['option' => $this->option]), array_keys(self::AVAILABLE_OPTIONS) ); } $this->{$this->option}(); } } try { $tool = new commandJobs($argv ?? []); $tool->execute(); } catch (Throwable $e) { $output = new commandInterface(); if ($e instanceof CommandInvalidArgumentException) { $output->errorBlock([$e->getMessage()]); return; } if ($e instanceof CommandNotFoundException) { $alternatives = $e->getAlternatives(); $message = __('admin.cli.tool.jobs.mean.those') . PHP_EOL . implode(PHP_EOL, $alternatives); $output->errorBlock([$e->getMessage(), $message]); return; } throw $e; }