Skip to content

Commit

Permalink
Merge pull request #200 from totten/master-updb-3
Browse files Browse the repository at this point in the history
Combine "upgrade:db" and "ext:upgrade-db". Support "updb" alias.
  • Loading branch information
colemanw authored Jun 23, 2024
2 parents b1f0a67 + 3ddd746 commit 5c16f1d
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 73 deletions.
1 change: 1 addition & 0 deletions src/Command/ExtensionUpgradeDbCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ protected function configure() {
}

protected function execute(InputInterface $input, OutputInterface $output) {
$output->writeln("<error>WARNING: \"ext:upgrade-db\" is deprecated. Use the main \"updb\" command instead.</error>");
$this->boot($input, $output);

$output->writeln("<info>Applying database upgrades from extensions</info>");
Expand Down
237 changes: 164 additions & 73 deletions src/Command/UpgradeDbCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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);
Expand All @@ -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("<info>Found CiviCRM database version <comment>%s</comment>.</info>", $dbVer), $niceMsgVerbosity);
$output->writeln(sprintf("<info>Found CiviCRM code version <comment>%s</comment>.</info>", $codeVer), $niceMsgVerbosity);
$output->writeln(sprintf("<info>Found CiviCRM database version <comment>%s</comment>.</info>", $dbVer), $this->niceVerbosity);
$output->writeln(sprintf("<info>Found CiviCRM code version <comment>%s</comment>.</info>", $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("<info>Have a nice day.</info>");
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();

Expand All @@ -87,65 +176,63 @@ protected function execute(InputInterface $input, OutputInterface $output) {
}

if ($isFirstTry) {
$output->writeln("<info>Checking pre-upgrade messages...</info>", $niceMsgVerbosity);
$output->writeln("<info>Checking pre-upgrade messages...</info>", $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("<error>Abort</error>");
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("<info>Dropping SQL triggers...</info>", $niceMsgVerbosity);
$output->writeln("<info>Dropping SQL triggers...</info>", $this->niceVerbosity);
if (!$input->getOption('dry-run')) {
\CRM_Core_DAO::dropTriggers();
}
}

if ($isFirstTry) {
$output->writeln("<info>Preparing upgrade...</info>", $niceMsgVerbosity);
file_put_contents($postUpgradeMessageFile, "");
chmod($postUpgradeMessageFile, 0700);
$output->writeln("<info>Preparing upgrade...</info>", $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("<info>Resuming upgrade...</info>", $niceMsgVerbosity);
$queue = \CRM_Queue_Service::singleton()->load(array(
$output->writeln("<info>Resuming upgrade...</info>", $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("<error>Skip task: %s</error>", $item->data->title));
$queue->deleteItem($item);
}

}

$output->writeln("<info>Executing upgrade...</info>", $niceMsgVerbosity);
$output->writeln("<info>Executing upgrade...</info>", $this->niceVerbosity);
$runner = new ConsoleQueueRunner($this->getIO(), $queue, $input->getOption('dry-run'), $input->getOption('step'));
$runner->runAll();

$output->writeln("<info>Finishing upgrade...</info>", $niceMsgVerbosity);
$output->writeln("<info>Finishing upgrade...</info>", $this->niceVerbosity);
if (!$input->getOption('dry-run')) {
\CRM_Upgrade_Form::doFinish();
}

$output->writeln("<info>Upgrade to <comment>$codeVer</comment> completed.</info>", $niceMsgVerbosity);
if (version_compare($codeVer, '5.53.alpha1', '<')) {
// Note: Before 5.53+, core-upgrade didn't touch extensions.
$this->runExtensionUpgrade($isFirstTry);
}

$output->writeln("<info>Upgrade to <comment>$codeVer</comment> completed.</info>", $this->niceVerbosity);

if (version_compare($codeVer, '5.26.alpha', '<')) {
// Work-around for bugs like dev/core#1713.
Expand All @@ -154,68 +241,72 @@ protected function execute(InputInterface $input, OutputInterface $output) {
\Civi\Cv\Util\Cv::passthru("flush");
}

$output->writeln("<info>Checking post-upgrade messages...</info>", $niceMsgVerbosity);
return 0;
}

protected function sendMessages(string $postUpgradeMessageFile, string $codeVer): void {
$input = $this->input;
$output = $this->output;

$output->writeln("<info>Checking post-upgrade messages...</info>", $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("<info>Have a nice day.</info>", $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("<info>Preparing extension upgrade...</info>", $this->niceVerbosity);
\CRM_Core_Invoke::rebuildMenuAndCaches(TRUE);
$queue = \CRM_Extension_Upgrades::createQueue();
$this->assertResumableQueue($queue);
}
else {
$output->writeln("<info>Resuming extension upgrade...</info>", $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("<error>Skip task: %s</error>", $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.");
}
}

}

0 comments on commit 5c16f1d

Please sign in to comment.