diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 540ae3d8e..ddc9dcb32 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -5761,36 +5761,37 @@ abstract class MetaModel self::$m_sEnvironment = $sEnvironment; - try { - if (!defined('MODULESROOT')) { - define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/'); + if (!defined('MODULESROOT')) { + define('MODULESROOT', APPROOT.'env-'.self::$m_sEnvironment.'/'); - self::$m_bTraceSourceFiles = $bTraceSourceFiles; + self::$m_bTraceSourceFiles = $bTraceSourceFiles; - // $config can be either a filename, or a Configuration object (volatile!) - if ($config instanceof Config) { - self::LoadConfig($config, $bAllowCache); - } else { - self::LoadConfig(new Config($config), $bAllowCache); - } - - if ($bModelOnly) { - return; - } + // $config can be either a filename, or a Configuration object (volatile!) + if ($config instanceof Config) { + self::LoadConfig($config, $bAllowCache); + } else { + self::LoadConfig(new Config($config), $bAllowCache); } - CMDBSource::SelectDB(self::$m_sDBName); - - foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) { - $oPHPClass::OnMetaModelStarted(); + if ($bModelOnly) { + // Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface + EventService::InitService(); + EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED)); + return; } - - ExpressionCache::Warmup(); - } finally { - // Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface - EventService::InitService(); - EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED)); } + + CMDBSource::SelectDB(self::$m_sDBName); + + foreach (MetaModel::EnumPlugins('ModuleHandlerApiInterface') as $oPHPClass) { + $oPHPClass::OnMetaModelStarted(); + } + + ExpressionCache::Warmup(); + + // Event service must be initialized after the MetaModel startup, otherwise it cannot discover classes implementing the iEventServiceSetup interface + EventService::InitService(); + EventService::FireEvent(new EventData(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED)); } /** diff --git a/setup/feature_removal/ModelReflectionSerializer.php b/setup/feature_removal/ModelReflectionSerializer.php index 7e858a909..c088e9603 100644 --- a/setup/feature_removal/ModelReflectionSerializer.php +++ b/setup/feature_removal/ModelReflectionSerializer.php @@ -2,11 +2,10 @@ namespace Combodo\iTop\Setup\FeatureRemoval; -use ContextTag; use CoreException; -use Exception; use IssueLog; use SetupLog; +use SetupUtils; use utils; class ModelReflectionSerializer @@ -15,6 +14,7 @@ class ModelReflectionSerializer protected function __construct() { + SetupLog::Enable(APPROOT.'log/setup.log'); } final public static function GetInstance(): ModelReflectionSerializer @@ -31,42 +31,53 @@ class ModelReflectionSerializer self::$oInstance = $oInstance; } + public const ERROR_LABEL = "Data consistency check failed: %s"; + public function GetModelFromEnvironment(string $sEnv): array { IssueLog::Debug(__METHOD__, null, ['env' => $sEnv]); - $sPHPExec = trim(utils::GetConfig()->Get('php_path')); - $sOutput = ""; - $iRes = 0; + SetupUtils::CheckCliPhpVersionIsOk(self::ERROR_LABEL); - $sCommandLine = sprintf("$sPHPExec %s/get_model_reflection.php --env=%s", __DIR__, escapeshellarg($sEnv)); - exec($sCommandLine, $sOutput, $iRes); - if ($iRes != 0) { - $this->LogErrorWithProperLogger("Cannot get classes", null, ['env' => $sEnv, 'code' => $iRes, "output" => $sOutput, 'cmd' => $sCommandLine]); - throw new CoreException("Cannot get classes from env ".$sEnv); + //preliminary check + $sEnvDir = APPROOT."env-$sEnv"; + if (! is_dir($sEnvDir)) { + $sMsg = sprintf(self::ERROR_LABEL, "Missing environment ($sEnvDir)"); + SetupLog::Error($sMsg); + throw new CoreException($sMsg); } - $aClasses = json_decode($sOutput[0] ?? null, true); + $sConfigFile = APPROOT."conf/$sEnv/config-itop.php"; + if (! is_file($sConfigFile)) { + $sMsg = sprintf(self::ERROR_LABEL, "Missing configuration ($sConfigFile)"); + SetupLog::Error($sMsg); + throw new CoreException($sMsg); + } + + $sPHPExec = trim(utils::GetConfig()->Get('php_path')); + $aOutput = null; + $iRes = 0; + $sCommandLine = sprintf("$sPHPExec %s/get_model_reflection.php --env=%s", __DIR__, escapeshellarg($sEnv)); + exec($sCommandLine, $aOutput, $iRes); + if ($iRes != 0) { + $sError = $aOutput[0] ?? 'Invalid output when serializing model'; + SetupLog::Error(sprintf(self::ERROR_LABEL, '(cli error) '.$sError), null, ['env' => $sEnv, 'code' => $iRes, "output" => $aOutput, 'cmd' => $sCommandLine]); + throw new CoreException(sprintf(self::ERROR_LABEL, $sError)); + } + + $aClasses = json_decode($aOutput[0] ?? null, true); if (false === $aClasses) { - $this->LogErrorWithProperLogger("Invalid JSON", null, ['env' => $sEnv, "output" => $sOutput]); - throw new Exception("cannot get classes"); + $sMsg = sprintf(self::ERROR_LABEL, 'Invalid JSON'); + SetupLog::Error($sMsg, null, ['env' => $sEnv, "output" => $aOutput]); + throw new CoreException($sMsg); } if (!is_array($aClasses)) { - $this->LogErrorWithProperLogger("not an array", null, ['env' => $sEnv, "classes" => $aClasses, "output" => $sOutput]); - throw new Exception("cannot get classes from $sEnv"); + $sError = $aOutput[0] ?? 'Invalid json array when serializing model'; + SetupLog::Error(sprintf(self::ERROR_LABEL, '(JSON output not an array) '.$sError), null, ['env' => $sEnv, "classes" => $aClasses, "output" => $aOutput]); + throw new CoreException(sprintf(self::ERROR_LABEL, $sError)); } return $aClasses; } - - //could be shared with others in log APIs ? - private function LogErrorWithProperLogger($sMessage, $sChannel = null, $aContext = []): void - { - if (ContextTag::Check(ContextTag::TAG_SETUP)) { - SetupLog::Error($sMessage, $sChannel, $aContext); - } else { - IssueLog::Error($sMessage, $sChannel, $aContext); - } - } } diff --git a/setup/feature_removal/get_model_reflection.php b/setup/feature_removal/get_model_reflection.php index 92f99e906..459be319a 100644 --- a/setup/feature_removal/get_model_reflection.php +++ b/setup/feature_removal/get_model_reflection.php @@ -21,8 +21,7 @@ $sConfFile = utils::GetConfigFilePath($sEnv); try { MetaModel::Startup($sConfFile, false /* $bModelOnly */, false /* $bAllowCache */, false /* $bTraceSourceFiles */, $sEnv); } catch (\Throwable $e) { - echo $e->getMessage(); - echo $e->getTraceAsString(); + SetupLog::Enable(APPROOT.'log/setup.log'); \SetupLog::Error( "Cannot read model from provided environment", null, @@ -32,7 +31,9 @@ try { 'stack' => $e->getTraceAsString(), ] ); - echo "Cannot read model from provided environment"; + + //keep first echo to have proper setup feedbacks + echo $e->getMessage(); exit(1); } diff --git a/setup/sequencers/DataAuditSequencer.php b/setup/sequencers/DataAuditSequencer.php index 2b68cfb56..6786be0e8 100644 --- a/setup/sequencers/DataAuditSequencer.php +++ b/setup/sequencers/DataAuditSequencer.php @@ -70,6 +70,7 @@ class DataAuditSequencer extends StepSequencer $aSelectedExtensionCodes = $this->oParams->Get('selected_extensions', []); $bUseSymbolicLinks = $this->oParams->Get('use_symbolic_links', null) === 'on'; MetaModel::ResetAllCaches($this->oRunTimeEnvironment->GetBuildEnv()); + $this->oRunTimeEnvironment->DoCompile( $aSelectedExtensionCodes, $aRemovedExtensionCodes, @@ -79,6 +80,7 @@ class DataAuditSequencer extends StepSequencer return $this->ComputeNextStep($sStep); case 'setup-audit': + $this->oRunTimeEnvironment->ExitMaintenanceMode(); $this->oRunTimeEnvironment->DataToCleanupAudit(); return $this->ComputeNextStep($sStep); diff --git a/setup/setuputils.class.inc.php b/setup/setuputils.class.inc.php index ec3077d63..082220ab7 100644 --- a/setup/setuputils.class.inc.php +++ b/setup/setuputils.class.inc.php @@ -232,6 +232,13 @@ class SetupUtils $aResult[] = new CheckResult(CheckResult::WARNING, "Missing optional PHP extension: $sExtension. ".$sMessage); } } + + try { + SetupUtils::CheckCliPhpVersionIsOk(); + } catch (CoreException $e) { + $aResult[] = new CheckResult(CheckResult::WARNING, $e->getMessage()); + } + // Check some ini settings here if (function_exists('php_ini_loaded_file')) { // PHP >= 5.2.4 $sPhpIniFile = php_ini_loaded_file(); @@ -2192,6 +2199,41 @@ JS return [$sButtonLabel, $sButtonUrl]; } + + /** + * @param string $sErrorLabel: error label with 1 placeholder inside for detailed error + * @return void + * @throws \ConfigException + * @throws \CoreException + */ + public static function CheckCliPhpVersionIsOk(string $sErrorLabel = '%s'): void + { + $sPHPExec = trim(utils::GetConfig()->Get('php_path')); + $aOutput = null; + $iRes = 0; + exec("$sPHPExec --version", $aOutput, $iRes); + if ($iRes != 0) { + $sError = sprintf($sErrorLabel, "Cannot check CLI/PHP version ($sPHPExec)"); + SetupLog::Error($sError, null, ['code' => $iRes, "output" => $aOutput, 'php_path' => $sPHPExec]); + throw new CoreException($sError); + } + + SetupUtils::CheckCliPhpVersionFromOutput($sErrorLabel, PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION, $sPHPExec, $aOutput); + } + + private static function CheckCliPhpVersionFromOutput(string $sErrorLabel, string $sUIPhpVersion, string $sPHPExec, $aOutput): void + { + $sFoundVersion = trim($aOutput[0] ?? ""); + if (false !== preg_match('/(\d+\.\d+)(?:\.\d+)?/', $sFoundVersion, $aMatches)) { + $sFoundVersion = $aMatches[1]; + } + + if ($sFoundVersion !== $sUIPhpVersion) { + $sError = sprintf($sErrorLabel, "Mismatch between PHP versions (CLI: $sFoundVersion/ UI: $sUIPhpVersion)"); + SetupLog::Error($sError, null, ["output" => $aOutput, 'php_path' => $sPHPExec]); + throw new CoreException($sError); + } + } } /** diff --git a/tests/php-unit-tests/unitary-tests/setup/SetupUtilsTest.php b/tests/php-unit-tests/unitary-tests/setup/SetupUtilsTest.php index 219692374..f13b586ad 100644 --- a/tests/php-unit-tests/unitary-tests/setup/SetupUtilsTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/SetupUtilsTest.php @@ -2,6 +2,7 @@ namespace Combodo\iTop\Test\UnitTest\Setup; +use Combodo\iTop\Setup\FeatureRemoval\ModelReflectionSerializer; use Combodo\iTop\Test\UnitTest\ItopTestCase; use SetupUtils; @@ -146,4 +147,42 @@ class SetupUtilsTest extends ItopTestCase ); } } + + public function testCheckCliPhpVersionIsOk() + { + $this->RequireOnceItopFile('/setup/feature_removal/ModelReflectionSerializer.php'); + SetupUtils::CheckCliPhpVersionIsOk(ModelReflectionSerializer::ERROR_LABEL); + $this->assertTrue(true); + } + + public function testCheckCliPhpVersionFromOutputFail() + { + $this->RequireOnceItopFile('/setup/feature_removal/ModelReflectionSerializer.php'); + $sOuput = <<expectException(\CoreException::class); + $this->expectExceptionMessage("Data consistency check failed: Mismatch between PHP versions (CLI: 7.4/ UI: 6.6)"); + $this->InvokeNonPublicStaticMethod(SetupUtils::class, 'CheckCliPhpVersionFromOutput', [ModelReflectionSerializer::ERROR_LABEL, '6.6', 'sPHPExec', [$sOuput]]); + } + + public function CheckOKProvider() + { + return [ + ["7.4 7.2 7.3.33"], + ["PHP 7.4.33 (cli) (built: Aug 2 2024 16:22:28) ( NTS )"], + ["PHP 7.4.33 PHP 7.33.22"], + ["version: 7.4.27 stable"], + ]; + } + /** + * @dataProvider CheckOKProvider + */ + public function testCheckCliPhpVersionFromOutputOK($sOuput) + { + $this->RequireOnceItopFile('/setup/feature_removal/ModelReflectionSerializer.php'); + $this->InvokeNonPublicStaticMethod(SetupUtils::class, 'CheckCliPhpVersionFromOutput', [ModelReflectionSerializer::ERROR_LABEL, '7.4', 'sPHPExec', [$sOuput]]); + $this->assertTrue(true); + } } diff --git a/tests/php-unit-tests/unitary-tests/setup/feature_removal/ModelSerializationTest.php b/tests/php-unit-tests/unitary-tests/setup/feature_removal/ModelSerializationTest.php index b671ac984..1def776fb 100644 --- a/tests/php-unit-tests/unitary-tests/setup/feature_removal/ModelSerializationTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/feature_removal/ModelSerializationTest.php @@ -20,10 +20,58 @@ class ModelSerializationTest extends ItopDataTestCase $this->assertEqualsCanonicalizing(MetaModel::GetClasses(), $aModel); } - public function testGetModelFromEnvironmentFailure() + public function testGetModelFromEnvironmentFailure_NoEnvt() { $this->expectException(\CoreException::class); - $this->expectExceptionMessage("Cannot get classes"); + $sEnvDir = APPROOT."env-gabuzomeu"; + $this->expectExceptionMessage("Data consistency check failed: Missing environment ($sEnvDir)"); ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment('gabuzomeu'); } + + public function testGetModelFromEnvironmentFailure_NoConfiguration() + { + $sEnvDir = APPROOT."env-gabuzomeu"; + $this->aFileToClean [] = $sEnvDir; + mkdir($sEnvDir); + + $this->expectException(\CoreException::class); + $sConfigFile = APPROOT."conf/gabuzomeu/config-itop.php"; + $this->expectExceptionMessage("Data consistency check failed: Missing configuration ($sConfigFile)"); + ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment('gabuzomeu'); + } + + public function testGetModelFromEnvironmentFailure_BrokenConfiguration() + { + $sEnvDir = APPROOT."env-gabuzomeu"; + mkdir($sEnvDir); + $this->aFileToClean [] = $sEnvDir; + + mkdir(APPROOT."conf/gabuzomeu"); + $this->aFileToClean [] = APPROOT."conf/gabuzomeu"; + $sConfigFile = APPROOT."conf/gabuzomeu/config-itop.php"; + touch($sConfigFile); + file_put_contents($sConfigFile, 'invalid php content...'); + + $this->expectException(\CoreException::class); + $sError = <<invalid php content... +ERROR; + + $this->expectExceptionMessage("Data consistency check failed: $sError"); + ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment('gabuzomeu'); + } + + public function testGetModelFromEnvironmentFailure_ItopInMaintenanceMode() + { + touch(MAINTENANCE_MODE_FILE); + $this->aFileToClean [] = MAINTENANCE_MODE_FILE; + + $this->expectException(\CoreException::class); + $sError = <<expectExceptionMessage("Data consistency check failed: $sError"); + ModelReflectionSerializer::GetInstance()->GetModelFromEnvironment($this->GetTestEnvironment()); + } } diff --git a/tests/php-unit-tests/unitary-tests/setup/sequencers/DataAuditSequencerTest.php b/tests/php-unit-tests/unitary-tests/setup/sequencers/DataAuditSequencerTest.php index 6f81e0b23..e50c3126a 100644 --- a/tests/php-unit-tests/unitary-tests/setup/sequencers/DataAuditSequencerTest.php +++ b/tests/php-unit-tests/unitary-tests/setup/sequencers/DataAuditSequencerTest.php @@ -201,6 +201,7 @@ class DataAuditSequencerTest extends ItopTestCase $oRunTimeEnvironment = $this->createMock(\RunTimeEnvironment::class); $oRunTimeEnvironment->expects($this->once())->method('GetApplicationVersion') ->willReturn(['product_version' => ITOP_VERSION_FULL]); + $oRunTimeEnvironment->expects($this->once())->method('ExitMaintenanceMode'); $oRunTimeEnvironment->expects($this->once())->method('DataToCleanupAudit'); $oRunTimeEnvironment->expects($this->any())->method('GetFinalEnv') ->willReturn('production');