diff --git a/application/utils.inc.php b/application/utils.inc.php index bdd7fce90..a91996b3f 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -46,6 +46,7 @@ class utils // Parameters loaded from a file, parameters of the page/command line still have precedence private static $m_aParamsFromFile = null; + private static $m_aParamSource = array(); protected static function LoadParamFile($sParamFile) { @@ -82,6 +83,7 @@ class utils $sParam = $aMatches[1]; $value = trim($aMatches[2]); self::$m_aParamsFromFile[$sParam] = $value; + self::$m_aParamSource[$sParam] = $sParamFile; } } } @@ -99,6 +101,25 @@ class utils } } + /** + * Return the source file from which the parameter has been found, + * usefull when it comes to pass user credential to a process executed + * in the background + * @param $sName Parameter name + * @return The file name if any, or null + */ + public static function GetParamSourceFile($sName) + { + if (array_key_exists($sName, self::$m_aParamSource)) + { + return self::$m_aParamSource[$sName]; + } + else + { + return null; + } + } + public static function IsModeCLI() { $sSAPIName = php_sapi_name(); @@ -152,10 +173,18 @@ class utils public static function Sanitize($value, $defaultValue, $sSanitizationFilter) { - $retValue = self::Sanitize_Internal($value, $sSanitizationFilter); - if ($retValue === false) + if ($value === $defaultValue) { - $retValue = $defaultValue; + // Preserve the real default value (can be used to detect missing mandatory parameters) + $retValue = $value; + } + else + { + $retValue = self::Sanitize_Internal($value, $sSanitizationFilter); + if ($retValue === false) + { + $retValue = $defaultValue; + } } return $retValue; } @@ -601,5 +630,86 @@ class utils } echo "
".print_r($aLightTrace, true)."\n"; } + + /** + * Execute the given iTop PHP script, passing it the current credentials + * Only CLI mode is supported, because of the need to hand the credentials over to the next process + * Throws an exception if the execution fails or could not be attempted (config issue) + * @param string $sScript Name and relative path to the file (relative to the iTop root dir) + * @param hash $aArguments Associative array of 'arg' => 'value' + * @return array(iCode, array(output lines)) + */ + /** + */ + static function ExecITopScript($sScriptName, $aArguments) + { + $aDisabled = explode(', ', ini_get('disable_functions')); + if (in_array('exec', $aDisabled)) + { + throw new Exception("The PHP exec() function has been disabled on this server"); + } + + $sPHPExec = trim(MetaModel::GetConfig()->Get('php_path')); + if (strlen($sPHPExec) == 0) + { + throw new Exception("The path to php must not be empty. Please set a value for 'php_path' in your configuration file."); + } + + $sAuthUser = self::ReadParam('auth_user', '', 'raw_data'); + $sAuthPwd = self::ReadParam('auth_pwd', '', 'raw_data'); + $sParamFile = self::GetParamSourceFile('auth_user'); + if (is_null($sParamFile)) + { + $aArguments['auth_user'] = $sAuthUser; + $aArguments['auth_pwd'] = $sAuthPwd; + } + else + { + $aArguments['param_file'] = $sParamFile; + } + + $aArgs = array(); + foreach($aArguments as $sName => $value) + { + // Note: See comment from the 23-Apr-2004 03:30 in the PHP documentation + // It suggests to rely on pctnl_* function instead of using escapeshellargs + $aArgs[] = "--$sName=".escapeshellarg($value); + } + $sArgs = implode(' ', $aArgs); + + $sScript = realpath(APPROOT.$sScriptName); + if (!file_exists($sScript)) + { + throw new Exception("Could not find the script file '$sScriptName' from the directory '".APPROOT."'"); + } + + $sCommand = '"'.$sPHPExec.'" '.escapeshellarg($sScript).' -- '.$sArgs; + + if (version_compare(phpversion(), '5.3.0', '<')) + { + if (substr(PHP_OS,0,3) == 'WIN') + { + // Under Windows, and for PHP 5.2.x, the whole command has to be quoted + // Cf PHP doc: http://php.net/manual/fr/function.exec.php, comment from the 27-Dec-2010 + $sCommand = '"'.$sCommand.'"'; + } + } + + $sLastLine = exec($sCommand, $aOutput, $iRes); + if ($iRes == 1) + { + throw new Exception(Dict::S('Core:ExecProcess:Code1')." - ".$sCommand); + } + elseif ($iRes == 255) + { + $sErrors = implode("\n", $aOutput); + throw new Exception(Dict::S('Core:ExecProcess:Code255')." - ".$sCommand.":\n".$sErrors); + } + + //$aOutput[] = $sCommand; + return array($iRes, $aOutput); + } + + } ?> diff --git a/core/config.class.inc.php b/core/config.class.inc.php index c3731ab71..fda3f9c98 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -124,6 +124,14 @@ class Config 'source_of_value' => '', 'show_in_conf_sample' => false, ), + 'php_path' => array( + 'type' => 'string', + 'description' => 'Path to the php executable in CLI mode', + 'default' => 'php', + 'value' => 'php', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), 'session_name' => array( 'type' => 'string', 'description' => 'The name of the cookie used to store the PHP session id', diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 9d144d860..961ab96d5 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -932,7 +932,7 @@ abstract class DBObject $oDeletionPlan->AddToDelete($oReplica, DEL_SILENT); - if ($oDataSource->GetKey() == SynchroDataSource::GetCurrentTaskId()) + if ($oDataSource->GetKey() == SynchroExecution::GetCurrentTaskId()) { // The current task has the right to delete the object continue; @@ -1862,7 +1862,7 @@ abstract class DBObject $oSet = $this->GetMasterReplica(); while($aData = $oSet->FetchAssoc()) { - if ($aData['datasource']->GetKey() == SynchroDataSource::GetCurrentTaskId()) + if ($aData['datasource']->GetKey() == SynchroExecution::GetCurrentTaskId()) { // Ignore the current task (check to write => ok) continue; diff --git a/core/kpi.class.inc.php b/core/kpi.class.inc.php index a9c129941..eed8e380f 100644 --- a/core/kpi.class.inc.php +++ b/core/kpi.class.inc.php @@ -191,6 +191,16 @@ class ExecutionKPI return $output[1] * 1024; } } + + static public function memory_get_peak_usage($bRealUsage = false) + { + if (function_exists('memory_get_peak_usage')) + { + return memory_get_peak_usage($bRealUsage); + } + // PHP > 5.2.1 - this verb depends on a compilation option + return 0; + } } class ApplicationStartupKPI extends ExecutionKPI diff --git a/core/metamodel.class.php b/core/metamodel.class.php index ae40074b5..bca844a07 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -1999,7 +1999,10 @@ if (!array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass])) $aOrderSpec = array(); foreach ($aOrderBy as $sFieldAlias => $bAscending) { - MyHelpers::CheckValueInArray('field name in ORDER BY spec', $sFieldAlias, self::GetAttributesList($oFilter->GetFirstJoinedClass())); + if ($sFieldAlias != 'id') + { + MyHelpers::CheckValueInArray('field name in ORDER BY spec', $sFieldAlias, self::GetAttributesList($oFilter->GetFirstJoinedClass())); + } if (!is_bool($bAscending)) { throw new CoreException("Wrong direction in ORDER BY spec, found '$bAscending' and expecting a boolean value"); diff --git a/dictionaries/de.dictionary.itop.core.php b/dictionaries/de.dictionary.itop.core.php index bbf4fbada..4b6f86b1e 100644 --- a/dictionaries/de.dictionary.itop.core.php +++ b/dictionaries/de.dictionary.itop.core.php @@ -537,7 +537,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', array( 'Core:Synchro:History' => 'Verlauf der Synchronisation', 'Core:Synchro:NeverRun' => 'Synchronisation noch nicht erfolgt. Kein Protokoll verfügbar.', 'Core:Synchro:SynchroEndedOn_Date' => 'Die letzte Synchronisation endete um %1$s.', - 'Core:Synchro:SynchroRunningStartedOn_Date' => 'Die Synchronisation, die um $1$s gestartet wurde, läuft noch ...', + 'Core:Synchro:SynchroRunningStartedOn_Date' => 'Die Synchronisation, die um %1$s gestartet wurde, läuft noch ...', 'Menu:DataSources' => 'Datenquellen für die Synchronisation', 'Menu:DataSources+' => 'Alle Datenquellen für die Synchronisation', 'Core:Synchro:label_repl_ignored' => 'Ignoriert (%1$s)', diff --git a/dictionaries/dictionary.itop.core.php b/dictionaries/dictionary.itop.core.php index adc9d23b9..160eacdc6 100644 --- a/dictionaries/dictionary.itop.core.php +++ b/dictionaries/dictionary.itop.core.php @@ -587,7 +587,7 @@ Dict::Add('EN US', 'English', 'English', array( 'Core:Synchro:History' => 'Synchronization History', 'Core:Synchro:NeverRun' => 'This synchro was never run. No log yet.', 'Core:Synchro:SynchroEndedOn_Date' => 'The latest synchronization ended on %1$s.', - 'Core:Synchro:SynchroRunningStartedOn_Date' => 'The synchronization started on $1$s is still running...', + 'Core:Synchro:SynchroRunningStartedOn_Date' => 'The synchronization started on %1$s is still running...', 'Menu:DataSources' => 'Synchronization Data Sources', 'Menu:DataSources+' => 'All Synchronization Data Sources', 'Core:Synchro:label_repl_ignored' => 'Ignored (%1$s)', @@ -639,6 +639,7 @@ Dict::Add('EN US', 'English', 'English', array( 'Core:SyncDataSourceObsolete' => 'The data source is marked as obsolete. Operation cancelled.', 'Core:SyncDataSourceAccessRestriction' => 'Only adminstrators or the user specified in the data source can execute this operation. Operation cancelled.', 'Core:SyncTooManyMissingReplicas' => 'All records have been untouched for some time (all of the objects could be deleted). Please check that the process that writes into the synchronization table is still running. Operation cancelled.', + 'Core:SyncSplitModeCLIOnly' => 'The synchronization can be executed in chunks only if run in mode CLI', 'Core:Synchro:ListReplicas_AllReplicas_Errors_Warnings' => '%1$s replicas, %2$s error(s), %3$s warning(s).', 'Core:SynchroReplica:TargetObject' => 'Synchronized Object: %1$s', 'Class:AsyncSendEmail' => 'Email (asynchronous)', @@ -733,6 +734,8 @@ Dict::Add('EN US', 'English', 'English', array( 'Class:appUserPreferences' => 'User Preferences', 'Class:appUserPreferences/Attribute:userid' => 'User', 'Class:appUserPreferences/Attribute:preferences' => 'Prefs', + 'Core:ExecProcess:Code1' => 'Wrong command or command finished with errors (e.g. wrong script name)', + 'Core:ExecProcess:Code255' => 'PHP Error (parsing, or runtime)', )); // diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index 3569a38d0..02e8e1668 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -556,7 +556,7 @@ Opérateurs :
The synchronization has been executed, $iErrors errors have been encountered. Click here to see the records being currently in error.
".$sStatistics); - } - else - { - //$this->SendNotification('success', 'The synchronization has been successfully executed.
'); - } - } - catch (SynchroExceptionNotStarted $e) - { - // Set information for reporting... but delete the object in DB - $oStatLog->Set('end_date', time()); - $oStatLog->Set('status', 'error'); - $oStatLog->Set('last_error', $e->getMessage()); - $oStatLog->DBDeleteTracked($oMyChange); - $this->SendNotification('fatal error', 'The synchronization could not start: \''.$e->getMessage().'\'
Please check its configuration
'); - } - catch (Exception $e) - { - $oStatLog->Set('end_date', time()); - $oStatLog->Set('status', 'error'); - $oStatLog->Set('last_error', $e->getMessage()); - $oStatLog->DBUpdateTracked($oMyChange); - $this->SendNotification('exception', 'The synchronization has been interrupted: \''.$e->getMessage().'\'
Please contact the application support team
'); - } - self::$m_oCurrentTask = null; - - return $oStatLog; - } - - protected function DoSynchronize($oLastFullLoadStartDate, $oMyChange, &$oStatLog) - { - if ($this->Get('status') == 'obsolete') - { - throw new SynchroExceptionNotStarted(Dict::S('Core:SyncDataSourceObsolete')); - } - if (!UserRights::IsAdministrator() && $this->Get('user_id') != UserRights::GetUserId()) - { - throw new SynchroExceptionNotStarted(Dict::S('Core:SyncDataSourceAccessRestriction')); - } - - // Get the list of SQL columns - $sClass = $this->GetTargetClass(); - $aAttCodesExpected = array(); - $aAttCodesToReconcile = array(); - $aAttCodesToUpdate = array(); - $sSelectAtt = "SELECT SynchroAttribute WHERE sync_source_id = :source_id AND (update = 1 OR reconcile = 1)"; - $oSetAtt = new DBObjectSet(DBObjectSearch::FromOQL($sSelectAtt), array() /* order by*/, array('source_id' => $this->GetKey()) /* aArgs */); - while ($oSyncAtt = $oSetAtt->Fetch()) - { - if ($oSyncAtt->Get('update')) - { - $aAttCodesToUpdate[$oSyncAtt->Get('attcode')] = $oSyncAtt; - } - if ($oSyncAtt->Get('reconcile')) - { - $aAttCodesToReconcile[$oSyncAtt->Get('attcode')] = $oSyncAtt; - } - $aAttCodesExpected[$oSyncAtt->Get('attcode')] = $oSyncAtt; - } - $aColumns = $this->GetSQLColumns(array_keys($aAttCodesExpected)); - $aExtDataFields = array_keys($aColumns); - $aExtDataFields[] = 'primary_key'; - $aExtDataSpec = array( - 'table' => $this->GetDataTable(), - 'join_key' => 'id', - 'fields' => $aExtDataFields - ); - - // Get the list of attributes, determine reconciliation keys and update targets - // - if ($this->Get('reconciliation_policy') == 'use_attributes') - { - $aReconciliationKeys = $aAttCodesToReconcile; - } - elseif ($this->Get('reconciliation_policy') == 'use_primary_key') - { - // Override the settings made at the attribute level ! - $aReconciliationKeys = array("primary_key" => null); - } - - $oStatLog->AddTrace("Update of: {".implode(', ', array_keys($aAttCodesToUpdate))."}"); - $oStatLog->AddTrace("Reconciliation on: {".implode(', ', array_keys($aReconciliationKeys))."}"); - - if (count($aAttCodesToUpdate) == 0) - { - $oStatLog->AddTrace("No attribute to update"); - throw new SynchroExceptionNotStarted('There is no attribute to update'); - } - if (count($aReconciliationKeys) == 0) - { - $oStatLog->AddTrace("No attribute for reconciliation"); - throw new SynchroExceptionNotStarted('No attribute for reconciliation'); - } - - $aAttributes = array(); - foreach($aAttCodesToUpdate as $sAttCode => $oSyncAtt) - { - $oAttDef = MetaModel::GetAttributeDef($this->GetTargetClass(), $sAttCode); - if ($oAttDef->IsWritable()) - { - $aAttributes[$sAttCode] = $oSyncAtt; - } - } - - // Count the replicas - $sSelectAll = "SELECT SynchroReplica WHERE sync_source_id = :source_id"; - $oSetAll = new DBObjectSet(DBObjectSearch::FromOQL($sSelectAll), array() /* order by*/, array('source_id' => $this->GetKey())); - $iCountAllReplicas = $oSetAll->Count(); - $oStatLog->Set('stats_nb_replica_total', $iCountAllReplicas); - - // Get all the replicas that were not seen in the last import and mark them as obsolete - if ($oLastFullLoadStartDate == null) - { - // No previous import known, use the full_load_periodicity value... and the current date - $oLastFullLoadStartDate = new DateTime(); // Now - $iLoadPeriodicity = $this->Get('full_load_periodicity'); // Duration in seconds - if ($iLoadPeriodicity > 0) - { - $sInterval = "-$iLoadPeriodicity seconds"; - $oLastFullLoadStartDate->Modify($sInterval); - } - else - { - $oLastFullLoadStartDate = new DateTime('1970-01-01'); - } - } - $sLimitDate = $oLastFullLoadStartDate->Format('Y-m-d H:i:s'); - $oStatLog->AddTrace("Limit Date: $sLimitDate"); - - $sDeletePolicy = $this->Get('delete_policy'); - - if ($sDeletePolicy != 'ignore') - { - $sSelectToObsolete = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('new', 'synchronized', 'modified', 'orphan') AND status_last_seen < :last_import"; - $oSetToObsolete = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToObsolete), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sLimitDate)); - if (($iCountAllReplicas > 10) && ($iCountAllReplicas == $oSetToObsolete->Count())) - { - throw new SynchroExceptionNotStarted(Dict::S('Core:SyncTooManyMissingReplicas')); - } - while($oReplica = $oSetToObsolete->Fetch()) - { - switch ($sDeletePolicy) - { - case 'update': - case 'update_then_delete': - $oStatLog->AddTrace("Destination object to be updated", $oReplica); - $aToUpdate = array(); - $aToUpdateSpec = explode(';', $this->Get('delete_policy_update')); //ex: 'status:obsolete;description:stopped', - foreach($aToUpdateSpec as $sUpdateSpec) - { - $aUpdateSpec = explode(':', $sUpdateSpec); - if (count($aUpdateSpec) == 2) - { - $sAttCode = $aUpdateSpec[0]; - $sValue = $aUpdateSpec[1]; - $aToUpdate[$sAttCode] = $sValue; - } - } - $oReplica->Set('status_last_error', ''); - $oReplica->UpdateDestObject($aToUpdate, $oMyChange, $oStatLog); - if ($oReplica->Get('status_last_error') == '') - { - // Change the status of the replica IIF - $oReplica->Set('status', 'obsolete'); - } - $oReplica->DBUpdateTracked($oMyChange); - break; - - case 'delete': - default: - $oStatLog->AddTrace("Destination object to be DELETED", $oReplica); - $oReplica->DeleteDestObject($oMyChange, $oStatLog); - } - } - } // if ($sDeletePolicy != 'ignore' - - //Count "seen" objects - $sSelectSeen = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('new', 'synchronized', 'modified', 'orphan') AND status_last_seen >= :last_import"; - $oSetSeen = new DBObjectSet(DBObjectSearch::FromOQL($sSelectSeen), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sLimitDate)); - $oStatLog->Set('stats_nb_replica_seen', $oSetSeen->Count()); - - // Get all the replicas that are 'new' or modified or synchronized with a warning - // - $sSelectToSync = "SELECT SynchroReplica WHERE (status = 'new' OR status = 'modified' OR (status = 'synchronized' AND status_last_warning != '')) AND sync_source_id = :source_id AND status_last_seen >= :last_import"; - $oSetToSync = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToSync), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sLimitDate) /* aArgs */, $aExtDataSpec, 0 /* limitCount */, 0 /* limitStart */); - - while($oReplica = $oSetToSync->Fetch()) - { - $oReplica->Synchro($this, $aReconciliationKeys, $aAttributes, $oMyChange, $oStatLog); - $oReplica->DBUpdateTracked($oMyChange); - } - - // Get all the replicas that are to be deleted - // - if ($sDeletePolicy == 'update_then_delete') - { - $oDeletionDate = $oLastFullLoadStartDate; - $iDeleteRetention = $this->Get('delete_policy_retention'); // Duration in seconds - if ($iDeleteRetention > 0) - { - $sInterval = "-$iDeleteRetention seconds"; - $oDeletionDate->Modify($sInterval); - } - $sDeletionDate = $oDeletionDate->Format('Y-m-d H:i:s'); - $oStatLog->AddTrace("Deletion date: $sDeletionDate"); - $sSelectToDelete = "SELECT SynchroReplica WHERE sync_source_id = :source_id AND status IN ('obsolete') AND status_last_seen < :last_import"; - $oSetToDelete = new DBObjectSet(DBObjectSearch::FromOQL($sSelectToDelete), array() /* order by*/, array('source_id' => $this->GetKey(), 'last_import' => $sDeletionDate)); - while($oReplica = $oSetToDelete->Fetch()) - { - $oStatLog->AddTrace("Destination object to be DELETED", $oReplica); - $oReplica->DeleteDestObject($oMyChange, $oStatLog); - } - } - } - /** * Get the list of attributes eligible to the synchronization */ @@ -1519,6 +1207,8 @@ class SynchroLog extends DBObject MetaModel::Init_AddAttribute(new AttributeDateTime("start_date", array("allowed_values"=>null, "sql"=>"start_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeDateTime("end_date", array("allowed_values"=>null, "sql"=>"end_date", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('running,completed,error'), "sql"=>"status", "default_value"=>"running", "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("status_curr_job", array("allowed_values"=>null, "sql"=>"status_curr_job", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("status_curr_pos", array("allowed_values"=>null, "sql"=>"status_curr_pos", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_replica_seen", array("allowed_values"=>null, "sql"=>"stats_nb_replica_seen", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeInteger("stats_nb_replica_total", array("allowed_values"=>null, "sql"=>"stats_nb_replica_total", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); @@ -1545,6 +1235,8 @@ class SynchroLog extends DBObject MetaModel::Init_AddAttribute(new AttributeText("last_error", array("allowed_values"=>null, "sql"=>"last_error", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeLongText("traces", array("allowed_values"=>null, "sql"=>"traces", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeInteger("memory_usage_peak", array("allowed_values"=>null, "sql"=>"memory_usage_peak", "default_value"=>0, "is_null_allowed"=>false, "depends_on"=>array()))); + // Display lists MetaModel::Init_SetZListItems('details', array('sync_source_id', 'start_date', 'end_date', 'status', 'stats_nb_replica_total', 'stats_nb_replica_seen', 'stats_nb_obj_created', /*'stats_nb_replica_reconciled',*/ 'stats_nb_obj_updated', 'stats_nb_obj_obsoleted', 'stats_nb_obj_deleted', 'stats_nb_obj_created_errors', 'stats_nb_replica_reconciled_errors', 'stats_nb_replica_disappeared_no_action', 'stats_nb_obj_updated_errors', 'stats_nb_obj_obsoleted_errors', 'stats_nb_obj_deleted_errors', 'stats_nb_obj_new_unchanged', 'stats_nb_obj_new_updated', 'traces')); // Attributes to be displayed for the complete details @@ -1618,14 +1310,26 @@ class SynchroLog extends DBObject return; } + $sPrevTrace = $this->Get('traces'); + $oAttDef = MetaModel::GetAttributeDef(get_class($this), 'traces'); $iMaxSize = $oAttDef->GetMaxSize(); - $sTrace = implode("\n", $this->m_aTraces); + if (strlen($sPrevTrace) > 0) + { + $sTrace = $sPrevTrace."\n".implode("\n", $this->m_aTraces); + } + else + { + $sTrace = implode("\n", $this->m_aTraces); + } if (strlen($sTrace) >= $iMaxSize) { $sTrace = substr($sTrace, 0, $iMaxSize - 255)."...\nTruncated (size: ".strlen($sTrace).')'; } $this->Set('traces', $sTrace); + + //DBUpdate may be called many times... the operation should not be repeated + $this->m_aTraces = array(); } protected function OnInsert() @@ -1637,6 +1341,8 @@ class SynchroLog extends DBObject protected function OnUpdate() { $this->TraceToText(); + $sMemPeak = max($this->Get('memory_usage_peak'), ExecutionKPI::memory_get_peak_usage()); + $this->Set('memory_usage_peak', $sMemPeak); parent::OnUpdate(); } } @@ -2281,13 +1987,16 @@ class SynchroReplica extends DBObject implements iDisplay } $oPage->Details($aDetails); $oPage->add(''); - $oDestObj = MetaModel::GetObject($this->Get('dest_class'), $this->Get('dest_id'), false); - if (is_object($oDestObj)) + if (strlen($this->Get('dest_class')) > 0) { - $oPage->add('