lock_file_prefix; } /** * @param $prefix * @return bool */ function setLockFilePrefix( $prefix ) { if ( $prefix != '' ) { $this->lock_file_prefix = $prefix; return true; } return false; } /** * @return string */ function getLockFileDirectory() { return $this->lock_file_dir; } /** * @param $dir * @return bool */ function setLockFileDirectory( $dir ) { if ( $dir != '' ) { if ( file_exists( $dir ) == false ) { @mkdir( $dir, 0775, true ); } if ( is_writable( $dir ) ) { $this->lock_file_dir = $dir; } return true; } return false; } /** * @return int */ function getMaxProcesses() { return $this->max_processes; } /** * @param $int * @return bool */ function setMaxProcesses( $int ) { $int = (int)$int; if ( $int <= 0 ) { $int = 1; } $this->max_processes = $int; return true; } /** * @param $lock_files * @return int */ function getCurrentProcesses( $lock_files ) { if ( is_array( $lock_files ) ) { $retval = count( $lock_files ); } else { $retval = 0; } //Debug::Text(' Current Running Processes: '. $retval, __FILE__, __LINE__, __METHOD__, 10); return $retval; } /** * @param bool $include_dir * @return string */ function getBaseLockFileName( $include_dir = false ) { if ( $include_dir == true ) { $retval = $this->getLockFileDirectory() . DIRECTORY_SEPARATOR; } else { $retval = ''; } $retval .= $this->getLockFilePrefix() . $this->lock_file_postfix; //Debug::Text(' Base Lock File Name: '. $retval, __FILE__, __LINE__, __METHOD__, 10); return $retval; } /** * @param $lock_files * @return string */ function getNextLockFileName( $lock_files ) { //Lock file name example: .lock. // ie: timeclocksync.lock.00001 $next_process_number = 1; if ( is_array( $lock_files ) ) { sort( $lock_files ); //Process them all in order, so we can quickly determine if there are any gaps in the numbers. $file_counter = 1; foreach ( $lock_files as $lock_file ) { if ( preg_match( '/' . $this->getLockFilePrefix() . '\.lock\.([0-9]{1,' . $this->process_number_digits . '})/i', $lock_file, $matches ) ) { if ( isset( $matches[0] ) && isset( $matches[1] ) && $matches[1] != '' ) { if ( (int)$matches[1] > $file_counter ) { //Found gap in the list, return the current file counter. $next_process_number = $file_counter; break; } else { //No gap in the list yet, use the file_counter + 1. $next_process_number = ( $file_counter + 1 ); } } $file_counter++; } } } //Pad process number to proper digits $next_process_number = str_pad( $next_process_number, $this->process_number_digits, '0', STR_PAD_LEFT ); $retval = $this->getBaseLockFileName( true ) . '.' . $next_process_number; Debug::Text(' Next Lock File Name: '. $retval .' Total Lock Files: '. count( (array)$lock_files ), __FILE__, __LINE__, __METHOD__, 10); return $retval; } /** * Delete any lock files older then max age, incase they are stale. * @param $lock_files * @return bool */ function purgeLockFiles( $lock_files, $check_pids = false ) { clearstatcache(); if ( is_array( $lock_files ) && count( $lock_files ) > 0 ) { $current_epoch = time(); $found_own_pid = false; foreach ( $lock_files as $key => $lock_file ) { if ( $check_pids == true ) { $lf = new LockFile( $lock_file ); $pid = $lf->readPIDFile( $lock_file ); if ( $lf->getCurrentPID() == $pid ) { Debug::Text( ' Not purging own lock file: ' . $lock_file .' PID: '. $pid, __FILE__, __LINE__, __METHOD__, 10 ); $found_own_pid = true; } else if ( $lf->isPIDRunning( $pid ) == false ) { Debug::Text( ' Purging stale lock file: ' . $lock_file .' PID: '. $pid, __FILE__, __LINE__, __METHOD__, 10 ); Misc::unlink( $lock_file ); unset( $lock_files[$key] ); //Remove from current lock file list. } else { if ( ( $pid == '~STARTING' && ( $current_epoch - @filemtime( $lock_file ) ) > 300 ) ) { //If lock file is in "STARTING" state for more than 5 minutes, consider it stale. Debug::Text( ' Purging stale lock file in STARTING state: ' . $lock_file .' PID: '. $pid, __FILE__, __LINE__, __METHOD__, 10 ); Misc::unlink( $lock_file ); unset( $lock_files[$key] ); //Remove from current lock file list. } else { Debug::Text( ' Not purging active lock file: ' . $lock_file . ' PID: ' . $pid, __FILE__, __LINE__, __METHOD__, 10 ); } } } else if ( file_exists( $lock_file ) && ( $current_epoch - @filemtime( $lock_file ) ) > $this->max_lock_file_age && @is_writable( $lock_file ) ) { Debug::Text( ' Purging stale lock file based on age: ' . $lock_file, __FILE__, __LINE__, __METHOD__, 10 ); Misc::unlink( $lock_file ); unset( $lock_files[$key] ); //Remove from current lock file list. } } if ( count( $lock_files ) > 0 && $check_pids == true && $found_own_pid == false ) { Debug::Text( ' NOTICE! Unable to find our own PID in lock files! ', __FILE__, __LINE__, __METHOD__, 10 ); } } return $lock_files; } /** * @return array|bool */ function getLockFiles( $purge_lock_files = true ) { $retarr = []; $start_dir = $this->getLockFileDirectory(); //Use glob() instead of Misc::getFileList() with a regex, because if there many files in the directory this is substantially faster. foreach ( glob( $start_dir . DIRECTORY_SEPARATOR . '*.lock.*') as $file_name ) { $retarr[] = $file_name; } //$regex_filter = $this->getLockFilePrefix() . '\.lock\..*'; //$retarr = Misc::getFileList( $start_dir, $regex_filter, false ); //Debug::Arr($retarr, ' Existing Lock Files: ', __FILE__, __LINE__, __METHOD__, 10); if ( $purge_lock_files == true ) { $retarr = $this->purgeLockFiles( $retarr, true ); } return $retarr; } /** * @param $cmd * @return bool */ function BackgroundExec( $cmd ) { if ( PHP_OS == 'WINNT' ) { //Windows global $config_vars; if ( strpos( $config_vars['path']['php_cli'], ' ' ) === false ) { //No space found in command, can run in background. //Unfortunately start.exe won't run a command with quotes around it, so we can't reliably run in the background without some extra //helper scripts, as TimeTrex could be installed in a directory which contains a space. //Remove quotes from command as "start.exe" fails to run if they exist. $full_command = str_replace( '"', '', 'start /B ' . $cmd ); Debug::Text( ' Executing Command in Background: ' . $full_command, __FILE__, __LINE__, __METHOD__, 10 ); // Sometimes Windows complains of an invalid argument, but we haven't been able to replicate it. So silence PHP warnings for now. $fh = @popen( $full_command, 'r' ); if ( is_resource( $fh ) ) { pclose( $fh ); } else { Debug::Text( ' NOTICE: Executing Command in Background failed or did not return a resource.', __FILE__, __LINE__, __METHOD__, 10 ); } unset( $fh ); } else { Debug::Text( ' Executing Command in Foreground: ' . $cmd, __FILE__, __LINE__, __METHOD__, 10 ); exec( $cmd ); } } else { //Linux/Unix //exec($cmd . ' 2>&1> /dev/null &'); exec( $cmd . ' > /dev/null &' ); } return true; } /** * @param $cmd * @param $next_lock_file_name * @return mixed */ function ReplaceCommandVariables( $cmd, $next_lock_file_name ) { $search_array = [ '#lock_file#', ]; $replace_array = [ $next_lock_file_name, ]; $retval = str_replace( $search_array, $replace_array, $cmd ); //Debug::Text(' Before: '. $cmd, __FILE__, __LINE__, __METHOD__, 10); //Debug::Text(' After: '. $retval, __FILE__, __LINE__, __METHOD__, 10); return $retval; } /** * @param $cmd * @return bool */ function run( $cmd, $skip_on_max_processes = false ) { //Check to see how many lock files with the prefix exist already. $timeout_start = time(); while ( ( time() - $timeout_start ) <= $this->max_process_check_timeout ) { //Count the processes (lock files) outside of a global lock first for performance reasons. Only if we are less than the total processes do we create the global lock and check again. $lock_files = $this->getLockFiles(); $current_processes = $this->getCurrentProcesses( $lock_files ); //Debug::Text(' Attempting to run command... Current Processes: '. $current_processes, __FILE__, __LINE__, __METHOD__, 10); if ( $current_processes < $this->getMaxProcesses() ) { //Debug::Text( ' Less than max processes running, checking again with locking... Current: '. $current_processes .' Max: '. $this->getMaxProcesses(), __FILE__, __LINE__, __METHOD__, 10 ); $global_lock_file = new LockFile( $this->getBaseLockFileName( true ).'global' ); if ( $global_lock_file->exists() == false ) { if ( $global_lock_file->create( false ) == true ) { //This should also be called as soon as the background process starts so its written with the proper PID. //Getting current number of processes needs to be wrapped in its own lock file, otherwise duplicates can be created. $lock_files = $this->getLockFiles(); $current_processes = $this->getCurrentProcesses( $lock_files ); //Debug::Text(' Attempting to run command... Current Processes: '. $current_current_processes, __FILE__, __LINE__, __METHOD__, 10); if ( $current_processes < $this->getMaxProcesses() ) { $next_lock_file_name = $this->getNextLockFileName( $lock_files ); $cmd = $this->ReplaceCommandVariables( $cmd, $next_lock_file_name ); Debug::Text( ' Running Command: ' . $cmd . ' Next Lock File Name: ' . $next_lock_file_name .' Processes: Current: '. $current_processes .' Max: '. $this->getMaxProcesses(), __FILE__, __LINE__, __METHOD__, 10 ); //Initialize the lock file before executing the command, so its there instantly and avoids race conditions allowing more processes to exist than the limit is. $lock_file = new LockFile( $next_lock_file_name ); $lock_file->create( true ); //Initialize only as we don't know the PID of the background process just yet, that will be written to the file once the process starts itself. unset( $lock_file ); //Run command $this->BackgroundExec( $cmd ); $global_lock_file->delete(); //Once the child process is spawned, then we can delete the global lock file. //Rather than sleep immediately waiting for the lock file to be created, keep checking for it immediately for the first 0.25 seconds so we can find it as soon as possible. // This greatly speeds up short running processes. $file_exists_start_time = microtime( true ); $i = 0; while ( true ) { if ( file_exists( $next_lock_file_name ) == true ) { Debug::Text( ' Lock file was created in: ' . ( microtime( true ) - $file_exists_start_time ) . ' returning...', __FILE__, __LINE__, __METHOD__, 10 ); break; } else { $elapsed_file_exists_time = ( microtime( true ) - $file_exists_start_time ); if ( $elapsed_file_exists_time > 2 ) { Debug::Text( 'I: ' . $i . ' Lock file not created within 2 seconds... File Name: ' . $next_lock_file_name, __FILE__, __LINE__, __METHOD__, 10 ); break; } else if ( $elapsed_file_exists_time > 0.25 ) { Debug::Text( 'I: ' . $i . ' Waiting for lock file to be created... File Name: ' . $next_lock_file_name, __FILE__, __LINE__, __METHOD__, 10 ); usleep( 250000 ); //.25 seconds } //else { // usleep( 10 ); //This prevents the main process from spending all its CPU cycles checking file_exists() sucking up about 80% CPU on its own process. //} $i++; } } return true; } else { $global_lock_file->delete(); if ( $skip_on_max_processes == true ) { Debug::Text( ' Max processes reached (b), not running command... Running: '. $current_processes, __FILE__, __LINE__, __METHOD__, 10 ); return false; } else { Debug::Text( ' Too many processes (b) already running (' . $current_processes . '), sleeping for: ' . $this->max_process_check_sleep . ' before next check...', __FILE__, __LINE__, __METHOD__, 10 ); sleep( $this->max_process_check_sleep ); } } } else { //Debug::Text( ' Waiting for process counter to finish (a)...', __FILE__, __LINE__, __METHOD__, 10 ); usleep( 250000 ); //.25 seconds } } else { //Debug::Text( ' Waiting for process counter to finish (b)...', __FILE__, __LINE__, __METHOD__, 10 ); usleep( 250000 ); //.25 seconds } } else { if ( $skip_on_max_processes == true ) { Debug::Text( ' Max processes reached (a), not running command... Running: '. $current_processes, __FILE__, __LINE__, __METHOD__, 10 ); return false; } else { Debug::Text( ' Too many processes (a) already running (' . $current_processes . '), sleeping for: ' . $this->max_process_check_sleep . ' before next check...', __FILE__, __LINE__, __METHOD__, 10 ); sleep( $this->max_process_check_sleep ); } } } Debug::Text( ' Timeout waiting for spot in process pool to open up.', __FILE__, __LINE__, __METHOD__, 10 ); return false; } /** * Waits for all background processes to finish. * @param int $timeout seconds. * @return bool */ function wait( $timeout = 300 ) { $timeout_start = time(); while ( ( time() - $timeout_start ) <= $timeout ) { $current_processes = $this->getCurrentProcesses( $this->getLockFiles() ); if ( $current_processes == 0 ) { Debug::Text( ' All background processes are completed!', __FILE__, __LINE__, __METHOD__, 10 ); return true; } else { Debug::Text( ' Waiting for: '. $current_processes .' background processes to be completed!', __FILE__, __LINE__, __METHOD__, 10 ); usleep( 25000 ); //0.25 seconds } } return false; } } ?>