From 3ddd746955578091f4165ee8f57f1f695aa2f1b4 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 21 Jun 2024 18:26:46 -0400 Subject: [PATCH] Combine "upgrade:db" and "ext:upgrade-db". Support "updb" alias. --- src/Command/ExtensionUpgradeDbCommand.php | 1 + src/Command/UpgradeDbCommand.php | 237 +++++++++++++++------- 2 files changed, 165 insertions(+), 73 deletions(-) diff --git a/src/Command/ExtensionUpgradeDbCommand.php b/src/Command/ExtensionUpgradeDbCommand.php index 5f7b72e7..6724c2b1 100644 --- a/src/Command/ExtensionUpgradeDbCommand.php +++ b/src/Command/ExtensionUpgradeDbCommand.php @@ -31,6 +31,7 @@ protected function configure() { } protected function execute(InputInterface $input, OutputInterface $output) { + $output->writeln("WARNING: \"ext:upgrade-db\" is deprecated. Use the main \"updb\" command instead."); $this->boot($input, $output); $output->writeln("Applying database upgrades from extensions"); diff --git a/src/Command/UpgradeDbCommand.php b/src/Command/UpgradeDbCommand.php index 20e53e77..a5a5cb32 100644 --- a/src/Command/UpgradeDbCommand.php +++ b/src/Command/UpgradeDbCommand.php @@ -19,12 +19,14 @@ class UpgradeDbCommand extends BaseCommand { protected function configure() { $this ->setName('upgrade:db') + ->setAliases(['updb']) ->setDescription('Run the database upgrade') ->configureOutputOptions(['fallback' => 'pretty']) ->addOption('dry-run', NULL, InputOption::VALUE_NONE, 'Preview the list of upgrade tasks') ->addOption('retry', NULL, InputOption::VALUE_NONE, 'Resume a failed upgrade, retrying the last step') ->addOption('skip', NULL, InputOption::VALUE_NONE, 'Resume a failed upgrade, skipping the last step') ->addOption('step', NULL, InputOption::VALUE_NONE, 'Run the upgrade queue in steps, pausing before each step') + ->addOption('mode', NULL, InputOption::VALUE_REQUIRED, 'Mode to run upgrade (auto|full|ext)', 'auto') ->setHelp('Run the database upgrade Examples: @@ -35,6 +37,24 @@ protected function configure() { $this->configureBootOptions(); } + /** + * @var \Symfony\Component\Console\Input\InputInterface + */ + protected $input; + + /** + * @var \Symfony\Component\Console\Output\OutputInterface + */ + protected $output; + + protected $niceVerbosity; + + protected function initialize(InputInterface $input, OutputInterface $output) { + $this->input = $input; + $this->output = $output; + parent::initialize($input, $output); + } + protected function execute(InputInterface $input, OutputInterface $output) { if (!defined('CIVICRM_UPGRADE_ACTIVE')) { define('CIVICRM_UPGRADE_ACTIVE', 1); @@ -54,31 +74,100 @@ protected function execute(InputInterface $input, OutputInterface $output) { } } - $niceMsgVerbosity = $input->getOption('out') === 'pretty' ? OutputInterface::VERBOSITY_NORMAL : OutputInterface::VERBOSITY_VERBOSE; + $this->niceVerbosity = $input->getOption('out') === 'pretty' ? OutputInterface::VERBOSITY_NORMAL : OutputInterface::VERBOSITY_VERBOSE; $isFirstTry = !$input->getOption('retry') && !$input->getOption('skip'); $codeVer = \CRM_Utils_System::version(); $dbVer = \CRM_Core_BAO_Domain::version(); $postUpgradeMessageFile = $this->getUpgradeFile(); - $output->writeln(sprintf("Found CiviCRM database version %s.", $dbVer), $niceMsgVerbosity); - $output->writeln(sprintf("Found CiviCRM code version %s.", $codeVer), $niceMsgVerbosity); + $output->writeln(sprintf("Found CiviCRM database version %s.", $dbVer), $this->niceVerbosity); + $output->writeln(sprintf("Found CiviCRM code version %s.", $codeVer), $this->niceVerbosity); - if (version_compare($codeVer, $dbVer) == 0) { - $result = array( - 'latestVer' => $codeVer, - 'message' => "You are already upgraded to CiviCRM $codeVer", - ); - $result['text'] = $result['message']; - $this->sendResult($input, $output, $result); - return 0; + if ($isFirstTry) { + file_put_contents($postUpgradeMessageFile, ""); + chmod($postUpgradeMessageFile, 0700); + } + if (!$isFirstTry && !file_exists($postUpgradeMessageFile)) { + throw new \Exception("Cannot resume upgrade: The log file ($postUpgradeMessageFile) is missing. Consider a regular upgrade (without --retry or --skip)."); + } + + $result = 0; + $mode = $input->getOption('mode'); + if ($mode === 'auto') { + $mode = (version_compare($dbVer, $codeVer, '<') ? 'full' : 'ext'); } + if ($mode === 'full') { + $result += $this->runCoreUpgrade($isFirstTry, $dbVer, $postUpgradeMessageFile, $codeVer); + $this->sendMessages($postUpgradeMessageFile, $codeVer); + } + elseif ($mode === 'ext') { + $result += $this->runExtensionUpgrade($isFirstTry); + } + + $output->writeln("Have a nice day."); + return $result; + } + + /** + * Determine the path to the upgrade messages file. + * + * @return string + * The full path to the upgrade data file. + * This path should be reproducible, so that the failed+resumed + * upgrades use the same file. + */ + protected function getUpgradeFile() { + $home = getenv('HOME') ? getenv('HOME') : getenv('USERPROFILE'); + if (empty($home) || !file_exists($home)) { + throw new \RuntimeException("Failed to locate HOME or USERPROFILE"); + } + + $dir = implode(DIRECTORY_SEPARATOR, [$home, '.cv', 'upgrade']); + if (!file_exists($dir)) { + if (!mkdir($dir, 0777, TRUE)) { + throw new \RuntimeException("Failed to initialize upgrade data folder: $dir"); + } + } + + $id = md5(implode(\CRM_Core_DAO::VALUE_SEPARATOR, array( + function_exists('posix_getuid') ? posix_getuid() : 0, + $home, + CIVICRM_SETTINGS_PATH, + $GLOBALS['civicrm_root'], + + // e.g. one codebase, multi database + parse_url(CIVICRM_DSN, PHP_URL_PATH), + + // e.g. CMS vs extern vs installer + \CRM_Utils_Array::value('SCRIPT_FILENAME', $_SERVER, ''), + + // e.g. name-based vhosts + \CRM_Utils_Array::value('HTTP_HOST', $_SERVER, ''), + + // e.g. port-based vhosts + \CRM_Utils_Array::value('SERVER_PORT', $_SERVER, ''), + ))); + + return $dir . DIRECTORY_SEPARATOR . $id . '.dat'; + } + + /** + * @param bool $isFirstTry + * @param string $dbVer + * @param string $postUpgradeMessageFile + * @param string $codeVer + * + * @return int|null + * @throws \CRM_Core_Exception + */ + protected function runCoreUpgrade(bool $isFirstTry, string $dbVer, string $postUpgradeMessageFile, string $codeVer): ?int { + $input = $this->input; + $output = $this->output; + if ($isFirstTry && FALSE !== stripos($dbVer, 'upgrade')) { throw new \Exception("Cannot begin upgrade: The database indicates that an incomplete upgrade is pending. If you would like to resume, use --retry or --skip."); } - if (!$isFirstTry && !file_exists($postUpgradeMessageFile)) { - throw new \Exception("Cannot resume upgrade: The log file ($postUpgradeMessageFile) is missing. Consider a regular upgrade (without --retry or --skip)."); - } $upgrade = new \CRM_Upgrade_Form(); @@ -87,65 +176,63 @@ protected function execute(InputInterface $input, OutputInterface $output) { } if ($isFirstTry) { - $output->writeln("Checking pre-upgrade messages...", $niceMsgVerbosity); + $output->writeln("Checking pre-upgrade messages...", $this->niceVerbosity); $preUpgradeMessage = NULL; $upgrade->setPreUpgradeMessage($preUpgradeMessage, $dbVer, $codeVer); if ($preUpgradeMessage) { - $output->writeln(\CRM_Utils_String::htmlToText($preUpgradeMessage), $niceMsgVerbosity); + $output->writeln(\CRM_Utils_String::htmlToText($preUpgradeMessage), $this->niceVerbosity); if (!$this->getIO()->confirm('Continue?')) { $output->writeln("Abort"); return 1; } } else { - $output->writeln("(No messages)", $niceMsgVerbosity); + $output->writeln("(No messages)", $this->niceVerbosity); } } // Why is dropTriggers() hard-coded? Can't we just enqueue this as part of buildQueue()? if ($isFirstTry) { - $output->writeln("Dropping SQL triggers...", $niceMsgVerbosity); + $output->writeln("Dropping SQL triggers...", $this->niceVerbosity); if (!$input->getOption('dry-run')) { \CRM_Core_DAO::dropTriggers(); } } if ($isFirstTry) { - $output->writeln("Preparing upgrade...", $niceMsgVerbosity); - file_put_contents($postUpgradeMessageFile, ""); - chmod($postUpgradeMessageFile, 0700); + $output->writeln("Preparing upgrade...", $this->niceVerbosity); $queue = \CRM_Upgrade_Form::buildQueue($dbVer, $codeVer, $postUpgradeMessageFile); - - if (!($queue instanceof \CRM_Queue_Queue_Sql)) { - // Sanity check -- only SQL queues are resuamble. - throw new \RuntimeException("Error: \"cv upgrade\" only supports SQL-based queues."); - } + $this->assertResumableQueue($queue); } else { - $output->writeln("Resuming upgrade...", $niceMsgVerbosity); - $queue = \CRM_Queue_Service::singleton()->load(array( + $output->writeln("Resuming upgrade...", $this->niceVerbosity); + $queue = \CRM_Queue_Service::singleton()->load([ 'name' => \CRM_Upgrade_Form::QUEUE_NAME, 'type' => 'Sql', - )); + ]); if ($input->getOption('skip')) { $item = $queue->stealItem(); $output->writeln(sprintf("Skip task: %s", $item->data->title)); $queue->deleteItem($item); } - } - $output->writeln("Executing upgrade...", $niceMsgVerbosity); + $output->writeln("Executing upgrade...", $this->niceVerbosity); $runner = new ConsoleQueueRunner($this->getIO(), $queue, $input->getOption('dry-run'), $input->getOption('step')); $runner->runAll(); - $output->writeln("Finishing upgrade...", $niceMsgVerbosity); + $output->writeln("Finishing upgrade...", $this->niceVerbosity); if (!$input->getOption('dry-run')) { \CRM_Upgrade_Form::doFinish(); } - $output->writeln("Upgrade to $codeVer completed.", $niceMsgVerbosity); + if (version_compare($codeVer, '5.53.alpha1', '<')) { + // Note: Before 5.53+, core-upgrade didn't touch extensions. + $this->runExtensionUpgrade($isFirstTry); + } + + $output->writeln("Upgrade to $codeVer completed.", $this->niceVerbosity); if (version_compare($codeVer, '5.26.alpha', '<')) { // Work-around for bugs like dev/core#1713. @@ -154,68 +241,72 @@ protected function execute(InputInterface $input, OutputInterface $output) { \Civi\Cv\Util\Cv::passthru("flush"); } - $output->writeln("Checking post-upgrade messages...", $niceMsgVerbosity); + return 0; + } + + protected function sendMessages(string $postUpgradeMessageFile, string $codeVer): void { + $input = $this->input; + $output = $this->output; + + $output->writeln("Checking post-upgrade messages...", $this->niceVerbosity); $message = file_get_contents($postUpgradeMessageFile); if ($input->getOption('out') === 'pretty') { if ($message) { $output->writeln(\CRM_Utils_String::htmlToText($message), OutputInterface::OUTPUT_RAW); } else { - $output->writeln("(No messages)", $niceMsgVerbosity); + $output->writeln("(No messages)", $this->niceVerbosity); } - $output->writeln("Have a nice day.", $niceMsgVerbosity); } else { - $this->sendResult($input, $output, array( + $this->sendResult($input, $output, [ 'latestVer' => $codeVer, 'message' => $message, 'text' => \CRM_Utils_String::htmlToText($message), - )); + ]); } unlink($postUpgradeMessageFile); } - /** - * Determine the path to the upgrade messages file. - * - * @return string - * The full path to the upgrade data file. - * This path should be reproducible, so that the failed+resumed - * upgrades use the same file. - */ - protected function getUpgradeFile() { - $home = getenv('HOME') ? getenv('HOME') : getenv('USERPROFILE'); - if (empty($home) || !file_exists($home)) { - throw new \RuntimeException("Failed to locate HOME or USERPROFILE"); + protected function runExtensionUpgrade(bool $isFirstTry): int { + $input = $this->input; + $output = $this->output; + + if ($isFirstTry) { + $output->writeln("Preparing extension upgrade...", $this->niceVerbosity); + \CRM_Core_Invoke::rebuildMenuAndCaches(TRUE); + $queue = \CRM_Extension_Upgrades::createQueue(); + $this->assertResumableQueue($queue); } + else { + $output->writeln("Resuming extension upgrade...", $this->niceVerbosity); + $queue = \CRM_Queue_Service::singleton()->load([ + 'name' => \CRM_Extension_Upgrades::QUEUE_NAME, + 'type' => 'Sql', + ]); - $dir = implode(DIRECTORY_SEPARATOR, [$home, '.cv', 'upgrade']); - if (!file_exists($dir)) { - if (!mkdir($dir, 0777, TRUE)) { - throw new \RuntimeException("Failed to initialize upgrade data folder: $dir"); + if ($input->getOption('skip')) { + $item = $queue->stealItem(); + $output->writeln(sprintf("Skip task: %s", $item->data->title)); + $queue->deleteItem($item); } } - $id = md5(implode(\CRM_Core_DAO::VALUE_SEPARATOR, array( - function_exists('posix_getuid') ? posix_getuid() : 0, - $home, - CIVICRM_SETTINGS_PATH, - $GLOBALS['civicrm_root'], - - // e.g. one codebase, multi database - parse_url(CIVICRM_DSN, PHP_URL_PATH), - - // e.g. CMS vs extern vs installer - \CRM_Utils_Array::value('SCRIPT_FILENAME', $_SERVER, ''), - - // e.g. name-based vhosts - \CRM_Utils_Array::value('HTTP_HOST', $_SERVER, ''), - - // e.g. port-based vhosts - \CRM_Utils_Array::value('SERVER_PORT', $_SERVER, ''), - ))); + $runner = new ConsoleQueueRunner($this->getIO(), $queue, $input->getOption('dry-run'), $input->getOption('step')); + $runner->runAll(); + return 0; + } - return $dir . DIRECTORY_SEPARATOR . $id . '.dat'; + /** + * @param \CRM_Queue_Service $queue + * + * @return void + */ + protected function assertResumableQueue($queue): void { + if (!($queue instanceof \CRM_Queue_Queue_Sql)) { + // Sanity check -- only SQL queues are resuamble. + throw new \RuntimeException("Error: \"cv upgrade\" only supports SQL-based queues."); + } } }