* @copyright 2021 DIGITAL RETAIL TECHNOLOGIES SL * @license https://opensource.org/licenses/MIT The MIT License * * Don't forget to prefix your containers with your own identifier * to avoid any conflicts with others containers. */ if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) { date_default_timezone_set(@date_default_timezone_get()); } require_once dirname(__FILE__) . '/../../../config/config.inc.php'; require_once dirname(__FILE__) . '/../../../init.php'; require_once dirname(__FILE__) . '/../bootstrap.php'; if (!defined('_PS_VERSION_')) { exit; } /** * Class RetailcrmCli * * @author DIGITAL RETAIL TECHNOLOGIES SL * @license GPL * * @see https://retailcrm.ru */ class RetailcrmCli { const CURRENT_TASK_CLI = 'RETAILCRM_JOB_CURRENT_CLI'; /** @var string CLI path */ private $cliPath; /** * RetailcrmCli constructor. * * @param string $cliPath */ public function __construct($cliPath) { RetailcrmLogger::setCloneToStdout(true); $this->cliPath = $cliPath; } /** * Run cli routine. Callable can be passed which will be used to handle terminate signals. * * @param callable|int|null $signalsHandler */ public function execute($signalsHandler = null) { if (function_exists('pcntl_signal')) { if (function_exists('pcntl_async_signals')) { pcntl_async_signals(true); } if (function_exists('pcntl_signal_dispatch')) { pcntl_signal_dispatch(); } if (!empty($signalsHandler) && (is_callable($signalsHandler) || function_exists($signalsHandler))) { pcntl_signal(SIGINT, $signalsHandler); pcntl_signal(SIGTERM, $signalsHandler); pcntl_signal(SIGHUP, $signalsHandler); } } else { RetailcrmLogger::output('WARNING: cannot handle signals properly, force stop can cause problems!'); } $shortopts = 'hj:s:'; $longopts = [ 'help', 'job:', 'shop:', 'query-shops', 'set-web-jobs:', 'query-web-jobs', 'run-jobs', 'reset-job-manager', 'reset-all', ]; $options = getopt($shortopts, $longopts); $jobName = isset($options['j']) ? $options['j'] : (isset($options['job']) ? $options['job'] : null); $shopId = isset($options['s']) ? $options['s'] : (isset($options['shop']) ? $options['shop'] : null); if (isset($options['reset-job-manager'])) { $this->resetJobManager(); } elseif (isset($options['reset-all'])) { $this->resetAll(); } elseif (isset($options['query-web-jobs'])) { $this->queryWebJobs($shopId); } elseif (isset($options['run-jobs'])) { RetailcrmTools::startJobManager(); } elseif (isset($options['set-web-jobs'])) { $this->setWebJobs(self::getBool($options['set-web-jobs']), $shopId); } elseif (isset($options['query-shops'])) { $this->queryShops(); } elseif (!empty($jobName)) { $this->setCleanupOnShutdown(); $this->runJob($jobName, $shopId); } elseif (isset($options['help']) || isset($options['h'])) { $this->printHelp(); } else { $this->printArgumentError('Unknown argument!'); } } /** * Shutdown handler. Moved here in order to keep compatibility with older PHP versions. * * @param mixed $error */ public function cleanupOnShutdown($error) { if (null !== $error) { self::clearCurrentJob(null); } } /** * This will register shutdown handler which will clean lock before shutdown */ private function setCleanupOnShutdown() { RetailcrmJobManager::setCustomShutdownHandler([$this, 'cleanupOnShutdown']); } /** * Runs provided job * * @param string $jobName */ private function runJob($jobName, $shopId) { try { $result = RetailcrmJobManager::runJob($jobName, true, false, $shopId); RetailcrmLogger::output(sprintf( 'Job %s was executed, result: %s', $jobName, $result ? 'true' : 'false' )); } catch (Exception $exception) { $this->handleException($jobName, $exception); } catch (Error $exception) { $this->handleException($jobName, $exception); } if (isset($result) && $result) { self::clearCurrentJob($jobName); } } /** * Prints error details * * @param Exception|Error $exception * @param string $header */ private function printStack($exception, $header = 'Error while executing a job: ') { RetailcrmLogger::output(sprintf('%s%s', $header, $exception->getMessage())); RetailcrmLogger::output(sprintf('%s:%d', $exception->getFile(), $exception->getLine())); RetailcrmLogger::output(); RetailcrmLogger::output($exception->getTraceAsString()); } /** * Prints CLI help */ private function printHelp() { RetailcrmLogger::output('Available jobs:'); RetailcrmLogger::output(); foreach ($this->getAllowedJobs() as $job) { RetailcrmLogger::output(sprintf(' - %s', $job)); } RetailcrmLogger::output(); RetailcrmLogger::output('Usage:'); RetailcrmLogger::output(); RetailcrmLogger::output(sprintf('> php %s -j - Runs provided job', $this->cliPath)); RetailcrmLogger::output(sprintf('> php %s --job - Runs provided job', $this->cliPath)); RetailcrmLogger::output(sprintf('> php %s --run-jobs - Run default jobs routine', $this->cliPath)); RetailcrmLogger::output(sprintf('> php %s --set-web-jobs true / false - Enable or disable web jobs', $this->cliPath)); RetailcrmLogger::output(sprintf('> php %s --query-web-jobs - Check web jobs status', $this->cliPath)); RetailcrmLogger::output(sprintf('> php %s --query-shops - Get list of shops with ids (for MultiShop)', $this->cliPath)); RetailcrmLogger::output(sprintf('> php %s -h - Shows this page', $this->cliPath)); RetailcrmLogger::output(sprintf('> php %s --help - Shows this page', $this->cliPath)); RetailcrmLogger::output(); RetailcrmLogger::output( 'NOTICE: If you have MultiShop feature enabled, you can additionally ' . 'specify shop id when manually running job: ' ); RetailcrmLogger::output('At default jobs are running for all active shops alternately.'); RetailcrmLogger::output(); RetailcrmLogger::output(sprintf('> php %s -j -s - Runs provided job for specified shop', $this->cliPath)); RetailcrmLogger::output(sprintf('> php %s --job --shop - Runs provided job for specified shop', $this->cliPath)); RetailcrmLogger::output(); RetailcrmLogger::output( 'WARNING: Commands below are dangerous and should be used only when ' . "job manager or cli doesn't work properly." ); RetailcrmLogger::output('Use them at your own risk.'); RetailcrmLogger::output(); RetailcrmLogger::output(sprintf( '> php %s --reset-job-manager - Will reset job manager internal timers & current job name', $this->cliPath )); RetailcrmLogger::output(sprintf( '> php %s --reset-all - Will reset the entire job subsystem state, can resolve most problems', $this->cliPath )); RetailcrmLogger::output(); } private function printArgumentError($message = '') { if (!empty($message)) { RetailcrmLogger::output($message); } RetailcrmLogger::output('Use -h or --help to get more info about CLI'); } /** * Sets new web jobs state * * @param bool $state * @param $shopId */ private function setWebJobs($state, $shopId = null) { $shopId = $this->setShopId($shopId); if (null === $shopId) { return; } $this->loadConfiguration(); Configuration::updateValue(RetailCRM::ENABLE_WEB_JOBS, $state ? '1' : '0'); RetailcrmLogger::output('Updated web jobs state.'); $this->queryWebJobs($shopId); } /** * Prints web jobs status * * @param $shopId */ private function queryWebJobs($shopId = null) { $shopId = $this->setShopId($shopId); if (null === $shopId) { return; } $this->loadConfiguration(); RetailcrmLogger::output(sprintf( 'Web jobs status: %s', RetailcrmTools::isWebJobsEnabled() ? 'true (enabled)' : 'false (disabled)' )); } private function queryShops() { $isFeatureActive = Shop::isFeatureActive(); RetailcrmLogger::output(sprintf( 'Multistore status: %s', $isFeatureActive ? 'true (enabled)' : 'false (disabled)' )); $shops = RetailcrmContextSwitcher::getShops(); RetailcrmLogger::output( "\nShop ID\t| Shop Name" ); foreach ($shops as $shop) { RetailcrmLogger::output(sprintf( "%s\t- %s", $shop['id_shop'], $shop['name'] )); } } private function setShopId($shopId) { if (null === $shopId) { $shopId = Shop::getContextShopID(); } if (null === $shopId) { $this->printArgumentError('You must specify shop id'); } else { RetailcrmContextSwitcher::setShopContext($shopId); } return $shopId; } /** * Load PrestaShop configuration if it's not loaded yet */ private function loadConfiguration() { if (!Configuration::configurationIsLoaded()) { Configuration::loadConfiguration(); } } /** * Resets JobManager */ private function resetJobManager() { try { if (RetailcrmJobManager::reset()) { RetailcrmLogger::output('Job manager internal state was cleared.'); } else { RetailcrmLogger::output('Job manager internal state was NOT cleared.'); } } catch (Exception $exception) { $this->printStack($exception); } catch (Error $exception) { $this->printStack($exception); } } /** * Resets JobManager and cli internal lock */ private function resetAll() { $this->resetJobManager(); self::clearCurrentJob(null); RetailcrmLogger::output('CLI command lock was cleared.'); } /** * Sets current running job. Every job must call this in CLI in order to work properly. * Current running job will be cleared automatically after job was finished (or crashed). * That way, JobManager will maintain it's data integrity and will coexist with manual runs and cron. * * @param string $job * * @return bool */ public static function setCurrentJob($job) { return (bool) Configuration::updateGlobalValue(self::CURRENT_TASK_CLI, $job); } /** * Returns current job or empty string if there's no jobs running at this moment * * @return string */ public static function getCurrentJob() { return (string) Configuration::getGlobalValue(self::CURRENT_TASK_CLI); } /** * Clears current job (job name must be provided to ensure we're removed correct job). * * @param string|null $job * * @return bool */ public static function clearCurrentJob($job) { if (null === $job || self::getCurrentJob() == $job) { return Configuration::deleteByName(self::CURRENT_TASK_CLI); } return true; } /** * Converts string param from CLI into boolean * * @param string $param */ private static function getBool($param) { if ('true' == $param) { return true; } if ('false' == $param) { return false; } return (bool) $param; } /** * Returns list of jobs which are allowed to be executed via cli * * @return string[] */ private function getAllowedJobs() { return [ 'RetailcrmAbandonedCartsEvent', 'RetailcrmIcmlEvent', 'RetailcrmIcmlUpdateUrlEvent', 'RetailcrmSyncEvent', 'RetailcrmInventoriesEvent', 'RetailcrmExportEvent', 'RetailcrmUpdateSinceIdEvent', 'RetailcrmClearLogsEvent', ]; } private function handleException($jobName, $exception) { if ($exception instanceof RetailcrmJobManagerException && ( $exception->getPrevious() instanceof Exception || $exception->getPrevious() instanceof Error ) ) { $this->printStack($exception->getPrevious()); } else { $this->printStack($exception); } self::clearCurrentJob($jobName); } }