<?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 Core */ class LockFile { var $file_name = null; var $max_lock_file_age = 86400; var $use_pid = true; /** * LockFile constructor. * @param $file_name */ function __construct( $file_name ) { $this->file_name = $file_name; return true; } /** * @return null */ function getFileName() { return $this->file_name; } /** * @param $file_name * @return bool */ function setFileName( $file_name ) { if ( $file_name != '' ) { $this->file_name = $file_name; return true; } return false; } /** * @return bool|int */ function getCurrentPID() { if ( $this->use_pid == true && function_exists( 'getmypid' ) == true ) { $retval = getmypid(); //Debug::Text( 'Current PID: ' . $retval, __FILE__, __LINE__, __METHOD__, 10 ); return $retval; } return false; } /** * @param int|string $pid Process ID * @return bool|null */ function isPIDRunning( $pid ) { if ( $pid == '~STARTING' ) { //Used in create() Debug::Text( 'PID is ~STARTING, assume its running: ' . $pid, __FILE__, __LINE__, __METHOD__, 10 ); return true; } else if ( $this->use_pid == true && (int)$pid > 0 && function_exists( 'posix_getpgid' ) == true ) { if ( posix_getpgid( $pid ) === false ) { Debug::Text( ' PID: '. $pid .' is NOT running!', __FILE__, __LINE__, __METHOD__, 10 ); return false; } else { Debug::Text( ' PID: '. $pid .' IS running!', __FILE__, __LINE__, __METHOD__, 10 ); return true; } } else { if ( trim( $pid ) == '' ) { Debug::Text( 'PID is blank, assume its NOT running: ' . $pid, __FILE__, __LINE__, __METHOD__, 10 ); return false; } else { if ( OPERATING_SYSTEM == 'WIN' ) { //Debug::Text( 'Checking if PID is running on Windows: ' . $pid, __FILE__, __LINE__, __METHOD__, 10 ); //Sometimes Windows can return: shell_exec(): Unable to execute 'tasklist.exe /FI "PID eq 13564" /FO CSV' // Not sure why, but silence the warning for now. $processes = array_map( 'str_getcsv', explode( "\n", @shell_exec( 'tasklist.exe /FI "PID eq ' . $pid . '" /FO CSV' ) ) ); //Filter tasklist to return just the PID we are looking for in CSV format. array_shift( $processes ); //Strip the first (header) off the array. if ( is_array( $processes ) ) { foreach ( $processes as $process ) { if ( isset( $process[1] ) && (int)$process[1] == (int)$pid ) { //PID Debug::Text( ' PID IS running: ' . $pid, __FILE__, __LINE__, __METHOD__, 10 ); return true; } } Debug::Text( ' PID is NOT running: ' . $pid, __FILE__, __LINE__, __METHOD__, 10 ); return false; } else { Debug::Text( 'Unable to get process list...', __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::Text( ' ERROR: Unable to determine if PID is running... PID: ' . $pid, __FILE__, __LINE__, __METHOD__, 10 ); } } } return null; //Assuming the process is still running if the file exists and PID is invalid. } /** * @return bool|int */ function create( $initialize = false ) { //Attempt to create directory if it does not already exist. $file_name = $this->getFileName(); $dir = dirname( $file_name ); if ( file_exists( $dir ) == false ) { $mkdir_result = @mkdir( $dir, 0777, true ); //ugo+rwx if ( $mkdir_result == false ) { Debug::Text( 'ERROR: Unable to create lock file directory: ' . $dir, __FILE__, __LINE__, __METHOD__, 10 ); } else { Debug::Text( 'WARNING: Created lock file directory as it didnt exist: ' . $dir, __FILE__, __LINE__, __METHOD__, 10 ); } } $current_pid = $this->getCurrentPID(); //Write current PID to file, so we can check if its still running later on. $lock_file_pid = $this->readPIDFile( $file_name ); if ( ( $initialize == true && $lock_file_pid === false ) || ( $initialize == false && ( $lock_file_pid == '~STARTING' || $lock_file_pid === false ) ) ) { //Write file with locking, this prevents duplicate lock files with the same name from being created. $fp = @fopen( $file_name, 'wb'); if ( $fp ) { Debug::Text( ' Creating Lock File: ' . $file_name .' Initialize: '. (int)$initialize .' Existing Lock File PID: '. $lock_file_pid .' Current PID: '. $current_pid, __FILE__, __LINE__, __METHOD__, 10 ); @flock( $fp, LOCK_EX ); @chmod( $file_name, 0660 ); //ug+rw @fwrite( $fp, ( ( $initialize == true ) ? '~STARTING' : $current_pid ) ); // ~STARTING is used in isPIDRunning() @flock( $fp, LOCK_UN ); @fclose( $fp ); return true; } else { Debug::Text( ' ERROR: Unable to create Lock File: ' . $file_name .' Initialize: '. (int)$initialize, __FILE__, __LINE__, __METHOD__, 10 ); } } else { Debug::Text( ' ERROR: Unable to create Lock File: ' . $file_name .' already exists with PID: '. $lock_file_pid .'... Initialize: '. (int)$initialize, __FILE__, __LINE__, __METHOD__, 10 ); } return false; } /** * @return bool */ function delete( $check_own_pid = true ) { $current_pid = $this->getCurrentPID(); if ( file_exists( $this->getFileName() ) ) { if ( $check_own_pid == true ) { $lock_file_pid = $this->readPIDFile( $this->getFileName() ); if ( is_numeric( $lock_file_pid ) ) { if ( $current_pid != $lock_file_pid ) { Debug::Text( 'ERROR: Lock file is NOT our own, unable to delete... Lock File: ' . $this->getFileName() .' PID: '. $lock_file_pid .' Current PID: '. $current_pid, __FILE__, __LINE__, __METHOD__, 10 ); return false; } } else { Debug::Text( 'Lock file does not exist or is starting... Lock File: ' . $this->getFileName() .' PID: '. $lock_file_pid .' Current PID: '. $current_pid, __FILE__, __LINE__, __METHOD__, 10 ); } } Debug::Text( ' Deleting Lock File: ' . $this->getFileName() .' PID: '. $current_pid, __FILE__, __LINE__, __METHOD__, 10 ); return Misc::unlink( $this->getFileName() ); } else { Debug::text( ' WARNING: Failed to delete lock file, does not exist: ' . $this->getFileName() .' PID: '. $current_pid, __FILE__, __LINE__, __METHOD__, 10 ); } return false; } /** * @param $file_name * @return false|int */ function readPIDFile( $file_name ) { clearstatcache( true, $file_name ); if ( file_exists( $file_name ) ) { $lock_file_pid = @file_get_contents( $file_name ); if ( $lock_file_pid != '' ) { if ( $lock_file_pid != '~STARTING' ) { $lock_file_pid = (int)$lock_file_pid; } Debug::text( ' Lock file exists with PID: ' . $lock_file_pid . ' Lock File: ' . $file_name, __FILE__, __LINE__, __METHOD__, 10 ); return $lock_file_pid; } else { Debug::text( ' Lock file exists (or did) but does not contain a PID: ' . $lock_file_pid . ' Lock File: ' . $file_name, __FILE__, __LINE__, __METHOD__, 10 ); } } return false; } /** * @return bool|null */ function exists() { //Ignore lock files older than max_lock_file_age, so if the server crashes or is rebooted during an operation, it will start again the next day. clearstatcache(); $lock_file_pid = $this->readPIDFile( $this->getFileName() ); if ( $lock_file_pid !== false ) { //Check to see if PID is still running or not. $pid_running = $this->isPIDRunning( $lock_file_pid ); if ( $pid_running !== null ) { //PID result is reliable, use it. if ( $pid_running === false ) { Debug::text( ' Stale (PID not running) lock file exists with PID: ' . $lock_file_pid .' Removing Lock File: '. $this->getFileName(), __FILE__, __LINE__, __METHOD__, 10 ); Misc::unlink( $this->getFileName() ); } else if ( ( $pid_running == '~STARTING' && ( time() - @filemtime( $this->getFileName() ) ) > 300 ) ) { //If lock file is in "STARTING" state for more than 5 minutes, consider it stale. Debug::text( ' Stale lock file exists in STARTING mode... PID: ' . $lock_file_pid .' Removing Lock File: '. $this->getFileName(), __FILE__, __LINE__, __METHOD__, 10 ); Misc::unlink( $this->getFileName() ); } return $pid_running; } else if ( ( time() - @filemtime( $this->getFileName() ) ) > $this->max_lock_file_age ) { //PID result may not be reliable, fall back to using file time instead. return true; } } return false; } } ?>