565 lines
15 KiB
PHP
565 lines
15 KiB
PHP
<?php
|
|
/*********************************************************************************
|
|
*
|
|
* TimeTrex is a Workforce Management program developed by
|
|
* TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU Affero General Public License version 3 as published by
|
|
* the Free Software Foundation with the addition of the following permission
|
|
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
|
|
* WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
|
|
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
* details.
|
|
*
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License along
|
|
* with this program; if not, see http://www.gnu.org/licenses or write to the Free
|
|
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301 USA.
|
|
*
|
|
*
|
|
* You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
|
|
* #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
|
|
*
|
|
*
|
|
* The interactive user interfaces in modified source and object code versions
|
|
* of this program must display Appropriate Legal Notices, as required under
|
|
* Section 5 of the GNU Affero General Public License version 3.
|
|
*
|
|
*
|
|
* In accordance with Section 7(b) of the GNU Affero General Public License
|
|
* version 3, these Appropriate Legal Notices must retain the display of the
|
|
* "Powered by TimeTrex" logo. If the display of the logo is not reasonably
|
|
* feasible for technical reasons, the Appropriate Legal Notices must display
|
|
* the words "Powered by TimeTrex".
|
|
*
|
|
********************************************************************************/
|
|
|
|
|
|
/**
|
|
* @package Modules\Cron
|
|
*/
|
|
class CronJobFactory extends Factory {
|
|
protected $table = 'cron';
|
|
protected $pk_sequence_name = 'cron_id_seq'; //PK Sequence name
|
|
|
|
protected $execute_flag = false;
|
|
|
|
/**
|
|
* @param $name
|
|
* @param null $parent
|
|
* @return array|null
|
|
*/
|
|
function _getFactoryOptions( $name, $parent = null ) {
|
|
|
|
$retval = null;
|
|
switch ( $name ) {
|
|
case 'limit':
|
|
$retval = [
|
|
'minute' => [ 'min' => 0, 'max' => 59 ],
|
|
'hour' => [ 'min' => 0, 'max' => 23 ],
|
|
'day_of_month' => [ 'min' => 1, 'max' => 31 ],
|
|
'month' => [ 'min' => 1, 'max' => 12 ],
|
|
'day_of_week' => [ 'min' => 0, 'max' => 7 ],
|
|
];
|
|
break;
|
|
case 'status':
|
|
$retval = [
|
|
10 => TTi18n::gettext( 'READY' ),
|
|
20 => TTi18n::gettext( 'RUNNING' ),
|
|
];
|
|
break;
|
|
}
|
|
|
|
return $retval;
|
|
}
|
|
|
|
/**
|
|
* @return bool|int
|
|
*/
|
|
function getStatus() {
|
|
return $this->getGenericDataValue( 'status_id' );
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @return bool
|
|
*/
|
|
function setStatus( $value ) {
|
|
$value = (int)trim( $value );
|
|
|
|
return $this->setGenericDataValue( 'status_id', $value );
|
|
}
|
|
|
|
/**
|
|
* @return bool|mixed
|
|
*/
|
|
function getName() {
|
|
return $this->getGenericDataValue( 'name' );
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @return bool
|
|
*/
|
|
function setName( $value ) {
|
|
$value = trim( $value );
|
|
|
|
return $this->setGenericDataValue( 'name', $value );
|
|
}
|
|
|
|
/**
|
|
* @param $value_arr
|
|
* @param $limit_arr
|
|
* @return bool
|
|
* @noinspection PhpUnusedLocalVariableInspection
|
|
*/
|
|
function isValidLimit( $value_arr, $limit_arr ) {
|
|
if ( is_array( $value_arr ) && is_array( $limit_arr ) ) {
|
|
foreach ( $value_arr as $value ) {
|
|
if ( $value == '*' ) {
|
|
$retval = true;
|
|
}
|
|
|
|
if ( $value >= $limit_arr['min'] && $value <= $limit_arr['max'] ) {
|
|
$retval = true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return $retval;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return bool|mixed
|
|
*/
|
|
function getMinute() {
|
|
return $this->getGenericDataValue( 'minute' );
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @return bool
|
|
*/
|
|
function setMinute( $value ) {
|
|
$value = trim( $value );
|
|
|
|
return $this->setGenericDataValue( 'minute', $value );
|
|
}
|
|
|
|
/**
|
|
* @return bool|mixed
|
|
*/
|
|
function getHour() {
|
|
return $this->getGenericDataValue( 'hour' );
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @return bool
|
|
*/
|
|
function setHour( $value ) {
|
|
$value = trim( $value );
|
|
|
|
return $this->setGenericDataValue( 'hour', $value );
|
|
}
|
|
|
|
/**
|
|
* @return bool|mixed
|
|
*/
|
|
function getDayOfMonth() {
|
|
return $this->getGenericDataValue( 'day_of_month' );
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @return bool
|
|
*/
|
|
function setDayOfMonth( $value ) {
|
|
$value = trim( $value );
|
|
|
|
return $this->setGenericDataValue( 'day_of_month', $value );
|
|
}
|
|
|
|
/**
|
|
* @return bool|mixed
|
|
*/
|
|
function getMonth() {
|
|
return $this->getGenericDataValue( 'month' );
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @return bool
|
|
*/
|
|
function setMonth( $value ) {
|
|
$value = trim( $value );
|
|
|
|
return $this->setGenericDataValue( 'month', $value );
|
|
}
|
|
|
|
/**
|
|
* @return bool|mixed
|
|
*/
|
|
function getDayOfWeek() {
|
|
return $this->getGenericDataValue( 'day_of_week' );
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @return bool
|
|
*/
|
|
function setDayOfWeek( $value ) {
|
|
$value = trim( $value );
|
|
|
|
return $this->setGenericDataValue( 'day_of_week', $value );
|
|
}
|
|
|
|
/**
|
|
* @return bool|mixed
|
|
*/
|
|
function getCommand() {
|
|
return $this->getGenericDataValue( 'command' );
|
|
}
|
|
|
|
/**
|
|
* @param $value
|
|
* @return bool
|
|
*/
|
|
function setCommand( $value ) {
|
|
$value = trim( $value );
|
|
|
|
return $this->setGenericDataValue( 'command', $value );
|
|
}
|
|
|
|
/**
|
|
* @param bool $raw
|
|
* @return bool|int
|
|
*/
|
|
function getLastRunDate( $raw = false ) {
|
|
$value = $this->getGenericDataValue( 'last_run_date' );
|
|
if ( $value !== false ) {
|
|
if ( $raw === true ) {
|
|
return $value;
|
|
} else {
|
|
return TTDate::strtotime( $value );
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param int $value EPOCH
|
|
* @return bool
|
|
*/
|
|
function setLastRunDate( $value ) {
|
|
$value = ( !is_int( $value ) && $value !== null ) ? trim( $value ) : $value;//Dont trim integer values, as it changes them to strings.
|
|
|
|
return $this->setGenericDataValue( 'last_run_date', $value );
|
|
}
|
|
|
|
/**
|
|
* @param $bool
|
|
*/
|
|
private function setExecuteFlag( $bool ) {
|
|
$this->execute_flag = (bool)$bool;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
private function getExecuteFlag() {
|
|
return $this->execute_flag;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
function isSystemLoadValid() {
|
|
return Misc::isSystemLoadValid();
|
|
}
|
|
|
|
/**
|
|
* Check if job is scheduled to run right NOW.
|
|
* If the job has missed a run, it will run immediately.
|
|
* @param int $epoch EPOCH
|
|
* @param int $last_run_date EPOCH
|
|
* @return bool
|
|
*/
|
|
function isScheduledToRun( $epoch = null, $last_run_date = null ) {
|
|
//Debug::text('Checking if Cron Job is scheduled to run: '. $this->getName(), __FILE__, __LINE__, __METHOD__, 10);
|
|
if ( $epoch == '' ) {
|
|
$epoch = time();
|
|
}
|
|
|
|
//Debug::text('Checking if Cron Job is scheduled to run: '. $this->getName(), __FILE__, __LINE__, __METHOD__, 10);
|
|
if ( $last_run_date == '' ) {
|
|
$last_run_date = (int)$this->getLastRunDate();
|
|
}
|
|
|
|
Debug::text( ' Name: ' . $this->getName() . ' Current Epoch: ' . TTDate::getDate( 'DATE+TIME', $epoch ) . ' Last Run Date: ' . TTDate::getDate( 'DATE+TIME', $last_run_date ) .' Status: '. $this->getStatus(), __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return Cron::isScheduledToRun( $this->getMinute(), $this->getHour(), $this->getDayOfMonth(), $this->getMonth(), $this->getDayOfWeek(), $epoch, $last_run_date );
|
|
}
|
|
|
|
/**
|
|
* @param int $current_epoch
|
|
* @param string $job_id UUID
|
|
*/
|
|
static function ExecuteForJobQueue( $job_id, $command ) {
|
|
$retval = true;
|
|
|
|
//Get each cronjob row again individually incase the status has changed.
|
|
$cjlf = new CronJobListFactory();
|
|
$cjlf->getById( $job_id ); //Let Execute determine if job is running or not so it can find orphans.
|
|
if ( $cjlf->getRecordCount() > 0 ) {
|
|
foreach ( $cjlf as $cjf_obj ) {
|
|
$retval = $cjf_obj->ExecuteCommand( $command ); //Run command no matter when it was scheduled to run, in case the job queue is delayed.
|
|
}
|
|
}
|
|
|
|
return $retval;
|
|
}
|
|
|
|
function ExecuteCommand( $command ) {
|
|
Debug::text( 'Command: ' . $command, __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
$start_time = microtime( true );
|
|
exec( $command, $output, $retcode );
|
|
Debug::Arr( $output, 'Time: ' . ( microtime( true ) - $start_time ) . 's - Command RetCode: ' . $retcode . ' Output: ', __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
TTLog::addEntry( $this->getId(), 500, TTi18n::getText( 'Executing Cron Job' ) . ': ' . $this->getID() . ' ' . TTi18n::getText( 'Command' ) . ': ' . $command . ' ' . TTi18n::getText( 'Return Code' ) . ': ' . $retcode, null, $this->getTable() );
|
|
|
|
return $retcode;
|
|
}
|
|
|
|
/**
|
|
* Executes the CronJob
|
|
* @param null $php_cli
|
|
* @param null $dir
|
|
* @return bool
|
|
*/
|
|
function Execute( $php_cli = null, $dir = null ) {
|
|
global $config_vars;
|
|
$lock_file = new LockFile( $config_vars['cache']['dir'] . DIRECTORY_SEPARATOR . $this->getName() . '.lock' );
|
|
|
|
//Check job last updated date, if its more then 12hrs and its still in the "running" status,
|
|
//chances are its an orphan. Change status.
|
|
//if ( $this->getStatus() != 10 AND $this->getUpdatedDate() > 0 AND $this->getUpdatedDate() < (time() - ( 6 * 3600 )) ) {
|
|
if ( $this->getStatus() != 10 && $this->getUpdatedDate() > 0 ) {
|
|
Debug::text( 'Status: '. $this->getStatus() .' Updated Date: '. TTDate::getDate('DATE+TIME', $this->getUpdatedDate() ), __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
$clear_lock = false;
|
|
if ( $lock_file->exists() == false ) {
|
|
Debug::text( 'ERROR: Job PID is not running assuming its an orphan, marking as ready for next run.', __FILE__, __LINE__, __METHOD__, 10 );
|
|
$clear_lock = true;
|
|
} else if ( $this->getUpdatedDate() < ( time() - ( 6 * 3600 ) ) ) {
|
|
Debug::text( 'ERROR: Job has been running for more then 6 hours! Assuming its an orphan, marking as ready for next run.', __FILE__, __LINE__, __METHOD__, 10 );
|
|
$clear_lock = true;
|
|
}
|
|
|
|
if ( $clear_lock == true ) {
|
|
$this->setStatus( 10 );
|
|
$this->Save( false );
|
|
$lock_file->delete();
|
|
}
|
|
|
|
unset( $clear_lock );
|
|
}
|
|
|
|
if ( !is_executable( $php_cli ) ) {
|
|
Debug::text( 'ERROR: PHP CLI is not executable: ' . $php_cli, __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return false;
|
|
}
|
|
|
|
if ( $this->isSystemLoadValid() == false ) {
|
|
Debug::text( 'System load is too high, skipping...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
return false;
|
|
}
|
|
|
|
//Cron script to execute
|
|
$script = $dir . DIRECTORY_SEPARATOR . $this->getCommand();
|
|
|
|
if ( $this->getStatus() == 10 && $lock_file->exists() == false ) {
|
|
$lock_file->create();
|
|
|
|
$this->setExecuteFlag( true ); //Does not get saved to the database.
|
|
|
|
Debug::text( 'Job is NOT currently running, running now...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
//Mark job as running
|
|
$this->setStatus( 20 ); //Running
|
|
$this->setLastRunDate( TTDate::roundTime( time(), 60, 30 ) ); //Set last run date when job is marked as running, so for long running processes it won't attempt to get executed multiple times while its running.
|
|
$this->Save( false );
|
|
|
|
//Even if the file does not exist, we still need to "pretend" the cron job ran (set last ran date) so we don't
|
|
//display the big red error message saying that NO jobs have run in the last 24hrs.
|
|
if ( file_exists( $script ) ) {
|
|
$command = '"' . $php_cli . '" "' . $script . '"';
|
|
Debug::text( 'Command: ' . $command, __FILE__, __LINE__, __METHOD__, 10 );
|
|
|
|
if ( !isset( $config_vars['other']['enable_job_queue'] ) || $config_vars['other']['enable_job_queue'] == true ) {
|
|
//**IMPORTANT: The CRON table must update the LastRunDate immediately before the command in **queued** (rather than after its executed from the queue itself), otherwise cron.php will continue to try to run it over and over again every 1 minute if its a long running task.
|
|
SystemJobQueue::Add( TTi18n::getText( 'CRON' ) .': '. $this->getName(), null, 'CronJobFactory', 'ExecuteForJobQueue', [ $this->getId(), Misc::getEnvironmentVariableConfigFile() . $command ], 100 );
|
|
} else {
|
|
if ( DEPLOYMENT_ON_DEMAND == true && !( !isset( $config_vars['other']['enable_job_queue'] ) || $config_vars['other']['enable_job_queue'] == true ) ) { //In cases where many instances may be triggering jobs at the same time, add a random sleep to stagger them.
|
|
$sleep_timer = rand( 0, 120 );
|
|
Debug::text( ' Random Sleep: ' . $sleep_timer, __FILE__, __LINE__, __METHOD__, 10 );
|
|
sleep( $sleep_timer );
|
|
}
|
|
|
|
$this->ExecuteCommand( Misc::getEnvironmentVariableConfigFile() . $command );
|
|
}
|
|
} else {
|
|
Debug::text( 'WARNING: File does not exist, skipping: ' . $script, __FILE__, __LINE__, __METHOD__, 10 );
|
|
}
|
|
|
|
$this->setStatus( 10 ); //Ready
|
|
$this->Save( false );
|
|
|
|
$this->setExecuteFlag( false ); //This does not get saved to the database, so it needs to come after Save().
|
|
|
|
$lock_file->delete();
|
|
|
|
return true;
|
|
} else {
|
|
Debug::text( 'Job is currently running, skipping...', __FILE__, __LINE__, __METHOD__, 10 );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
function Validate() {
|
|
//
|
|
// BELOW: Validation code moved from set*() functions.
|
|
//
|
|
// Status
|
|
$this->Validator->inArrayKey( 'status',
|
|
$this->getStatus(),
|
|
TTi18n::gettext( 'Incorrect Status' ),
|
|
$this->getOptions( 'status' )
|
|
);
|
|
// Name
|
|
$this->Validator->isLength( 'name',
|
|
$this->getName(),
|
|
TTi18n::gettext( 'Name is invalid' ),
|
|
1, 250
|
|
);
|
|
// Minute
|
|
$this->Validator->isLength( 'minute',
|
|
$this->getMinute(),
|
|
TTi18n::gettext( 'Minute is invalid' ),
|
|
1, 250
|
|
);
|
|
// Hour
|
|
$this->Validator->isLength( 'hour',
|
|
$this->getHour(),
|
|
TTi18n::gettext( 'Hour is invalid' ),
|
|
1, 250
|
|
);
|
|
// Day of Month
|
|
$this->Validator->isLength( 'day_of_month',
|
|
$this->getDayOfMonth(),
|
|
TTi18n::gettext( 'Day of Month is invalid' ),
|
|
1, 250
|
|
);
|
|
// Month
|
|
$this->Validator->isLength( 'month',
|
|
$this->getMonth(),
|
|
TTi18n::gettext( 'Month is invalid' ),
|
|
1, 250
|
|
);
|
|
// Day of Week
|
|
$this->Validator->isLength( 'day_of_week',
|
|
$this->getDayOfWeek(),
|
|
TTi18n::gettext( 'Day of Week is invalid' ),
|
|
1, 250
|
|
);
|
|
// Command
|
|
$this->Validator->isLength( 'command',
|
|
$this->getCommand(),
|
|
TTi18n::gettext( 'Command is invalid' ),
|
|
1, 250
|
|
);
|
|
// last run
|
|
if ( $this->getLastRunDate() !== false ) {
|
|
$this->Validator->isDate( 'last_run',
|
|
$this->getLastRunDate(),
|
|
TTi18n::gettext( 'Incorrect last run' )
|
|
);
|
|
}
|
|
//
|
|
// ABOVE: Validation code moved from set*() functions.
|
|
//
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
function preSave() {
|
|
if ( $this->getStatus() == '' ) {
|
|
$this->setStatus( 10 ); //Ready
|
|
}
|
|
|
|
if ( $this->getMinute() == '' ) {
|
|
$this->setMinute( '*' );
|
|
}
|
|
|
|
if ( $this->getHour() == '' ) {
|
|
$this->setHour( '*' );
|
|
}
|
|
|
|
if ( $this->getDayOfMonth() == '' ) {
|
|
$this->setDayOfMonth( '*' );
|
|
}
|
|
|
|
if ( $this->getMonth() == '' ) {
|
|
$this->setMonth( '*' );
|
|
}
|
|
|
|
if ( $this->getDayOfWeek() == '' ) {
|
|
$this->setDayOfWeek( '*' );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
function postSave() {
|
|
$this->removeCache( $this->getId() );
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param $log_action
|
|
* @return bool
|
|
*/
|
|
function addLog( $log_action ) {
|
|
if ( $this->getExecuteFlag() == false ) {
|
|
return TTLog::addEntry( $this->getId(), $log_action, TTi18n::getText( 'Cron Job' ), null, $this->getTable() );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
?>
|