Permalink
Browse files

ESDEV-4156 Handle errors from external commands

`CommandExecutionFailedException` will be used as a way to
trigger showing an error insie setup from failed execution of
external commands, e.g.:

* Database migration tool
* Database views regenerator
  • Loading branch information...
1 parent dcd8268 commit 4e7d85452ddd8205d0eeed718f23259163247f23 @rezonanc-oxid rezonanc-oxid committed Jan 6, 2017
@@ -26,6 +26,7 @@
use OxidEsales\Eshop\Core\Edition\EditionSelector;
use OxidEsales\Eshop\Core\SystemRequirements;
use OxidEsales\EshopCommunity\Setup\Controller\ModuleStateMapGenerator;
+use OxidEsales\EshopCommunity\Setup\Exception\CommandExecutionFailedException;
use OxidEsales\EshopCommunity\Setup\Exception\SetupControllerExitException;
/**
@@ -274,6 +275,10 @@ public function dbConnect()
* `Acceptance/Frontend/ShopSetUpTest.php::testUserIsNotifiedIfAValidDatabaseAlreadyExistsBeforeTryingToOverwriteIt`
* `Acceptance/Frontend/ShopSetUpTest.php::testSetupRedirectsToDatabaseEntryPageWhenSetupSqlFileIsMissing`
* `Acceptance/Frontend/ShopSetUpTest.php::testSetupRedirectsToDatabaseEntryPageWhenSetupSqlFileHasSyntaxError`
+ * `Acceptance/Frontend/ShopSetUpTest.php::testSetupShowsErrorMessageWhenMigrationFileContainsSyntaxErrors`
+ * `Acceptance/Frontend/ShopSetUpTest.php::testSetupShowsErrorMessageWhenMigrationExecutableIsMissing`
+ * `Acceptance/Frontend/ShopSetUpTest.php::testSetupShowsErrorMessageWhenViewRegenerationReturnsErrorCode`
+ * `Acceptance/Frontend/ShopSetUpTest.php::testSetupShowsErrorMessageWhenViewsRegenerationExecutableIsMissing`
*/
public function dbCreate()
{
@@ -334,14 +339,23 @@ public function dbCreate()
// install demo/initial data
try {
$this->installShopData($database, $databaseConfigValues['dbiDemoData']);
+ } catch (CommandExecutionFailedException $exception) {
+ $this->handleCommandExecutionFailedException($exception);
+ throw new SetupControllerExitException();
} catch (Exception $exception) {
// there where problems with queries
$view->setMessage($language->getText('ERROR_BAD_DEMODATA') . "<br><br>" . $exception->getMessage());
throw new SetupControllerExitException();
}
- $this->getUtilitiesInstance()->regenerateViews();
+ try {
+ $this->getUtilitiesInstance()->executeExternalRegenerateViewsCommand();
+ } catch (CommandExecutionFailedException $exception) {
+ $this->handleCommandExecutionFailedException($exception);
+
+ throw new SetupControllerExitException();
+ }
} catch (Exception $exception) {
$view->setMessage($exception->getMessage());
@@ -591,16 +605,16 @@ private function installShopData($database, $demodata = 0)
// If demodata files are provided.
if ($this->getUtilitiesInstance()->checkIfDemodataPrepared($demodata)) {
- $this->getUtilitiesInstance()->migrateDatabase();
+ $this->getUtilitiesInstance()->executeExternalDatabaseMigrationCommand();
// Install demo data.
$database->queryFile($this->getUtilitiesInstance()->getActiveEditionDemodataPackageSqlFilePath());
// Copy demodata files.
- $this->getUtilitiesInstance()->demodataAssetsInstall();
+ $this->getUtilitiesInstance()->executeExternalDemodataAssetsInstallCommand();
} else {
$database->queryFile("$baseSqlDir/initial_data.sql");
- $this->getUtilitiesInstance()->migrateDatabase();
+ $this->getUtilitiesInstance()->executeExternalDatabaseMigrationCommand();
if ($demodata) {
$database->queryFile("$editionSqlDir/demodata.sql");
@@ -718,4 +732,42 @@ private function canUpdateHtaccess()
$utilities = $this->getUtilitiesInstance();
return $utilities->canHtaccessFileBeUpdated();
}
+
+ /**
+ * @param CommandExecutionFailedException $exception
+ */
+ private function handleCommandExecutionFailedException($exception)
+ {
+ $language = $this->getLanguageInstance();
+ $view = $this->getView();
+
+ $commandOutput = $exception->getCommandOutput();
+ $htmlCommandOutput = $this->convertCommandOutputToHtmlOutput($commandOutput);
+
+ $errorLines[] = sprintf(
+ $language->getText('EXTERNAL_COMMAND_ERROR_1'),
+ $exception->getCommand(),
+ $exception->getReturnCode()
+ );
+ $errorLines[] = $language->getText('EXTERNAL_COMMAND_ERROR_2');
+
+ $errorHeader = implode("<br />", $errorLines);
+ $errorMessage = implode("<br /><br />", [$errorHeader, $htmlCommandOutput]);
+
+ $view->setMessage($errorMessage);
+ }
+
+ /**
+ * @param string $commandOutput
+ * @return string
+ */
+ private function convertCommandOutputToHtmlOutput($commandOutput)
+ {
+ $commandOutput = Utilities::stripAnsiControlCodes($commandOutput);
+ $commandOutput = htmlspecialchars($commandOutput);
+ $commandOutput = str_replace("\n", "<br />", $commandOutput);
+ $commandOutput = "<span style=\"font-family: courier,serif\">$commandOutput</span>";
+
+ return $commandOutput;
+ }
}
@@ -204,4 +204,7 @@
'LOAD_DYN_CONTENT_NOTICE' => '<p>Wenn diese Option gesetzt ist, sehen Sie ein zusätzliches Menü im Administrationsbereich Ihres OXID eShop.</p><p>Über dieses Menü erhalten Sie weitere Informationen über E-Commerce Services, wie z.B. Google Produktsuche oder econda.</p> <p>Sie können diese Einstellung im Administrationsbereich jederzeit wieder ändern.</p>',
'ERROR_SETUP_CANCELLED' => 'Das Setup wurde abgebrochen, weil Sie die Lizenzvereinbarungen nicht akzeptiert haben.',
'BUTTON_START_INSTALL' => 'Setup erneut starten',
+
+'EXTERNAL_COMMAND_ERROR_1' => 'Fehler beim Ausführen des Kommandos \'%s\'. Returncode: \'%d\'.',
+'EXTERNAL_COMMAND_ERROR_2' => 'Das Kommando gibt folgende Meldung zurück:',
);
@@ -203,4 +203,7 @@
'LOAD_DYN_CONTENT_NOTICE' => '<p>If checkbox is set, you will see an additional menu in the admin area of your OXID eShop.</p><p>In that menu you get further information about e-commerce services like Google product search.</p> <p>You can change these settings at any time.</p>',
'ERROR_SETUP_CANCELLED' => 'Setup has been cancelled because you didn\'t accept the license conditions.',
'BUTTON_START_INSTALL' => 'Restart setup',
+
+'EXTERNAL_COMMAND_ERROR_1' => 'Error while executing command \'%s\'. Return code: \'%d\'.',
+'EXTERNAL_COMMAND_ERROR_2' => 'The command returns the following message:',
);
@@ -0,0 +1,102 @@
+<?php
+/**
+ * This file is part of OXID eShop Community Edition.
+ *
+ * OXID eShop Community Edition is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * OXID eShop Community Edition is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with OXID eShop Community Edition. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @link http://www.oxid-esales.com
+ * @copyright (C) OXID eSales AG 2003-2017
+ * @version OXID eShop CE
+ */
+
+namespace OxidEsales\EshopCommunity\Setup\Exception;
+
+/**
+ * Class CommandExecutionFailedException.
+ *
+ * Exception class to indicate absence of template
+ */
+class CommandExecutionFailedException extends \Exception
+{
+ private $command = null;
+
+ private $returnCode = 0;
+
+ private $commandOutput = null;
+
+ /**
+ * CommandExecutionFailedException constructor.
+ *
+ * @param string $message Name of the command which was executed.
+ * @param int $code Exception code.
+ * @param \Exception|null $previous Link to previous exception.
+ */
+ public function __construct($message = '', $code = 0, \Exception $previous = null)
+ {
+ $this->command = $message;
+
+ $message = sprintf("There was an error while executing '%s'.", $message);
+ parent::__construct($message, $code, $previous);
+ }
+
+ /**
+ * Returns the command which was used for execution.
+ *
+ * @return string
+ */
+ public function getCommand()
+ {
+ return $this->command;
+ }
+
+ /**
+ * Sets value for the return code.
+ *
+ * @param int $returnCode
+ */
+ public function setReturnCode($returnCode)
+ {
+ $this->returnCode = $returnCode;
+ }
+
+ /**
+ * Returns value of return code.
+ *
+ * @return int
+ */
+ public function getReturnCode()
+ {
+ return $this->returnCode;
+ }
+
+ /**
+ * Sets value for command output which was shown after the execution of command.
+ *
+ * @param array $outputLines
+ */
+ public function setCommandOutput($outputLines)
+ {
+ $this->commandOutput = $outputLines;
+ }
+
+ /**
+ * Returns the value of command output which was shown after the execution of command.
+ *
+ * @return string
+ */
+ public function getCommandOutput()
+ {
+ return $this->commandOutput ? implode("\n", $this->commandOutput) : null;
+ }
+}
@@ -29,6 +29,7 @@
use OxidEsales\Eshop\Core\Edition\EditionRootPathProvider;
use OxidEsales\Eshop\Core\Edition\EditionPathProvider;
use OxidEsales\Eshop\Core\Edition\EditionSelector;
+use OxidEsales\EshopCommunity\Setup\Exception\CommandExecutionFailedException;
/**
* Setup utilities class
@@ -38,10 +39,16 @@ class Utilities extends Core
const DEMODATA_PACKAGE_NAME = 'oxideshop-demodata-%s';
const DEMODATA_PACKAGE_SOURCE_DIRECTORY = 'src';
+ const COMPOSER_VENDOR_BIN_DIRECTORY = 'bin';
const DEMODATA_SQL_FILENAME = 'demodata.sql';
const LICENSE_TEXT_FILENAME = "lizenz.txt";
+ const ESHOP_FACTS_BINARY_FILENAME = 'oe-eshop-facts';
+ const DATABASE_VIEW_REGENERATION_BINARY_FILENAME = 'oe-eshop-db_views_regenerate';
+ const DATABASE_MIGRATION_BINARY_FILENAME = 'oe-eshop-db_migrate';
+ const DEMODATA_ASSETS_INSTALL_BINARY_FILENAME = 'oe-eshop-demodata_install';
+
/**
* Unable to find file
*
@@ -446,38 +453,68 @@ public function isValidEmail($sEmail)
}
/**
- * Calls views regeneration command
+ * Calls external database views regeneration command.
*/
- public function regenerateViews()
+ public function executeExternalRegenerateViewsCommand()
{
- $vendorDir = $this->getVendorDir();
- exec("{$vendorDir}/bin/oe-eshop-facts oe-eshop-db_views_regenerate");
+ $this->executeShellCommandViaEshopFactsBinary(self::DATABASE_VIEW_REGENERATION_BINARY_FILENAME);
}
/**
- * Calls database migration command
+ * Calls external database migration command.
*/
- public function migrateDatabase()
+ public function executeExternalDatabaseMigrationCommand()
{
- $vendorDir = $this->getVendorDir();
+ $this->executeShellCommandViaEshopFactsBinary(self::DATABASE_MIGRATION_BINARY_FILENAME);
+ }
- exec("{$vendorDir}/bin/oe-eshop-facts oe-eshop-db_migrate");
+ /**
+ * Calls external demodata assets install command.
+ */
+ public function executeExternalDemodataAssetsInstallCommand()
+ {
+ $this->executeShellCommandViaEshopFactsBinary(self::DEMODATA_ASSETS_INSTALL_BINARY_FILENAME);
}
/**
- * Calls demodata assets install command
+ * Executes a given command via the eShop facts helper binary file.
+ *
+ * @param string $command Command to execute.
*/
- public function demodataAssetsInstall()
+ private function executeShellCommandViaEshopFactsBinary($command)
{
- $vendorDir = $this->getVendorDir();
+ $eshopFactsPathToBinary = $this->getFullPathToEshopFacts();
+ $this->executeShellCommand("$eshopFactsPathToBinary $command");
+ }
- exec("{$vendorDir}/bin/oe-eshop-facts oe-eshop-demodata_install");
+ /**
+ * Execute shell command and capture output when return code is non zero.
+ *
+ * @param string $command Command to execute.
+ *
+ * @throws CommandExecutionFailedException When execution returns non zero return code.
+ */
+ private function executeShellCommand($command)
+ {
+ $commandWithStdErrRedirection = $command . " 2>&1";
+
+ exec($commandWithStdErrRedirection, $outputLines, $returnCode);
+
+ if (($returnCode !== 0)) {
+ $exception = new CommandExecutionFailedException($command);
+ $exception->setReturnCode($returnCode);
+ $exception->setCommandOutput($returnCode !== 127 ? $outputLines : ['Impossible to execute the command.']);
+
+ throw $exception;
+ }
}
/**
+ * Return path to composer vendor directory.
+ *
* @return string
*/
- private function getVendorDir()
+ private function getVendorDirectory()
{
/** @var ConfigFile $shopConfig */
$shopConfig = Registry::get("oxConfigFile");
@@ -487,6 +524,26 @@ private function getVendorDir()
}
/**
+ * Return path to composer vendor bin directory.
+ *
+ * @return string
+ */
+ private function getVendorBinaryDirectory()
+ {
+ return implode(DIRECTORY_SEPARATOR, [$this->getVendorDirectory(), self::COMPOSER_VENDOR_BIN_DIRECTORY]);
+ }
+
+ /**
+ * Return full path to eShop facts binary file.
+ *
+ * @return string
+ */
+ private function getFullPathToEshopFacts()
+ {
+ return implode(DIRECTORY_SEPARATOR, [$this->getVendorBinaryDirectory(), self::ESHOP_FACTS_BINARY_FILENAME]);
+ }
+
+ /**
* Check if database is up and running
*
* @param Database $database
@@ -561,7 +618,7 @@ public function getActiveEditionDemodataPackageSqlFilePath()
return implode(
DIRECTORY_SEPARATOR,
[
- $this->getUtilitiesInstance()->getVendorDir(),
+ $this->getUtilitiesInstance()->getVendorDirectory(),
EditionRootPathProvider::EDITIONS_DIRECTORY,
sprintf(self::DEMODATA_PACKAGE_NAME, strtolower($editionSelector->getEdition())),
self::DEMODATA_PACKAGE_SOURCE_DIRECTORY,
@@ -589,4 +646,15 @@ public function getLicenseContent($languageId)
return $licenseContent;
}
+
+ /**
+ * Removes any ANSI control codes from command output.
+ *
+ * @param string $outputWithAnsiControlCodes
+ * @return string
+ */
+ public static function stripAnsiControlCodes($outputWithAnsiControlCodes)
+ {
+ return preg_replace('/\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]/', "", $outputWithAnsiControlCodes);
+ }
}
Oops, something went wrong.

0 comments on commit 4e7d854

Please sign in to comment.