TimeTrex/classes/modules/core/LockFile.class.php

276 lines
10 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 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;
}
}
?>