p("ERROR: Missing argument '$sParam'\n"); UsageAndExit($oP); } return trim($sValue); } function UsageAndExit($oP) { $bModeCLI = ($oP instanceof CLIPage); if ($bModeCLI) { $oP->p("USAGE:\n"); $oP->p("php cron.php --auth_user= --auth_pwd= [--param_file=] [--verbose=0] [--status_only=1]\n"); } else { $oP->p("Optional parameters: verbose, param_file, status_only\n"); } $oP->output(); exit(EXIT_CODE_FATAL); } /** * @param \BackgroundTask $oTask * @param int $iTimeLimit * * @return string * @throws \ArchivedObjectException * @throws \CoreCannotSaveObjectException * @throws \CoreException * @throws \CoreUnexpectedValue * @throws \MySQLHasGoneAwayException * @throws \ProcessFatalException * @throws \ReflectionException * @throws \Exception */ function RunTask(BackgroundTask $oTask, $iTimeLimit) { $TaskClass = $oTask->Get('class_name'); $oProcess = new $TaskClass(); $oRefClass = new ReflectionClass(get_class($oProcess)); $oDateStarted = new DateTime(); $fStart = microtime(true); $oCtx = new ContextTag('CRON:Task:'.$TaskClass); $sMessage = ''; $oExceptionToThrow = null; try { // Record (when starting) that this task was started, just in case it crashes during the execution if ($oTask->Get('total_exec_count') == 0) { // First execution $oTask->Set('first_run_date', $oDateStarted->format('Y-m-d H:i:s')); } $oTask->Set('latest_run_date', $oDateStarted->format('Y-m-d H:i:s')); // Record the current user running the cron $oTask->Set('system_user', utils::GetCurrentUserName()); $oTask->Set('running', 1); // Compute the next run date if ($oRefClass->implementsInterface('iScheduledProcess')) { // Schedules process do repeat at specific moments $oPlannedStart = $oProcess->GetNextOccurrence(); } else { // Background processes do repeat periodically $oDatePlanned = new DateTime($oTask->Get('next_run_date')); $oPlannedStart = clone $oDatePlanned; // Let's schedule from the previous planned date of execution to avoid shift $oPlannedStart->modify('+'.$oProcess->GetPeriodicity().' seconds'); $oNow = new DateTime(); while ($oPlannedStart->format('U') <= $oNow->format('U')) { // Next planned start is already in the past, increase it again by a period $oPlannedStart = $oPlannedStart->modify('+'.$oProcess->GetPeriodicity().' seconds'); } } $oTask->Set('next_run_date', $oPlannedStart->format('Y-m-d H:i:s')); $oTask->DBUpdate(); $sMessage = $oProcess->Process($iTimeLimit); } catch (MySQLHasGoneAwayException $e) { throw $e; } catch (ProcessFatalException $e) { $oExceptionToThrow = $e; } catch (Exception $e) { // we shouldn't get so much exceptions... but we need to handle legacy code, and cron.php has to keep running if ($oTask->IsDebug()) { $sMessage = 'Processing failed with message: '.$e->getMessage().'. '.$e->getTraceAsString(); } else { $sMessage = 'Processing failed with message: '.$e->getMessage(); } } finally { $oTask->Set('running', 0); $fDuration = microtime(true) - $fStart; $oTask->ComputeDurations($fDuration); // does increment the counter and compute statistics $oTask->DBUpdate(); } if ($oExceptionToThrow) { throw $oExceptionToThrow; } unset($oCtx); return $sMessage; } /** * * @param bool $bDebug * * @throws \ArchivedObjectException * @throws \CoreCannotSaveObjectException * @throws \CoreException * @throws \CoreUnexpectedValue * @throws \CoreWarning * @throws \MissingQueryArgument * @throws \MySQLException * @throws \MySQLHasGoneAwayException * @throws \OQLException * @throws \ReflectionException */ function CronExec($bDebug ) { $iStarted = time(); $iMaxDuration = MetaModel::GetConfig()->Get('cron_max_execution_time'); $iTimeLimit = $iStarted + $iMaxDuration; $iCronSleep = MetaModel::GetConfig()->Get('cron_sleep'); $iMaxCronProcess = max(MetaModel::GetConfig()->Get('cron.max_processes'), 1); // Allow a time slot for every task // knowing that there are $iMaxCronProcess running in parallel for the amount of tasks $oSearch = new DBObjectSearch('BackgroundTask'); $oSearch->AddCondition('status', 'active'); $oTasks = new DBObjectSet($oSearch); $iCount = $oTasks->Count(); $iTotalAvailableTime = $iMaxDuration * $iMaxCronProcess; $iTimeSlot = (int)($iTotalAvailableTime / max($iCount, 1)); CronLog::Trace("Planned duration = $iMaxDuration seconds"); CronLog::Trace("Planned duration per task = $iTimeSlot seconds"); CronLog::Trace("Loop pause = $iCronSleep seconds"); ReSyncProcesses($bDebug); while (time() < $iTimeLimit) { CheckMaintenanceMode(); $oNow = new DateTime(); $sNow = $oNow->format('Y-m-d H:i:s'); $oSearch = new DBObjectSearch('BackgroundTask'); $oSearch->AddCondition('next_run_date', $sNow, '<='); $oSearch->AddCondition('status', 'active'); $oTasks = new DBObjectSet($oSearch, ['next_run_date' => true]); $aTasks = []; if ($oTasks->CountExceeds(0)) { $aDebugMessages = []; while ($oTask = $oTasks->Fetch()) { $sTaskName = $oTask->Get('class_name'); $oTaskMutex = new iTopMutex("cron_$sTaskName"); if ($oTaskMutex->IsLocked()) { // Already running, ignore continue; } $aTasks[] = $oTask; $sStatus = $oTask->Get('status'); $sLastRunDate = $oTask->Get('latest_run_date'); $sNextRunDate = $oTask->Get('next_run_date'); $aDebugMessages[] = sprintf('Task Class: %1$-25.25s Status: %2$-7s Last Run: %3$-19s Next Run: %4$-19s', $sTaskName, $sStatus, $sLastRunDate, $sNextRunDate); } $sCount = count($aDebugMessages); CronLog::Trace("$sCount Tasks planned to run now ($sNow):"); foreach ($aDebugMessages as $sDebugMessage) { CronLog::Trace($sDebugMessage); } $aRunTasks = []; while (count($aTasks) > 0) { $oTask = array_shift($aTasks); $sTaskClass = $oTask->Get('class_name'); // Check if the current task is running $oTaskMutex = new iTopMutex("cron_$sTaskClass"); if (!$oTaskMutex->TryLock()) { // Task is already running, try next one continue; } $aRunTasks[] = $sTaskClass; // N°3219 for each process will use a specific CMDBChange object with a specific track info // Any BackgroundProcess can override this as needed CMDBObject::SetCurrentChangeFromParams("Background task ($sTaskClass)"); // Run the task and record its next run time $sDebugTaskClass = CronLog::GetDebugClassName($sTaskClass); $oNow = new DateTime(); CronLog::Debug(sprintf("> Starting >>> %-'>49s", $sDebugTaskClass.' ')); try { // The limit of time for this task corresponds to the time slot allowed for every task // but limited to the cron job time limit $sMessage = RunTask($oTask, min($iTimeLimit, time() + $iTimeSlot)); } catch (MySQLHasGoneAwayException $e) { CronLog::Error("ERROR : 'MySQL has gone away' thrown when processing $sDebugTaskClass (error_code=".$e->getCode().")", CronLog::CHANNEL_DEFAULT, ['stack' => $e->getTraceAsString()]); exit(EXIT_CODE_FATAL); } catch (ProcessFatalException $e) { CronLog::Error("ERROR : an exception was thrown when processing '$sDebugTaskClass' (".$e->getInfoLog().")", CronLog::CHANNEL_DEFAULT, ['stack' => $e->getTraceAsString()]); } finally { $oTaskMutex->Unlock(); } if (!empty($sMessage)) { CronLog::Debug("$sDebugTaskClass: $sMessage"); } $oEnd = new DateTime(); $sNextRunDate = $oTask->Get('next_run_date'); CronLog::Debug(sprintf("< Ending <<<<< %-'<49s", $sDebugTaskClass.' ')." Next: $sNextRunDate"); if (time() > $iTimeLimit) { break 2; } CheckMaintenanceMode(); if ($iMaxCronProcess > 1) { // Reindex tasks every time break; } } // Tasks to run later if (count($aTasks) == 0) { $oSearch = new DBObjectSearch('BackgroundTask'); $oSearch->AddCondition('next_run_date', $sNow, '>'); $oSearch->AddCondition('status', 'active'); $oTasks = new DBObjectSet($oSearch, ['next_run_date' => true]); while ($oTask = $oTasks->Fetch()) { if (!in_array($oTask->Get('class_name'), $aRunTasks)) { $sDebugTaskClass = CronLog::GetDebugClassName($oTask->Get('class_name')); CronLog::Trace(sprintf("-- Skipping task: %-'-40s", $sDebugTaskClass.' ')." until: ".$oTask->Get('next_run_date')); } } } } if (count($aTasks) == 0) { CronLog::Trace("sleeping..."); sleep($iCronSleep); } } CronLog::Trace("Reached normal execution time limit (exceeded by ".(time() - $iTimeLimit)."s)"); } function CheckMaintenanceMode() { // Verify files instead of reloading the full config each time if (file_exists(MAINTENANCE_MODE_FILE) || file_exists(READONLY_MODE_FILE)) { CronLog::Info("Maintenance detected, exiting"); exit(EXIT_CODE_ERROR); } } /** * @param $oP * @param array $aTaskOrderBy * * @throws \ArchivedObjectException * @throws \CoreException * @throws \CoreUnexpectedValue * @throws \MySQLException * @throws \OQLException */ function DisplayStatus($oP = null, $aTaskOrderBy = []) { $oSearch = new DBObjectSearch('BackgroundTask'); $oTasks = new DBObjectSet($oSearch, $aTaskOrderBy); $oP->p('+---------------------------+---------+---------------------+---------------------+--------+-----------+'); $oP->p('| Task Class | Status | Last Run | Next Run | Nb Run | Avg. Dur. |'); $oP->p('+---------------------------+---------+---------------------+---------------------+--------+-----------+'); while ($oTask = $oTasks->Fetch()) { $sTaskName = $oTask->Get('class_name'); $sStatus = $oTask->Get('status'); $sLastRunDate = $oTask->Get('latest_run_date'); $sNextRunDate = $oTask->Get('next_run_date'); $iNbRun = (int)$oTask->Get('total_exec_count'); $sAverageRunTime = $oTask->Get('average_run_duration'); $oP->p(sprintf( '| %1$-25.25s | %2$-7s | %3$-19s | %4$-19s | %5$6d | %6$7s s |', $sTaskName, $sStatus, $sLastRunDate, $sNextRunDate, $iNbRun, $sAverageRunTime )); } $oP->p('+---------------------------+---------+---------------------+---------------------+--------+-----------+'); } /** * @param $bDebug * * @throws \ArchivedObjectException * @throws \CoreCannotSaveObjectException * @throws \CoreException * @throws \CoreUnexpectedValue * @throws \CoreWarning * @throws \MySQLException * @throws \OQLException * @throws \ReflectionException */ function ReSyncProcesses($bDebug) { // Enumerate classes implementing BackgroundProcess // $oSearch = new DBObjectSearch('BackgroundTask'); $oTasks = new DBObjectSet($oSearch); $aTasks = []; while ($oTask = $oTasks->Fetch()) { $aTasks[$oTask->Get('class_name')] = $oTask; } $oNow = new DateTime(); $aProcesses = []; foreach (InterfaceDiscovery::GetInstance()->FindItopClasses(iProcess::class) as $sTaskClass) { $oProcess = new $sTaskClass(); $aProcesses[$sTaskClass] = $oProcess; // Create missing entry if needed if (!array_key_exists($sTaskClass, $aTasks)) { // New entry, let's create a new BackgroundTask record, and plan the first execution $oTask = new BackgroundTask(); $oTask->SetDebug($bDebug); $oTask->Set('class_name', $sTaskClass); $oTask->Set('total_exec_count', 0); $oTask->Set('min_run_duration', 99999.999); $oTask->Set('max_run_duration', 0); $oTask->Set('average_run_duration', 0); $oRefClass = new ReflectionClass($sTaskClass); if ($oRefClass->implementsInterface('iScheduledProcess')) { $oNextOcc = $oProcess->GetNextOccurrence(); $oTask->Set('next_run_date', $oNextOcc->format('Y-m-d H:i:s')); } else { // Background processes do start asap, i.e. "now" $oTask->Set('next_run_date', $oNow->format('Y-m-d H:i:s')); } $sDebugTaskClass = CronLog::GetDebugClassName($sTaskClass); CronLog::Trace('Creating record for: '.$sDebugTaskClass); CronLog::Trace('First execution planned at: '.$oTask->Get('next_run_date')); $oTask->DBInsert(); } else { /** @var \BackgroundTask $oTask */ $oTask = $aTasks[$sTaskClass]; if ($oTask->Get('next_run_date') == '3000-01-01 00:00:00') { // check for rescheduled tasks $oRefClass = new ReflectionClass($sTaskClass); if ($oRefClass->implementsInterface('iScheduledProcess')) { $oNextOcc = $oProcess->GetNextOccurrence(); $oTask->Set('next_run_date', $oNextOcc->format('Y-m-d H:i:s')); $oTask->DBUpdate(); } } // Reactivate task if necessary if ($oTask->Get('status') == 'removed') { $oTask->Set('status', 'active'); $oTask->DBUpdate(); } // task having a real class to execute unset($aTasks[$sTaskClass]); } } // Remove all the tasks not having a valid class foreach ($aTasks as $oTask) { $sTaskClass = $oTask->Get('class_name'); if (!class_exists($sTaskClass)) { $oTask->Set('status', 'removed'); $oTask->DBUpdate(); } } $aDisplayProcesses = []; foreach ($aProcesses as $oExecInstance) { $aDisplayProcesses[] = get_class($oExecInstance); } $sDisplayProcesses = implode(', ', $aDisplayProcesses); CronLog::Trace("Background processes: ".$sDisplayProcesses); } //////////////////////////////////////////////////////////////////////////////// // // Main // set_time_limit(0); // Some background actions may really take long to finish (like backup) try { $bIsModeCLI = utils::IsModeCLI(); if ($bIsModeCLI) { $oP = new CLIPage("iTop - cron"); SetupUtils::CheckPhpAndExtensionsForCli($oP, EXIT_CODE_FATAL); utils::UseParamFile(); } else { $oP = new WebPage("iTop - cron"); } try { utils::UseParamFile(); // Allow verbosity on output from 0 => none, 1 => debug, 2 => trace // (writing debug messages to the cron.log file is configured with log_level_min config parameter) $iVerbose = utils::ReadParam('verbose', 0, true /* Allow CLI */); CronLog::SetDebug($oP, $iVerbose); if ($bIsModeCLI) { // Next steps: // specific arguments: 'csv file' // $sAuthUser = ReadMandatoryParam($oP, 'auth_user', 'raw_data'); $sAuthPwd = ReadMandatoryParam($oP, 'auth_pwd', 'raw_data'); if (UserRights::CheckCredentials($sAuthUser, $sAuthPwd)) { UserRights::Login($sAuthUser); // Login & set the user's language } else { $oP->p("Access wrong credentials ('$sAuthUser')"); $oP->output(); exit(EXIT_CODE_ERROR); } } else { require_once(APPROOT.'/application/loginwebpage.class.inc.php'); LoginWebPage::DoLogin(); // Check user rights and prompt if needed } if (!UserRights::IsAdministrator()) { $oP->p("Access restricted to administrators"); $oP->Output(); exit(EXIT_CODE_ERROR); } if (utils::ReadParam('status_only', false, true /* Allow CLI */)) { // Display status and exit DisplayStatus($oP); exit(0); } require_once(APPROOT.'core/mutex.class.inc.php'); } catch (Exception $e) { $oP->p("Error: ".$e->GetMessage()); $oP->output(); exit(EXIT_CODE_FATAL); } CronLog::Enable(APPROOT.'/log/error.log'); try { if (!MetaModel::DBHasAccess(ACCESS_ADMIN_WRITE)) { CronLog::Debug("A maintenance is ongoing"); } else { // Limit the number of cron process to run in parallel $iMaxCronProcess = max(MetaModel::GetConfig()->Get('cron.max_processes'), 1); $bCanRun = false; $iProcessNumber = 0; for ($i = 0; $i < $iMaxCronProcess; $i++) { $oMutex = new iTopMutex("cron#$i"); if ($oMutex->TryLock()) { $iProcessNumber = $i + 1; $bCanRun = true; break; } } if ($bCanRun) { CronLog::$iProcessNumber = $iProcessNumber; CronLog::Debug('Starting: '.time().' ('.date('Y-m-d H:i:s').')'); CronExec($iVerbose > 0); } else { CronLog::$iProcessNumber = $iMaxCronProcess + 1; CronLog::Trace("The limit of $iMaxCronProcess cron process running in parallel is already reached"); } } } catch (Exception $e) { CronLog::Error("ERROR: '".$e->getMessage()."'", CronLog::CHANNEL_DEFAULT, ['stack' => $e->getTraceAsString()]); } finally { try { $oMutex->Unlock(); } catch (Exception $e) { $oP->p("ERROR: '".$e->getMessage()."'"); if ($bDebug) { // Might contain verb parameters such a password... $oP->p($e->getTraceAsString()); } } } CronLog::Debug("Exiting: ".time().' ('.date('Y-m-d H:i:s').')'); $oP->Output();