Subversion Repositories web.active

Rev

Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download

<?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);
  }

}