artabro/wire/modules/LazyCron.module
2024-08-27 11:35:37 +02:00

297 lines
8.1 KiB
Text

<?php namespace ProcessWire;
/**
* ProcessWire LazyCron Module
* ===========================
*
* Provides hooks that are automatically executed at various intervals.
* It is called 'lazy' because it's triggered by a pageview, so it's accuracy
* executing at specific times will depend upon how may pageviews your site gets.
* So when a specified time is triggered, it's guaranteed to have been that length
* of time OR longer. This is fine for most cases.
* But here's how you make it NOT lazy:
*
* Setup a real CRON job to pull a page from your site once per minute.
* Here is an example of a command that you could schedule to execute once per
* minute:
*
* wget --quiet --no-cache -O - http://www.your-site.com > /dev/null
*
*
* USAGE IN YOUR MODULES:
* ----------------------
*
* // In your own module or template, add the function you want executed:
* public function myFunc(HookEvent $e) { echo "30 Minutes have passed!"; }
*
* // Then add the hook to it in your module's init() function:
* $this->addHook('LazyCron::every30Minutes', $this, 'myFunc');
*
*
* PROCEDURAL USAGE (i.e. in templates):
* -------------------------------------
*
* // create your hook function
* function myHook(HookEvent $e) { echo "30 Minutes have passed!"; }
*
* // add a hook to it:
* $wire->addHook('LazyCron::every30Minutes', null, 'myFunc');
*
*
* FUNCTIONS YOU CAN HOOK:
* -----------------------
*
* every30Seconds
* everyMinute
* every2Minutes
* every3Minutes
* every4Minutes
* every5Minutes
* every10Minutes
* every15Minutes
* every30Minutes
* every45Minutes
* everyHour
* every2Hours
* every4Hours
* every6Hours
* every12Hours
* everyDay
* every2Days
* every4Days
* everyWeek
* every2Weeks
* every4Weeks
*
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
*
*/
class LazyCron extends WireData implements Module {
public static function getModuleInfo() {
return array(
'title' => 'Lazy Cron',
'version' => 103,
'summary' =>
"Provides hooks that are automatically executed at various intervals. " .
"It is called 'lazy' because it's triggered by a pageview, so the interval " .
"is guaranteed to be at least the time requested, rather than exactly the " .
"time requested. This is fine for most cases, but you can make it not lazy " .
"by connecting this to a real CRON job. See the module file for details. ",
'href' => 'https://processwire.com/api/modules/lazy-cron/',
'permanent' => false,
'singular' => true,
'autoload' => true,
);
}
/**
* Hookable time functions that we are allowing indexed by number of seconds.
*
*/
protected $timeFuncs = array(
30 => 'every30Seconds',
60 => 'everyMinute',
120 => 'every2Minutes',
180 => 'every3Minutes',
240 => 'every4Minutes',
300 => 'every5Minutes',
600 => 'every10Minutes',
900 => 'every15Minutes',
1800 => 'every30Minutes',
2700 => 'every45Minutes',
3600 => 'everyHour',
7200 => 'every2Hours',
14400 => 'every4Hours',
21600 => 'every6Hours',
43200 => 'every12Hours',
86400 => 'everyDay',
172800 => 'every2Days',
345600 => 'every4Days',
604800 => 'everyWeek',
1209600 => 'every2Weeks',
2419200 => 'every4Weeks',
);
/**
* @var string
*
*/
protected $lockfile = '';
/**
* Initialize the hooks
*
*/
public function init() {
$this->addHookAfter('ProcessPageView::finished', $this, 'afterPageView');
}
/**
* Function triggered after every page view.
*
* This is intentionally scheduled after the page has been delivered so
* that the cron jobs don't slow down the pageview.
*
* @param HookEvent $e
*
*/
public function afterPageView(HookEvent $e) {
/** @var ProcessPageView $process */
$process = $e->object;
// don't execute cron now if this is anything other than a normal response
$responseType = $process->getResponseType();
if($responseType != ProcessPageView::responseTypeNormal) return;
$files = $this->wire()->files;
$cachePath = $this->wire()->config->paths->cache;
$time = time();
$filename = $cachePath . 'LazyCron.cache';
$this->lockfile = $cachePath . 'LazyCronLock.cache';
$times = array();
$writeFile = false;
if(is_file($this->lockfile)) {
// other LazyCron process potentially running
if(filemtime($this->lockfile) < (time() - 3600)) {
// expired lock file, some fatal error must have occurred during last LazyCron run
$this->removeLockfile();
} else {
// skip running this time as an active lock file exists
return;
}
}
if(!$files->filePutContents($this->lockfile, (string) time(), LOCK_EX)) {
// $this->error("Unable to write lock file: $this->lockfile", Notice::logOnly);
return;
}
if(is_file($filename)) {
$filedata = file($filename, FILE_IGNORE_NEW_LINES);
// file is probably locked, so skip it this time
if($filedata === false) {
$this->removeLockfile();
return;
}
} else {
// file does not exist
$filedata = false;
}
$shutdown = $this->wire()->shutdown;
if($shutdown) $shutdown->addHook('fatalError', $this, 'hookFatalError');
if($filedata) {
$n = 0;
foreach($this->timeFuncs as $seconds => $func) {
$lasttime = (int) (isset($filedata[$n]) ? $filedata[$n] : $time);
$elapsedTime = $time - $lasttime;
if(empty($filedata[$n]) || $elapsedTime >= $seconds) {
try {
$this->$func($elapsedTime);
} catch(\Exception $e) {
$this->error($e->getMessage(), Notice::logOnly);
}
$lasttime = $time;
$writeFile = true;
}
$times[$seconds] = $lasttime;
$n++;
}
} else {
// file does not exist
$writeFile = true;
foreach($this->timeFuncs as $seconds => $func) {
try {
$this->$func(0);
} catch(\Exception $e) {
$this->error($e->getMessage(), Notice::logOnly);
}
$times[$seconds] = $time;
}
}
if($writeFile) {
$files->filePutContents($filename, implode("\n", $times), LOCK_EX);
}
$this->removeLockfile();
}
/**
* Remove lock file
*
*/
protected function removeLockfile() {
if(!$this->lockfile || !is_readable($this->lockfile)) return;
$files = $this->wire()->files;
if($files) {
$files->unlink($this->lockfile, false, false);
} else {
unlink($this->lockfile); // might be impossible to reach
}
}
/**
* Hook to WireShutdown::fatalError
*
* @param HookEvent $e
*
*/
public function hookFatalError(HookEvent $e) {
$this->removeLockfile();
$e->removeHook(null); // remove self
}
/**
* One or more of the following functions is called if the given interval has passed.
*
* You can hook into any of these functions and your hook will be called at the given interval.
*
* @param int $seconds The number of seconds that have actually elapsed. Most likely not useful, but provided just in case.
*
*/
public function ___every30Seconds($seconds) { }
public function ___everyMinute($seconds) { }
public function ___every2Minutes($seconds) { }
public function ___every3Minutes($seconds) { }
public function ___every4Minutes($seconds) { }
public function ___every5Minutes($seconds) { }
public function ___every10Minutes($seconds) { }
public function ___every15Minutes($seconds) { }
public function ___every30Minutes($seconds) { }
public function ___every45Minutes($seconds) { }
public function ___everyHour($seconds) { }
public function ___every2Hours($seconds) { }
public function ___every4Hours($seconds) { }
public function ___every6Hours($seconds) { }
public function ___every12Hours($seconds) { }
public function ___everyDay($seconds) { }
public function ___every2Days($seconds) { }
public function ___every4Days($seconds) { }
public function ___everyWeek($seconds) { }
public function ___every2Weeks($seconds) { }
public function ___every4Weeks($seconds) { }
/**
* Uninstall
*
*/
public function ___uninstall() {
$files = $this->wire()->files;
$cachePath = $this->wire()->config->paths->cache;
$file = $cachePath . 'LazyCron.cache';
if(is_file($file)) $files->unlink($file, true, false);
$file = $cachePath . 'LazyCronLock.cache';
if(is_file($file)) $files->unlink($file, true, false);
}
}