first commi

This commit is contained in:
2021-03-05 09:30:59 +01:00
commit bfa5e61d5e
853 changed files with 120974 additions and 0 deletions
+577
View File
@@ -0,0 +1,577 @@
<?php
/**
* @package Grav\Common\Scheduler
* @author Originally based on jqCron by Arnaud Buathier <arnaud@arnapou.net> modified for Grav integration
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Scheduler;
/*
* Usage examples :
* ----------------
*
* $cron = new Cron('10-30/5 12 * * *');
*
* var_dump($cron->getMinutes());
* // array(5) {
* // [0]=> int(10)
* // [1]=> int(15)
* // [2]=> int(20)
* // [3]=> int(25)
* // [4]=> int(30)
* // }
*
* var_dump($cron->getText('fr'));
* // string(32) "Chaque jour à 12:10,15,20,25,30"
*
* var_dump($cron->getText('en'));
* // string(30) "Every day at 12:10,15,20,25,30"
*
* var_dump($cron->getType());
* // string(3) "day"
*
* var_dump($cron->getCronHours());
* // string(2) "12"
*
* var_dump($cron->matchExact(new \DateTime('2012-07-01 13:25:10')));
* // bool(false)
*
* var_dump($cron->matchExact(new \DateTime('2012-07-01 12:15:20')));
* // bool(true)
*
* var_dump($cron->matchWithMargin(new \DateTime('2012-07-01 12:32:50'), -3, 5));
* // bool(true)
*/
use DateInterval;
use DateTime;
use RuntimeException;
use function count;
use function in_array;
use function is_array;
use function is_string;
class Cron
{
public const TYPE_UNDEFINED = '';
public const TYPE_MINUTE = 'minute';
public const TYPE_HOUR = 'hour';
public const TYPE_DAY = 'day';
public const TYPE_WEEK = 'week';
public const TYPE_MONTH = 'month';
public const TYPE_YEAR = 'year';
/**
*
* @var array
*/
protected $texts = [
'fr' => [
'empty' => '-tout-',
'name_minute' => 'minute',
'name_hour' => 'heure',
'name_day' => 'jour',
'name_week' => 'semaine',
'name_month' => 'mois',
'name_year' => 'année',
'text_period' => 'Chaque %s',
'text_mins' => 'à %s minutes',
'text_time' => 'à %02s:%02s',
'text_dow' => 'le %s',
'text_month' => 'de %s',
'text_dom' => 'le %s',
'weekdays' => ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'],
'months' => ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
],
'en' => [
'empty' => '-all-',
'name_minute' => 'minute',
'name_hour' => 'hour',
'name_day' => 'day',
'name_week' => 'week',
'name_month' => 'month',
'name_year' => 'year',
'text_period' => 'Every %s',
'text_mins' => 'at %s minutes past the hour',
'text_time' => 'at %02s:%02s',
'text_dow' => 'on %s',
'text_month' => 'of %s',
'text_dom' => 'on the %s',
'weekdays' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
'months' => ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'],
],
];
/**
* min hour dom month dow
* @var string
*/
protected $cron = '';
/**
*
* @var array
*/
protected $minutes = [];
/**
*
* @var array
*/
protected $hours = [];
/**
*
* @var array
*/
protected $months = [];
/**
* 0-7 : sunday, monday, ... saturday, sunday
* @var array
*/
protected $dow = [];
/**
*
* @var array
*/
protected $dom = [];
/**
* @param string|null $cron
*/
public function __construct($cron = null)
{
if (null !== $cron) {
$this->setCron($cron);
}
}
/**
* @return string
*/
public function getCron()
{
return implode(' ', [
$this->getCronMinutes(),
$this->getCronHours(),
$this->getCronDaysOfMonth(),
$this->getCronMonths(),
$this->getCronDaysOfWeek(),
]);
}
/**
* @param string $lang 'fr' or 'en'
* @return string
*/
public function getText($lang)
{
// check lang
if (!isset($this->texts[$lang])) {
return $this->getCron();
}
$texts = $this->texts[$lang];
// check type
$type = $this->getType();
if ($type === self::TYPE_UNDEFINED) {
return $this->getCron();
}
// init
$elements = [];
$elements[] = sprintf($texts['text_period'], $texts['name_' . $type]);
// hour
if ($type === self::TYPE_HOUR) {
$elements[] = sprintf($texts['text_mins'], $this->getCronMinutes());
}
// week
if ($type === self::TYPE_WEEK) {
$dow = $this->getCronDaysOfWeek();
foreach ($texts['weekdays'] as $i => $wd) {
$dow = str_replace((string) ($i + 1), $wd, $dow);
}
$elements[] = sprintf($texts['text_dow'], $dow);
}
// month + year
if (in_array($type, [self::TYPE_MONTH, self::TYPE_YEAR], true)) {
$elements[] = sprintf($texts['text_dom'], $this->getCronDaysOfMonth());
}
// year
if ($type === self::TYPE_YEAR) {
$months = $this->getCronMonths();
for ($i = count($texts['months']) - 1; $i >= 0; $i--) {
$months = str_replace((string) ($i + 1), $texts['months'][$i], $months);
}
$elements[] = sprintf($texts['text_month'], $months);
}
// day + week + month + year
if (in_array($type, [self::TYPE_DAY, self::TYPE_WEEK, self::TYPE_MONTH, self::TYPE_YEAR], true)) {
$elements[] = sprintf($texts['text_time'], $this->getCronHours(), $this->getCronMinutes());
}
return str_replace('*', $texts['empty'], implode(' ', $elements));
}
/**
* @return string
*/
public function getType()
{
$mask = preg_replace('/[^\* ]/', '-', $this->getCron());
$mask = preg_replace('/-+/', '-', $mask);
$mask = preg_replace('/[^-\*]/', '', $mask);
if ($mask === '*****') {
return self::TYPE_MINUTE;
}
if ($mask === '-****') {
return self::TYPE_HOUR;
}
if (substr($mask, -3) === '***') {
return self::TYPE_DAY;
}
if (substr($mask, -3) === '-**') {
return self::TYPE_MONTH;
}
if (substr($mask, -3) === '**-') {
return self::TYPE_WEEK;
}
if (substr($mask, -2) === '-*') {
return self::TYPE_YEAR;
}
return self::TYPE_UNDEFINED;
}
/**
* @param string $cron
* @return $this
*/
public function setCron($cron)
{
// sanitize
$cron = trim($cron);
$cron = preg_replace('/\s+/', ' ', $cron);
// explode
$elements = explode(' ', $cron);
if (count($elements) !== 5) {
throw new RuntimeException('Bad number of elements');
}
$this->cron = $cron;
$this->setMinutes($elements[0]);
$this->setHours($elements[1]);
$this->setDaysOfMonth($elements[2]);
$this->setMonths($elements[3]);
$this->setDaysOfWeek($elements[4]);
return $this;
}
/**
* @return string
*/
public function getCronMinutes()
{
return $this->arrayToCron($this->minutes);
}
/**
* @return string
*/
public function getCronHours()
{
return $this->arrayToCron($this->hours);
}
/**
* @return string
*/
public function getCronDaysOfMonth()
{
return $this->arrayToCron($this->dom);
}
/**
* @return string
*/
public function getCronMonths()
{
return $this->arrayToCron($this->months);
}
/**
* @return string
*/
public function getCronDaysOfWeek()
{
return $this->arrayToCron($this->dow);
}
/**
* @return array
*/
public function getMinutes()
{
return $this->minutes;
}
/**
* @return array
*/
public function getHours()
{
return $this->hours;
}
/**
* @return array
*/
public function getDaysOfMonth()
{
return $this->dom;
}
/**
* @return array
*/
public function getMonths()
{
return $this->months;
}
/**
* @return array
*/
public function getDaysOfWeek()
{
return $this->dow;
}
/**
* @param string|string[] $minutes
* @return $this
*/
public function setMinutes($minutes)
{
$this->minutes = $this->cronToArray($minutes, 0, 59);
return $this;
}
/**
* @param string|string[] $hours
* @return $this
*/
public function setHours($hours)
{
$this->hours = $this->cronToArray($hours, 0, 23);
return $this;
}
/**
* @param string|string[] $months
* @return $this
*/
public function setMonths($months)
{
$this->months = $this->cronToArray($months, 1, 12);
return $this;
}
/**
* @param string|string[] $dow
* @return $this
*/
public function setDaysOfWeek($dow)
{
$this->dow = $this->cronToArray($dow, 0, 7);
return $this;
}
/**
* @param string|string[] $dom
* @return $this
*/
public function setDaysOfMonth($dom)
{
$this->dom = $this->cronToArray($dom, 1, 31);
return $this;
}
/**
* @param mixed $date
* @param int $min
* @param int $hour
* @param int $day
* @param int $month
* @param int $weekday
* @return DateTime
*/
protected function parseDate($date, &$min, &$hour, &$day, &$month, &$weekday)
{
if (is_numeric($date) && (int)$date == $date) {
$date = new DateTime('@' . $date);
} elseif (is_string($date)) {
$date = new DateTime('@' . strtotime($date));
}
if ($date instanceof DateTime) {
$min = (int)$date->format('i');
$hour = (int)$date->format('H');
$day = (int)$date->format('d');
$month = (int)$date->format('m');
$weekday = (int)$date->format('w'); // 0-6
} else {
throw new RuntimeException('Date format not supported');
}
return new DateTime($date->format('Y-m-d H:i:sP'));
}
/**
* @param int|string|DateTime $date
*/
public function matchExact($date)
{
$date = $this->parseDate($date, $min, $hour, $day, $month, $weekday);
return
(empty($this->minutes) || in_array($min, $this->minutes, true)) &&
(empty($this->hours) || in_array($hour, $this->hours, true)) &&
(empty($this->dom) || in_array($day, $this->dom, true)) &&
(empty($this->months) || in_array($month, $this->months, true)) &&
(empty($this->dow) || in_array($weekday, $this->dow, true) || ($weekday == 0 && in_array(7, $this->dow, true)) || ($weekday == 7 && in_array(0, $this->dow, true))
);
}
/**
* @param int|string|DateTime $date
* @param int $minuteBefore
* @param int $minuteAfter
*/
public function matchWithMargin($date, $minuteBefore = 0, $minuteAfter = 0)
{
if ($minuteBefore > 0) {
throw new RuntimeException('MinuteBefore parameter cannot be positive !');
}
if ($minuteAfter < 0) {
throw new RuntimeException('MinuteAfter parameter cannot be negative !');
}
$date = $this->parseDate($date, $min, $hour, $day, $month, $weekday);
$interval = new DateInterval('PT1M'); // 1 min
if ($minuteBefore !== 0) {
$date->sub(new DateInterval('PT' . abs($minuteBefore) . 'M'));
}
$n = $minuteAfter - $minuteBefore + 1;
for ($i = 0; $i < $n; $i++) {
if ($this->matchExact($date)) {
return true;
}
$date->add($interval);
}
return false;
}
/**
* @param array $array
* @return string
*/
protected function arrayToCron($array)
{
$n = count($array);
if (!is_array($array) || $n === 0) {
return '*';
}
$cron = [$array[0]];
$s = $c = $array[0];
for ($i = 1; $i < $n; $i++) {
if ($array[$i] == $c + 1) {
$c = $array[$i];
$cron[count($cron) - 1] = $s . '-' . $c;
} else {
$s = $c = $array[$i];
$cron[] = $c;
}
}
return implode(',', $cron);
}
/**
*
* @param array|string $string
* @param int $min
* @param int $max
* @return array
*/
protected function cronToArray($string, $min, $max)
{
$array = [];
if (is_array($string)) {
foreach ($string as $val) {
if (is_numeric($val) && (int)$val == $val && $val >= $min && $val <= $max) {
$array[] = (int)$val;
}
}
} elseif ($string !== '*') {
while ($string !== '') {
// test "*/n" expression
if (preg_match('/^\*\/([0-9]+),?/', $string, $m)) {
for ($i = max(0, $min); $i <= min(59, $max); $i += $m[1]) {
$array[] = (int)$i;
}
$string = substr($string, strlen($m[0]));
continue;
}
// test "a-b/n" expression
if (preg_match('/^([0-9]+)-([0-9]+)\/([0-9]+),?/', $string, $m)) {
for ($i = max($m[1], $min); $i <= min($m[2], $max); $i += $m[3]) {
$array[] = (int)$i;
}
$string = substr($string, strlen($m[0]));
continue;
}
// test "a-b" expression
if (preg_match('/^([0-9]+)-([0-9]+),?/', $string, $m)) {
for ($i = max($m[1], $min); $i <= min($m[2], $max); $i++) {
$array[] = (int)$i;
}
$string = substr($string, strlen($m[0]));
continue;
}
// test "c" expression
if (preg_match('/^([0-9]+),?/', $string, $m)) {
if ($m[1] >= $min && $m[1] <= $max) {
$array[] = (int)$m[1];
}
$string = substr($string, strlen($m[0]));
continue;
}
// something goes wrong in the expression
return [];
}
}
sort($array, SORT_NUMERIC);
return $array;
}
}
@@ -0,0 +1,404 @@
<?php
/**
* @package Grav\Common\Scheduler
* @author Originally based on peppeocchi/php-cron-scheduler modified for Grav integration
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Scheduler;
use Cron\CronExpression;
use InvalidArgumentException;
use function is_string;
/**
* Trait IntervalTrait
* @package Grav\Common\Scheduler
*/
trait IntervalTrait
{
/**
* Set the Job execution time.
*compo
* @param string $expression
* @return self
*/
public function at($expression)
{
$this->at = $expression;
$this->executionTime = CronExpression::factory($expression);
return $this;
}
/**
* Set the execution time to every minute.
*
* @return self
*/
public function everyMinute()
{
return $this->at('* * * * *');
}
/**
* Set the execution time to every hour.
*
* @param int|string $minute
* @return self
*/
public function hourly($minute = 0)
{
$c = $this->validateCronSequence($minute);
return $this->at("{$c['minute']} * * * *");
}
/**
* Set the execution time to once a day.
*
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function daily($hour = 0, $minute = 0)
{
if (is_string($hour)) {
$parts = explode(':', $hour);
$hour = $parts[0];
$minute = $parts[1] ?? '0';
}
$c = $this->validateCronSequence($minute, $hour);
return $this->at("{$c['minute']} {$c['hour']} * * *");
}
/**
* Set the execution time to once a week.
*
* @param int|string $weekday
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function weekly($weekday = 0, $hour = 0, $minute = 0)
{
if (is_string($hour)) {
$parts = explode(':', $hour);
$hour = $parts[0];
$minute = $parts[1] ?? '0';
}
$c = $this->validateCronSequence($minute, $hour, null, null, $weekday);
return $this->at("{$c['minute']} {$c['hour']} * * {$c['weekday']}");
}
/**
* Set the execution time to once a month.
*
* @param int|string $month
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function monthly($month = '*', $day = 1, $hour = 0, $minute = 0)
{
if (is_string($hour)) {
$parts = explode(':', $hour);
$hour = $parts[0];
$minute = $parts[1] ?? '0';
}
$c = $this->validateCronSequence($minute, $hour, $day, $month);
return $this->at("{$c['minute']} {$c['hour']} {$c['day']} {$c['month']} *");
}
/**
* Set the execution time to every Sunday.
*
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function sunday($hour = 0, $minute = 0)
{
return $this->weekly(0, $hour, $minute);
}
/**
* Set the execution time to every Monday.
*
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function monday($hour = 0, $minute = 0)
{
return $this->weekly(1, $hour, $minute);
}
/**
* Set the execution time to every Tuesday.
*
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function tuesday($hour = 0, $minute = 0)
{
return $this->weekly(2, $hour, $minute);
}
/**
* Set the execution time to every Wednesday.
*
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function wednesday($hour = 0, $minute = 0)
{
return $this->weekly(3, $hour, $minute);
}
/**
* Set the execution time to every Thursday.
*
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function thursday($hour = 0, $minute = 0)
{
return $this->weekly(4, $hour, $minute);
}
/**
* Set the execution time to every Friday.
*
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function friday($hour = 0, $minute = 0)
{
return $this->weekly(5, $hour, $minute);
}
/**
* Set the execution time to every Saturday.
*
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function saturday($hour = 0, $minute = 0)
{
return $this->weekly(6, $hour, $minute);
}
/**
* Set the execution time to every January.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function january($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(1, $day, $hour, $minute);
}
/**
* Set the execution time to every February.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function february($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(2, $day, $hour, $minute);
}
/**
* Set the execution time to every March.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function march($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(3, $day, $hour, $minute);
}
/**
* Set the execution time to every April.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function april($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(4, $day, $hour, $minute);
}
/**
* Set the execution time to every May.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function may($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(5, $day, $hour, $minute);
}
/**
* Set the execution time to every June.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function june($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(6, $day, $hour, $minute);
}
/**
* Set the execution time to every July.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function july($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(7, $day, $hour, $minute);
}
/**
* Set the execution time to every August.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function august($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(8, $day, $hour, $minute);
}
/**
* Set the execution time to every September.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function september($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(9, $day, $hour, $minute);
}
/**
* Set the execution time to every October.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function october($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(10, $day, $hour, $minute);
}
/**
* Set the execution time to every November.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function november($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(11, $day, $hour, $minute);
}
/**
* Set the execution time to every December.
*
* @param int|string $day
* @param int|string $hour
* @param int|string $minute
* @return self
*/
public function december($day = 1, $hour = 0, $minute = 0)
{
return $this->monthly(12, $day, $hour, $minute);
}
/**
* Validate sequence of cron expression.
*
* @param int|string|null $minute
* @param int|string|null $hour
* @param int|string|null $day
* @param int|string|null $month
* @param int|string|null $weekday
* @return array
*/
private function validateCronSequence($minute = null, $hour = null, $day = null, $month = null, $weekday = null)
{
return [
'minute' => $this->validateCronRange($minute, 0, 59),
'hour' => $this->validateCronRange($hour, 0, 23),
'day' => $this->validateCronRange($day, 1, 31),
'month' => $this->validateCronRange($month, 1, 12),
'weekday' => $this->validateCronRange($weekday, 0, 6),
];
}
/**
* Validate sequence of cron expression.
*
* @param int|string|null $value
* @param int $min
* @param int $max
* @return mixed
*/
private function validateCronRange($value, $min, $max)
{
if ($value === null || $value === '*') {
return '*';
}
if (! is_numeric($value) ||
! ($value >= $min && $value <= $max)
) {
throw new InvalidArgumentException(
"Invalid value: it should be '*' or between {$min} and {$max}."
);
}
return $value;
}
}
+564
View File
@@ -0,0 +1,564 @@
<?php
/**
* @package Grav\Common\Scheduler
* @author Originally based on peppeocchi/php-cron-scheduler modified for Grav integration
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Scheduler;
use Closure;
use Cron\CronExpression;
use DateTime;
use Grav\Common\Grav;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Component\Process\Process;
use function call_user_func;
use function call_user_func_array;
use function count;
use function is_array;
use function is_callable;
use function is_string;
/**
* Class Job
* @package Grav\Common\Scheduler
*/
class Job
{
use IntervalTrait;
/** @var string */
private $id;
/** @var bool */
private $enabled;
/** @var callable|string */
private $command;
/** @var string */
private $at;
/** @var array */
private $args = [];
/** @var bool */
private $runInBackground = true;
/** @var DateTime */
private $creationTime;
/** @var CronExpression */
private $executionTime;
/** @var string */
private $tempDir;
/** @var string */
private $lockFile;
/** @var bool */
private $truthTest = true;
/** @var string */
private $output;
/** @var int */
private $returnCode = 0;
/** @var array */
private $outputTo = [];
/** @var array */
private $emailTo = [];
/** @var array */
private $emailConfig = [];
/** @var callable|null */
private $before;
/** @var callable|null */
private $after;
/** @var callable */
private $whenOverlapping;
/** @var string */
private $outputMode;
/** @var Process|null $process */
private $process;
/** @var bool */
private $successful = false;
/** @var string|null */
private $backlink;
/**
* Create a new Job instance.
*
* @param string|callable $command
* @param array $args
* @param string|null $id
*/
public function __construct($command, $args = [], $id = null)
{
if (is_string($id)) {
$this->id = Grav::instance()['inflector']->hyphenize($id);
} else {
if (is_string($command)) {
$this->id = md5($command);
} else {
/* @var object $command */
$this->id = spl_object_hash($command);
}
}
$this->creationTime = new DateTime('now');
// initialize the directory path for lock files
$this->tempDir = sys_get_temp_dir();
$this->command = $command;
$this->args = $args;
// Set enabled state
$status = Grav::instance()['config']->get('scheduler.status');
$this->enabled = !(isset($status[$id]) && $status[$id] === 'disabled');
}
/**
* Get the command
*
* @return Closure|string
*/
public function getCommand()
{
return $this->command;
}
/**
* Get the cron 'at' syntax for this job
*
* @return string
*/
public function getAt()
{
return $this->at;
}
/**
* Get the status of this job
*
* @return bool
*/
public function getEnabled()
{
return $this->enabled;
}
/**
* Get optional arguments
*
* @return string|null
*/
public function getArguments()
{
if (is_string($this->args)) {
return $this->args;
}
return null;
}
/**
* @return CronExpression
*/
public function getCronExpression()
{
return CronExpression::factory($this->at);
}
/**
* Get the status of the last run for this job
*
* @return bool
*/
public function isSuccessful()
{
return $this->successful;
}
/**
* Get the Job id.
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Check if the Job is due to run.
* It accepts as input a DateTime used to check if
* the job is due. Defaults to job creation time.
* It also default the execution time if not previously defined.
*
* @param DateTime|null $date
* @return bool
*/
public function isDue(DateTime $date = null)
{
// The execution time is being defaulted if not defined
if (!$this->executionTime) {
$this->at('* * * * *');
}
$date = $date ?? $this->creationTime;
return $this->executionTime->isDue($date);
}
/**
* Check if the Job is overlapping.
*
* @return bool
*/
public function isOverlapping()
{
return $this->lockFile &&
file_exists($this->lockFile) &&
call_user_func($this->whenOverlapping, filemtime($this->lockFile)) === false;
}
/**
* Force the Job to run in foreground.
*
* @return $this
*/
public function inForeground()
{
$this->runInBackground = false;
return $this;
}
/**
* Sets/Gets an option backlink
*
* @param string|null $link
* @return string|null
*/
public function backlink($link = null)
{
if ($link) {
$this->backlink = $link;
}
return $this->backlink;
}
/**
* Check if the Job can run in background.
*
* @return bool
*/
public function runInBackground()
{
return !(is_callable($this->command) || $this->runInBackground === false);
}
/**
* This will prevent the Job from overlapping.
* It prevents another instance of the same Job of
* being executed if the previous is still running.
* The job id is used as a filename for the lock file.
*
* @param string|null $tempDir The directory path for the lock files
* @param callable|null $whenOverlapping A callback to ignore job overlapping
* @return self
*/
public function onlyOne($tempDir = null, callable $whenOverlapping = null)
{
if ($tempDir === null || !is_dir($tempDir)) {
$tempDir = $this->tempDir;
}
$this->lockFile = implode('/', [
trim($tempDir),
trim($this->id) . '.lock',
]);
if ($whenOverlapping) {
$this->whenOverlapping = $whenOverlapping;
} else {
$this->whenOverlapping = function () {
return false;
};
}
return $this;
}
/**
* Configure the job.
*
* @param array $config
* @return self
*/
public function configure(array $config = [])
{
// Check if config has defined a tempDir
if (isset($config['tempDir']) && is_dir($config['tempDir'])) {
$this->tempDir = $config['tempDir'];
}
return $this;
}
/**
* Truth test to define if the job should run if due.
*
* @param callable $fn
* @return self
*/
public function when(callable $fn)
{
$this->truthTest = $fn();
return $this;
}
/**
* Run the job.
*
* @return bool
*/
public function run()
{
// If the truthTest failed, don't run
if ($this->truthTest !== true) {
return false;
}
// If overlapping, don't run
if ($this->isOverlapping()) {
return false;
}
// Write lock file if necessary
$this->createLockFile();
// Call before if required
if (is_callable($this->before)) {
call_user_func($this->before);
}
// If command is callable...
if (is_callable($this->command)) {
$this->output = $this->exec();
} else {
$args = is_string($this->args) ? explode(' ', $this->args) : $this->args;
$command = array_merge([$this->command], $args);
$process = new Process($command);
$this->process = $process;
if ($this->runInBackground()) {
$process->start();
} else {
$process->run();
$this->finalize();
}
}
return true;
}
/**
* Finish up processing the job
*
* @return void
*/
public function finalize()
{
$process = $this->process;
if ($process) {
$process->wait();
if ($process->isSuccessful()) {
$this->successful = true;
$this->output = $process->getOutput();
} else {
$this->successful = false;
$this->output = $process->getErrorOutput();
}
$this->postRun();
unset($this->process);
}
}
/**
* Things to run after job has run
*
* @return void
*/
private function postRun()
{
if (count($this->outputTo) > 0) {
foreach ($this->outputTo as $file) {
$output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX;
file_put_contents($file, $this->output, $output_mode);
}
}
// Send output to email
$this->emailOutput();
// Call any callback defined
if (is_callable($this->after)) {
call_user_func($this->after, $this->output, $this->returnCode);
}
$this->removeLockFile();
}
/**
* Create the job lock file.
*
* @param mixed $content
* @return void
*/
private function createLockFile($content = null)
{
if ($this->lockFile) {
if ($content === null || !is_string($content)) {
$content = $this->getId();
}
file_put_contents($this->lockFile, $content);
}
}
/**
* Remove the job lock file.
*
* @return void
*/
private function removeLockFile()
{
if ($this->lockFile && file_exists($this->lockFile)) {
unlink($this->lockFile);
}
}
/**
* Execute a callable job.
*
* @return string
* @throws RuntimeException
*/
private function exec()
{
$return_data = '';
ob_start();
try {
$return_data = call_user_func_array($this->command, $this->args);
$this->successful = true;
} catch (RuntimeException $e) {
$return_data = $e->getMessage();
$this->successful = false;
}
$this->output = ob_get_clean() . (is_string($return_data) ? $return_data : '');
$this->postRun();
return $this->output;
}
/**
* Set the file/s where to write the output of the job.
*
* @param string|array $filename
* @param bool $append
* @return self
*/
public function output($filename, $append = false)
{
$this->outputTo = is_array($filename) ? $filename : [$filename];
$this->outputMode = $append === false ? 'overwrite' : 'append';
return $this;
}
/**
* Get the job output.
*
* @return mixed
*/
public function getOutput()
{
return $this->output;
}
/**
* Set the emails where the output should be sent to.
* The Job should be set to write output to a file
* for this to work.
*
* @param string|array $email
* @return self
*/
public function email($email)
{
if (!is_string($email) && !is_array($email)) {
throw new InvalidArgumentException('The email can be only string or array');
}
$this->emailTo = is_array($email) ? $email : [$email];
// Force the job to run in foreground
$this->inForeground();
return $this;
}
/**
* Email the output of the job, if any.
*
* @return bool
*/
private function emailOutput()
{
if (!count($this->outputTo) || !count($this->emailTo)) {
return false;
}
if (is_callable('Grav\Plugin\Email\Utils::sendEmail')) {
$subject ='Grav Scheduled Job [' . $this->getId() . ']';
$content = "<h1>Output from Job ID: {$this->getId()}</h1>\n<h4>Command: {$this->getCommand()}</h4><br /><pre style=\"font-size: 12px; font-family: Monaco, Consolas, monospace\">\n".$this->getOutput()."\n</pre>";
$to = $this->emailTo;
\Grav\Plugin\Email\Utils::sendEmail($subject, $content, $to);
}
return true;
}
/**
* Set function to be called before job execution
* Job object is injected as a parameter to callable function.
*
* @param callable $fn
* @return self
*/
public function before(callable $fn)
{
$this->before = $fn;
return $this;
}
/**
* Set a function to be called after job execution.
* By default this will force the job to run in foreground
* because the output is injected as a parameter of this
* function, but it could be avoided by passing true as a
* second parameter. The job will run in background if it
* meets all the other criteria.
*
* @param callable $fn
* @param bool $runInBackground
* @return self
*/
public function then(callable $fn, $runInBackground = false)
{
$this->after = $fn;
// Force the job to run in foreground
if ($runInBackground === false) {
$this->inForeground();
}
return $this;
}
}
@@ -0,0 +1,437 @@
<?php
/**
* @package Grav\Common\Scheduler
* @author Originally based on peppeocchi/php-cron-scheduler modified for Grav integration
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Scheduler;
use DateTime;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Utils;
use InvalidArgumentException;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
use RocketTheme\Toolbox\File\YamlFile;
use function is_callable;
use function is_string;
/**
* Class Scheduler
* @package Grav\Common\Scheduler
*/
class Scheduler
{
/** @var Job[] The queued jobs. */
private $jobs = [];
/** @var Job[] */
private $saved_jobs = [];
/** @var Job[] */
private $executed_jobs = [];
/** @var Job[] */
private $failed_jobs = [];
/** @var Job[] */
private $jobs_run = [];
/** @var array */
private $output_schedule = [];
/** @var array */
private $config;
/** @var string */
private $status_path;
/**
* Create new instance.
*/
public function __construct()
{
$config = Grav::instance()['config']->get('scheduler.defaults', []);
$this->config = $config;
$this->status_path = Grav::instance()['locator']->findResource('user-data://scheduler', true, true);
if (!file_exists($this->status_path)) {
Folder::create($this->status_path);
}
}
/**
* Load saved jobs from config/scheduler.yaml file
*
* @return $this
*/
public function loadSavedJobs()
{
$this->saved_jobs = [];
$saved_jobs = (array) Grav::instance()['config']->get('scheduler.custom_jobs', []);
foreach ($saved_jobs as $id => $j) {
$args = $j['args'] ?? [];
$id = Grav::instance()['inflector']->hyphenize($id);
$job = $this->addCommand($j['command'], $args, $id);
if (isset($j['at'])) {
$job->at($j['at']);
}
if (isset($j['output'])) {
$mode = isset($j['output_mode']) && $j['output_mode'] === 'append';
$job->output($j['output'], $mode);
}
if (isset($j['email'])) {
$job->email($j['email']);
}
// store in saved_jobs
$this->saved_jobs[] = $job;
}
return $this;
}
/**
* Get the queued jobs as background/foreground
*
* @param bool $all
* @return array
*/
public function getQueuedJobs($all = false)
{
$background = [];
$foreground = [];
foreach ($this->jobs as $job) {
if ($all || $job->getEnabled()) {
if ($job->runInBackground()) {
$background[] = $job;
} else {
$foreground[] = $job;
}
}
}
return [$background, $foreground];
}
/**
* Get all jobs if they are disabled or not as one array
*
* @return Job[]
*/
public function getAllJobs()
{
[$background, $foreground] = $this->loadSavedJobs()->getQueuedJobs(true);
return array_merge($background, $foreground);
}
/**
* Get a specific Job based on id
*
* @param string $jobid
* @return Job|null
*/
public function getJob($jobid)
{
$all = $this->getAllJobs();
foreach ($all as $job) {
if ($jobid == $job->getId()) {
return $job;
}
}
return null;
}
/**
* Queues a PHP function execution.
*
* @param callable $fn The function to execute
* @param array $args Optional arguments to pass to the php script
* @param string|null $id Optional custom identifier
* @return Job
*/
public function addFunction(callable $fn, $args = [], $id = null)
{
$job = new Job($fn, $args, $id);
$this->queueJob($job->configure($this->config));
return $job;
}
/**
* Queue a raw shell command.
*
* @param string $command The command to execute
* @param array $args Optional arguments to pass to the command
* @param string|null $id Optional custom identifier
* @return Job
*/
public function addCommand($command, $args = [], $id = null)
{
$job = new Job($command, $args, $id);
$this->queueJob($job->configure($this->config));
return $job;
}
/**
* Run the scheduler.
*
* @param DateTime|null $runTime Optional, run at specific moment
* @param bool $force force run even if not due
*/
public function run(DateTime $runTime = null, $force = false)
{
$this->loadSavedJobs();
[$background, $foreground] = $this->getQueuedJobs(false);
$alljobs = array_merge($background, $foreground);
if (null === $runTime) {
$runTime = new DateTime('now');
}
// Star processing jobs
foreach ($alljobs as $job) {
if ($job->isDue($runTime) || $force) {
$job->run();
$this->jobs_run[] = $job;
}
}
// Finish handling any background jobs
foreach ($background as $job) {
$job->finalize();
}
// Store states
$this->saveJobStates();
}
/**
* Reset all collected data of last run.
*
* Call before run() if you call run() multiple times.
*
* @return $this
*/
public function resetRun()
{
// Reset collected data of last run
$this->executed_jobs = [];
$this->failed_jobs = [];
$this->output_schedule = [];
return $this;
}
/**
* Get the scheduler verbose output.
*
* @param string $type Allowed: text, html, array
* @return string|array The return depends on the requested $type
*/
public function getVerboseOutput($type = 'text')
{
switch ($type) {
case 'text':
return implode("\n", $this->output_schedule);
case 'html':
return implode('<br>', $this->output_schedule);
case 'array':
return $this->output_schedule;
default:
throw new InvalidArgumentException('Invalid output type');
}
}
/**
* Remove all queued Jobs.
*
* @return $this
*/
public function clearJobs()
{
$this->jobs = [];
return $this;
}
/**
* Helper to get the full Cron command
*
* @return string
*/
public function getCronCommand()
{
$command = $this->getSchedulerCommand();
return "(crontab -l; echo \"* * * * * {$command} 1>> /dev/null 2>&1\") | crontab -";
}
/**
* @param string|null $php
* @return string
*/
public function getSchedulerCommand($php = null)
{
$phpBinaryFinder = new PhpExecutableFinder();
$php = $php ?? $phpBinaryFinder->find();
$command = 'cd ' . str_replace(' ', '\ ', GRAV_ROOT) . ';' . $php . ' bin/grav scheduler';
return $command;
}
/**
* Helper to determine if cron job is setup
* 0 - Crontab Not found
* 1 - Crontab Found
* 2 - Error
*
* @return int
*/
public function isCrontabSetup()
{
$process = new Process(['crontab', '-l']);
$process->run();
if ($process->isSuccessful()) {
$output = $process->getOutput();
$command = str_replace('/', '\/', $this->getSchedulerCommand('.*'));
$full_command = '/^(?!#).* .* .* .* .* ' . $command . '/m';
return preg_match($full_command, $output) ? 1 : 0;
}
$error = $process->getErrorOutput();
return Utils::startsWith($error, 'crontab: no crontab') ? 0 : 2;
}
/**
* Get the Job states file
*
* @return YamlFile
*/
public function getJobStates()
{
return YamlFile::instance($this->status_path . '/status.yaml');
}
/**
* Save job states to statys file
*
* @return void
*/
private function saveJobStates()
{
$now = time();
$new_states = [];
foreach ($this->jobs_run as $job) {
if ($job->isSuccessful()) {
$new_states[$job->getId()] = ['state' => 'success', 'last-run' => $now];
$this->pushExecutedJob($job);
} else {
$new_states[$job->getId()] = ['state' => 'failure', 'last-run' => $now, 'error' => $job->getOutput()];
$this->pushFailedJob($job);
}
}
$saved_states = $this->getJobStates();
$saved_states->save(array_merge($saved_states->content(), $new_states));
}
/**
* Try to determine who's running the process
*
* @return false|string
*/
public function whoami()
{
$process = new Process('whoami');
$process->run();
if ($process->isSuccessful()) {
return trim($process->getOutput());
}
return $process->getErrorOutput();
}
/**
* Queue a job for execution in the correct queue.
*
* @param Job $job
* @return void
*/
private function queueJob(Job $job)
{
$this->jobs[] = $job;
// Store jobs
}
/**
* Add an entry to the scheduler verbose output array.
*
* @param string $string
* @return void
*/
private function addSchedulerVerboseOutput($string)
{
$now = '[' . (new DateTime('now'))->format('c') . '] ';
$this->output_schedule[] = $now . $string;
// Print to stdoutput in light gray
// echo "\033[37m{$string}\033[0m\n";
}
/**
* Push a succesfully executed job.
*
* @param Job $job
* @return Job
*/
private function pushExecutedJob(Job $job)
{
$this->executed_jobs[] = $job;
$command = $job->getCommand();
$args = $job->getArguments();
// If callable, log the string Closure
if (is_callable($command)) {
$command = is_string($command) ? $command : 'Closure';
}
$this->addSchedulerVerboseOutput("<green>Success</green>: <white>{$command} {$args}</white>");
return $job;
}
/**
* Push a failed job.
*
* @param Job $job
* @return Job
*/
private function pushFailedJob(Job $job)
{
$this->failed_jobs[] = $job;
$command = $job->getCommand();
// If callable, log the string Closure
if (is_callable($command)) {
$command = is_string($command) ? $command : 'Closure';
}
$output = trim($job->getOutput());
$this->addSchedulerVerboseOutput("<red>Error</red>: <white>{$command}</white> → <normal>{$output}</normal>");
return $job;
}
}