Page MenuHomeIn-Portal Phabricator

in-portal
No OneTemporary

File Metadata

Created
Sat, Feb 22, 12:03 AM

in-portal

Index: branches/5.3.x/core/units/helpers/deployment_helper.php
===================================================================
--- branches/5.3.x/core/units/helpers/deployment_helper.php (revision 16093)
+++ branches/5.3.x/core/units/helpers/deployment_helper.php (revision 16094)
@@ -1,722 +1,753 @@
<?php
/**
* @version $Id$
* @package In-Portal
* @copyright Copyright (C) 1997 - 2011 Intechnic. All rights reserved.
* @license GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/
defined('FULL_PATH') or die('restricted access!');
class DeploymentHelper extends kHelper {
/**
* How many symbols from sql should be shown.
*/
const SQL_TRIM_LENGTH = 120;
+ const STAGE_DB_MIGRATE = 'db-migrate';
+
+ const STAGE_CACHE_RESET = 'cache-reset';
+
/**
* Name of module, that is processed right now.
*
* @var string
*/
private $moduleName = '';
/**
* List of sqls, associated with each revision (from project_upgrades.sql file).
*
* @var array
*/
private $revisionSqls = array();
/**
* List of revision titles as user typed them (from project_upgrades.sql file).
*
* @var array
*/
private $revisionTitles = array();
/**
* Revision dependencies.
*
* @var array
*/
private $revisionDependencies = array();
/**
* Numbers of revisions, that were already applied.
*
* @var array
*/
private $appliedRevisions = array();
/**
* Don't change database, but only check syntax of project_upgrades.sql file and mark all revisions discovered as applied.
*
* @var boolean
*/
private $dryRun = false;
/**
* Remembers script invocation method.
*
* @var boolean
*/
public $isCommandLine = false;
/**
* IP Address of script invoker.
*
* @var string
*/
public $ip = '';
/**
+ * Deployment stages to run.
+ *
+ * @var array
+ */
+ public $stages = array(
+ self::STAGE_DB_MIGRATE,
+ self::STAGE_CACHE_RESET,
+ );
+
+ /**
* Event, that triggered deployment.
*
* @var kEvent
*/
private $_event;
/**
* Field values for log record.
*
* @var string
*/
private $logData = array();
/**
* Creates class instance.
*/
public function __construct()
{
parent::__construct();
kUtil::setResourceLimit();
$this->_event = new kEvent('adm:OnDummy');
$this->isCommandLine = isset($GLOBALS['argv']) && count($GLOBALS['argv']);
if ( !$this->isCommandLine ) {
$this->ip = $this->Application->getClientIp();
}
- elseif ( isset($GLOBALS['argv'][3]) ) {
- $this->ip = $GLOBALS['argv'][3];
+ else {
+ if ( isset($GLOBALS['argv'][3]) ) {
+ $this->ip = $GLOBALS['argv'][3];
+ }
+
+ if ( isset($GLOBALS['argv'][4]) ) {
+ $new_stages = explode(',', $GLOBALS['argv'][4]);
+ $unknown_stages = array_diff($new_stages, $this->stages);
+
+ if ( $unknown_stages ) {
+ throw new InvalidArgumentException('Unknown deployment stages: ' . implode(', ', $unknown_stages));
+ }
+
+ $this->stages = $new_stages;
+ }
}
}
/**
* Sets event, associated with deployment.
*
* @param kEvent $event Event.
*
* @return void
*/
public function setEvent(kEvent $event)
{
$this->_event = $event;
}
/**
* Adds message to script execution log.
*
* @param string $message Message.
* @param boolean $new_line Jump to next line.
*
* @return string
*/
private function toLog($message, $new_line = true)
{
if ( $new_line ) {
$message .= PHP_EOL;
}
$this->logData['Output'] .= $message;
return $message;
}
/**
* Loads already applied revisions list of current module.
*
* @return self
*/
private function loadAppliedRevisions()
{
$sql = 'SELECT RevisionNumber
FROM ' . TABLE_PREFIX . 'ModuleDeploymentLog
WHERE Module = ' . $this->Conn->qstr($this->moduleName);
$this->appliedRevisions = array_flip($this->Conn->GetCol($sql));
return $this;
}
/**
* Deploys changes from all installed modules.
*
* @param boolean $dry_run Use dry run mode?
*
* @return boolean
*/
public function deployAll($dry_run = false)
{
if ( !$this->isCommandLine ) {
echo '<pre style="font-size: 10pt; color: #BBB; background-color: black; border: 2px solid darkgreen; padding: 8px;">' . PHP_EOL;
}
$ret = true;
$this->dryRun = $dry_run;
- foreach ( $this->Application->ModuleInfo as $module_name => $module_info ) {
- $this->moduleName = $module_name;
+ if ( in_array(self::STAGE_DB_MIGRATE, $this->stages) ) {
+ foreach ( $this->Application->ModuleInfo as $module_name => $module_info ) {
+ $this->moduleName = $module_name;
- if ( !file_exists($this->getModuleFile('project_upgrades.sql')) ) {
- continue;
- }
+ if ( !file_exists($this->getModuleFile('project_upgrades.sql')) ) {
+ continue;
+ }
- $ret = $ret && $this->deploy($module_name);
+ $ret = $ret && $this->deploy($module_name);
+ }
}
- if ( $ret && !$this->dryRun ) {
- $this->resetCaches();
- $this->refreshThemes();
+ if ( in_array(self::STAGE_CACHE_RESET, $this->stages) ) {
+ if ( $ret && !$this->dryRun ) {
+ $this->resetCaches();
+ $this->refreshThemes();
+ }
}
if ( !$this->isCommandLine ) {
echo kUtil::escape($this->_runShellScript());
echo '</pre>' . PHP_EOL;
}
return $ret;
}
/**
* Runs user-specific shell script when deployment happens from Web.
*
* @return string
*/
protected function _runShellScript()
{
if ( !$this->Application->isDebugMode(false) ) {
return '';
}
$wrapper_script = '/usr/local/bin/guest2host_server.sh';
$script_name = FULL_PATH . '/tools/' . ($this->dryRun ? 'synchronize.sh' : 'deploy.sh');
if ( file_exists($wrapper_script) && file_exists($script_name) ) {
$script_name = preg_replace('/^.*\/web/', constant('DBG_LOCAL_BASE_PATH'), $script_name);
return shell_exec($wrapper_script . ' ' . $script_name . ' 2>&1');
}
return '';
}
/**
* Deploys pending changes to a site.
*
* @param string $module_name Module name.
*
* @return boolean
*/
private function deploy($module_name)
{
echo $this->colorText('Deploying Module "' . $module_name . '":', 'cyan', true) . PHP_EOL;
if ( !$this->upgradeDatabase() ) {
return false;
}
if ( $this->dryRun ) {
$this->exportLanguagePack();
}
else {
$this->importLanguagePack();
}
echo $this->colorText('Done with Module "' . $module_name . '".', 'green', true) . PHP_EOL . PHP_EOL;
return true;
}
/**
* Import latest language pack (without overwrite).
*
* @return self
*/
private function importLanguagePack()
{
$language_import_helper = $this->Application->recallObject('LanguageImportHelper');
/* @var $language_import_helper LanguageImportHelper */
$this->out('Importing LanguagePack ... ');
$filename = $this->getModuleFile('english.lang');
$language_import_helper->performImport($filename, '|0|1|2|', $this->moduleName);
$this->displayStatus('OK');
return $this;
}
/**
* Exports latest language pack.
*
* @return self
*/
private function exportLanguagePack()
{
static $languages = null;
if ( !isset($languages) ) {
$sql = 'SELECT LanguageId
FROM ' . $this->Application->getUnitConfig('lang')->getTableName() . '
WHERE Enabled = 1';
$languages = $this->Conn->GetCol($sql);
}
$language_import_helper = $this->Application->recallObject('LanguageImportHelper');
/* @var $language_import_helper LanguageImportHelper */
$language_import_helper->performExport(EXPORT_PATH . '/' . $this->moduleName . '.lang', '|0|1|2|', $languages, '|' . $this->moduleName . '|');
return $this;
}
/**
* Resets unit and section cache.
*
* @return self
*/
private function resetCaches()
{
// 2. reset unit config cache (so new classes get auto-registered)
$this->out('Resetting Configs Files Cache and Parsed System Data ... ');
$this->_event->CallSubEvent('OnResetConfigsCache');
$this->displayStatus('OK');
// 3. reset sections cache
$this->out('Resetting Admin Console Sections ... ');
$this->_event->CallSubEvent('OnResetSections');
$this->displayStatus('OK');
// 4. reset mod-rewrite cache
$this->out('Resetting ModRewrite Cache ... ');
$this->_event->CallSubEvent('OnResetModRwCache');
$this->displayStatus('OK');
return $this;
}
/**
* Rebuild theme files.
*
* @return self
*/
private function refreshThemes()
{
$this->out('Refreshing Theme Files ... ');
$this->_event->CallSubEvent('OnRebuildThemes');
$this->displayStatus('OK');
return $this;
}
/**
* Runs database upgrade script.
*
* @return boolean
*/
private function upgradeDatabase()
{
$this->loadAppliedRevisions();
$this->Conn->setErrorHandler(array(&$this, 'handleSqlError'));
$this->out('Verifying Database Revisions ... ');
if ( !$this->collectDatabaseRevisions() || !$this->checkRevisionDependencies() ) {
return false;
}
$this->displayStatus('OK');
return $this->applyRevisions();
}
/**
* Collects database revisions from "project_upgrades.sql" file.
*
* @return boolean
*/
private function collectDatabaseRevisions()
{
$filename = $this->getModuleFile('project_upgrades.sql');
if ( !file_exists($filename) ) {
return true;
}
$sqls = file_get_contents($filename);
preg_match_all("/# r([\d]+)([^\:]*):(.*?)(\n|$)/s", $sqls, $matches, PREG_SET_ORDER + PREG_OFFSET_CAPTURE);
if ( !$matches ) {
$this->displayStatus('FAILED' . PHP_EOL . 'No Database Revisions Found');
return false;
}
foreach ( $matches as $index => $match ) {
$revision = $match[1][0];
if ( $this->revisionApplied($revision) ) {
// skip applied revisions
continue;
}
if ( isset($this->revisionSqls[$revision]) ) {
// duplicate revision among non-applied ones
$this->displayStatus('FAILED' . PHP_EOL . 'Duplicate revision #' . $revision . ' found');
return false;
}
// get revision sqls
$start_pos = $match[0][1] + strlen($match[0][0]);
$end_pos = isset($matches[$index + 1]) ? $matches[$index + 1][0][1] : strlen($sqls);
$revision_sqls = substr($sqls, $start_pos, $end_pos - $start_pos);
if ( !$revision_sqls ) {
// revision without sqls
continue;
}
$this->revisionTitles[$revision] = trim($match[3][0]);
$this->revisionSqls[$revision] = $revision_sqls;
$revision_dependencies = $this->parseRevisionDependencies($match[2][0]);
if ( $revision_dependencies ) {
$this->revisionDependencies[$revision] = $revision_dependencies;
}
}
ksort($this->revisionSqls);
ksort($this->revisionDependencies);
return true;
}
/**
* Checks that all dependent revisions are either present now OR were applied before.
*
* @return boolean
*/
private function checkRevisionDependencies()
{
foreach ( $this->revisionDependencies as $revision => $revision_dependencies ) {
foreach ( $revision_dependencies as $revision_dependency ) {
if ( $this->revisionApplied($revision_dependency) ) {
// revision dependent upon already applied -> dependency fulfilled
continue;
}
if ( $revision_dependency >= $revision ) {
$this->displayStatus('FAILED' . PHP_EOL . 'Revision #' . $revision . ' has incorrect dependency to revision #' . $revision_dependency . '. Only dependencies to older revisions are allowed!');
return false;
}
if ( !isset($this->revisionSqls[$revision_dependency]) ) {
$this->displayStatus('FAILED' . PHP_EOL . 'Revision #' . $revision . ' depends on missing revision #' . $revision_dependency . '!');
return false;
}
}
}
return true;
}
/**
* Runs all pending sqls.
*
* @return boolean
*/
private function applyRevisions()
{
if ( !$this->revisionSqls ) {
return true;
}
if ( $this->dryRun ) {
foreach ( $this->revisionSqls as $revision => $sqls ) {
$this->initLog($revision, ModuleDeploymentLog::MODE_MANUAL);
echo PHP_EOL . $this->colorText($this->revisionTitles[$revision], 'gray', true) . PHP_EOL; // 'Processing DB Revision: #' . $revision . ' ... ';
echo $this->toLog($this->colorText('SKIPPING', 'purple'));
$this->saveLog(ModuleDeploymentLog::STATUS_SKIPPED);
}
return true;
}
$this->out('Upgrading Database ... ', true);
foreach ( $this->revisionSqls as $revision => $sqls ) {
echo PHP_EOL . $this->colorText($this->revisionTitles[$revision], 'gray', true) . PHP_EOL; // 'Processing DB Revision: #' . $revision . ' ... ';
$sqls = str_replace("\r\n", "\n", $sqls); // convert to linux line endings
$no_comment_sqls = preg_replace("/#\s([^;]*?)\n/is", "# \\1;\n", $sqls); // add ";" to each comment end to ensure correct split
$sqls = explode(";\n", $no_comment_sqls . "\n"); // ensures that last sql won't have ";" in it
$sqls = array_map('trim', $sqls);
$this->initLog($revision);
foreach ( $sqls as $sql ) {
if ( substr($sql, 0, 1) == '#' ) {
// output comment as is
echo $this->toLog($this->colorText($sql, 'purple'));
continue;
}
elseif ( $sql ) {
echo $this->toLog($this->shortenQuery($sql), false);
$this->Conn->Query($sql);
if ( $this->Conn->hasError() ) {
// consider revisions with errors applied
$this->saveLog(ModuleDeploymentLog::STATUS_ERROR);
return false;
}
else {
$this->displayStatus('OK (' . $this->Conn->getAffectedRows() . ')', true, true);
}
}
}
$this->saveLog(ModuleDeploymentLog::STATUS_SUCCESS);
}
echo PHP_EOL;
return true;
}
/**
* Returns shortened version of SQL query.
*
* @param string $sql SQL query.
*
* @return string
*/
protected function shortenQuery($sql)
{
$escaped_sql = $this->isCommandLine ? $sql : kUtil::escape($sql);
$single_line_sql = preg_replace('/(\n|\t| )+/is', ' ', $escaped_sql);
return mb_substr(trim($single_line_sql), 0, self::SQL_TRIM_LENGTH) . ' ... ';
}
/**
* Initializes log record for a revision.
*
* @param integer $revision Revision.
* @param integer $mode Mode.
*
* @return self
*/
protected function initLog($revision, $mode = ModuleDeploymentLog::MODE_AUTOMATIC)
{
$this->logData = array(
'Module' => $this->moduleName,
'RevisionNumber' => $revision,
'RevisionTitle' => $this->revisionTitles[$revision],
'IPAddress' => $this->ip,
'Output' => '',
'Mode' => $mode,
'Status' => ModuleDeploymentLog::STATUS_SUCCESS,
);
return $this;
}
/**
* Creates log record.
*
* @param integer $status Status.
*
* @return self
*/
private function saveLog($status)
{
$this->logData['Status'] = $status;
$log = $this->Application->recallObject('module-deployment-log', null, array('skip_autoload' => true));
/* @var $log kDBItem */
$log->Clear();
$log->SetFieldsFromHash($this->logData);
$log->Create();
return $this;
}
/**
* Error handler for sql errors.
*
* @param int $code Error code.
* @param string $msg Error message.
* @param string $sql SQL query, that raised an error.
*
* @return boolean
*/
public function handleSqlError($code, $msg, $sql)
{
$this->displayStatus('FAILED', true, true);
$error_msg = 'SQL Error #' . $code . ': ' . $msg;
$this->logData['ErrorMessage'] = $error_msg;
$this->displayStatus($error_msg);
$this->out('Please execute rest of SQLs in this Revision by hand and run deployment script again.', true);
return true;
}
/**
* Checks if given revision was already applied.
*
* @param int $revision Revision.
*
* @return boolean
*/
private function revisionApplied($revision)
{
return isset($this->appliedRevisions[$revision]);
}
/**
* Returns path to given file in current module install folder.
*
* @param string $filename Filename.
*
* @return string
*/
private function getModuleFile($filename)
{
$module_folder = $this->Application->findModule('Name', $this->moduleName, 'Path');
return FULL_PATH . DIRECTORY_SEPARATOR . $module_folder . 'install/' . $filename;
}
/**
* Extracts revisions from string in format "(1,3,5464,23342,3243)".
*
* @param string $string Comma-separated revision list.
*
* @return array
*/
private function parseRevisionDependencies($string)
{
if ( !$string ) {
return array();
}
$string = explode(',', substr($string, 1, -1));
return array_map('trim', $string);
}
/**
* Applies requested color and bold attributes to given text string.
*
* @param string $text Text.
* @param string $color Color.
* @param boolean $bold Bold flag.
*
* @return string
*/
private function colorText($text, $color, $bold = false)
{
if ( $this->isCommandLine ) {
$color_map = array(
'black' => 30, // dark gray (in bold)
'blue' => 34, // light blue (in bold)
'green' => 32, // light green (in bold)
'cyan' => 36, // light cyan (in bold)
'red' => 31, // light red (in bold)
'purple' => 35, // light purple (in bold)
'brown' => 33, // yellow (in bold)
'gray' => 37, // white (in bold)
);
return "\033[" . ($bold ? 1 : 0) . ";" . $color_map[$color] . "m" . $text . "\033[0m";
}
$html_color_map = array(
'black' => array('normal' => '#000000', 'bold' => '#666666'),
'blue' => array('normal' => '#00009C', 'bold' => '#3C3CFF'),
'green' => array('normal' => '#009000', 'bold' => '#00FF00'),
'cyan' => array('normal' => '#009C9C', 'bold' => '#00FFFF'),
'red' => array('normal' => '#9C0000', 'bold' => '#FF0000'),
'purple' => array('normal' => '#900090', 'bold' => '#F99CF9'),
'brown' => array('normal' => '#C9C909', 'bold' => '#FFFF00'),
'gray' => array('normal' => '#909090', 'bold' => '#FFFFFF'),
);
$html_color = $html_color_map[$color][$bold ? 'bold' : 'normal'];
return '<span style="color: ' . $html_color . '">' . kUtil::escape($text, kUtil::ESCAPE_HTML) . '</span>';
}
/**
* Displays last command execution status.
*
* @param string $status_text Status text.
* @param boolean $new_line Jump to next line.
* @param boolean $to_log Also write to log.
*
* @return self
*/
private function displayStatus($status_text, $new_line = true, $to_log = false)
{
$color = substr($status_text, 0, 2) == 'OK' ? 'green' : 'red';
$ret = $this->colorText($status_text, $color, false);
if ( $to_log ) {
echo $this->toLog($ret, $new_line);
}
else {
echo $ret . ($new_line ? PHP_EOL : '');
}
return $this;
}
/**
* Outputs a text and escapes it if necessary.
*
* @param string $text Text.
* @param boolean $new_line Jump to next line.
*
* @return self
*/
private function out($text, $new_line = false)
{
if ( !$this->isCommandLine ) {
$text = kUtil::escape($text);
}
echo $text . ($new_line ? PHP_EOL : '');
return $this;
}
-}
\ No newline at end of file
+}

Event Timeline