Page Menu
Home
In-Portal Phabricator
Search
Configure Global Search
Log In
Files
F800178
in-portal
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sat, Feb 22, 12:03 AM
Size
19 KB
Mime Type
text/x-diff
Expires
Mon, Feb 24, 12:03 AM (7 h, 42 m)
Engine
blob
Format
Raw Data
Handle
573440
Attached To
rINP In-Portal
in-portal
View Options
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
Log In to Comment