Elgg  Version 6.1
Cron.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
10 use GO\Job;
11 use GO\Scheduler;
12 
18 class Cron {
19 
20  use Loggable;
21  use TimeUsing;
22 
23  protected const LOG_FILES_TO_KEEP = 5;
24 
25  protected array $default_intervals = [
26  'minute' => '* * * * *',
27  'fiveminute' => '*/5 * * * *',
28  'fifteenmin' => '*/15 * * * *',
29  'halfhour' => '*/30 * * * *',
30  'hourly' => '0 * * * *',
31  'daily' => '0 0 * * *',
32  'weekly' => '0 0 * * 0',
33  'monthly' => '0 0 1 * *',
34  'yearly' => '0 0 1 1 *',
35  ];
36 
43  public function __construct(protected EventsService $events, protected Translator $translator) {
44  }
45 
55  public function run(array $intervals = null, bool $force = false): array {
56  if (!isset($intervals)) {
57  $intervals = array_keys($this->default_intervals);
58  }
59 
60  $allowed_intervals = $this->getConfiguredIntervals();
61 
62  $scheduler = new Scheduler();
63  $time = $this->getCurrentTime();
64  $immutable = \DateTimeImmutable::createFromInterface($time);
65 
66  foreach ($intervals as $interval) {
67  if (!array_key_exists($interval, $allowed_intervals)) {
68  throw new CronException("{$interval} is not a recognized cron interval. Please use one of the following: " . implode(', ', array_keys($allowed_intervals)));
69  }
70 
71  $cron_interval = $force ? $allowed_intervals['minute'] : $allowed_intervals[$interval];
72  $filename = $this->getLogFilename($interval, $immutable);
73 
74  $cron_logger = \Elgg\Logger\Cron::factory([
75  'interval' => $interval,
76  'filename' => $filename,
77  ]);
78 
79  $scheduler
80  ->call(function () use ($interval, $immutable, $cron_logger, $filename) {
81  return $this->execute($interval, $cron_logger, $filename, $immutable);
82  })
83  ->at($cron_interval)
84  ->before(function () use ($interval, $immutable, $cron_logger) {
85  $this->before($interval, $cron_logger, $immutable);
86  })
87  ->then(function ($output) use ($interval, $cron_logger) {
88  $this->after($output, $interval, $cron_logger);
89  });
90  }
91 
92  return $scheduler->run($time);
93  }
94 
104  protected function before(string $interval, \Elgg\Logger\Cron $cron_logger, \DateTimeImmutable $time): void {
105  try {
106  $this->events->triggerBefore('cron', $interval, $time);
107  } catch (\Throwable $t) {
108  $this->getLogger()->error($t);
109  }
110 
111  // give every period at least 'max_execution_time' (PHP ini setting)
112  set_time_limit((int) ini_get('max_execution_time'));
113 
114  $now = new DateTime();
115 
116  $cron_logger->notice($this->translator->translate('admin:cron:started', [$interval, $time->format(DATE_RFC2822)]));
117  $cron_logger->notice($this->translator->translate('admin:cron:started:actual', [$interval, $now->format(DATE_RFC2822)]));
118  }
119 
130  protected function execute(string $interval, \Elgg\Logger\Cron $cron_logger, string $filename, \DateTimeImmutable $time): string {
131  try {
132  $begin_callback = function (array $params) use ($cron_logger) {
133  $readable_callable = (string) elgg_extract('readable_callable', $params);
134 
135  $cron_logger->notice("Starting {$readable_callable}");
136  };
137 
138  $end_callback = function (array $params) use ($cron_logger) {
139  $readable_callable = (string) elgg_extract('readable_callable', $params);
140 
141  $cron_logger->notice("Finished {$readable_callable}");
142  };
143 
144  // for BC this needs to be a triggerResults
145  $this->events->triggerResults('cron', $interval, [
146  'time' => $time->getTimestamp(),
147  'dt' => $time,
148  'logger' => $cron_logger,
149  ], null, [
150  EventsService::OPTION_BEGIN_CALLBACK => $begin_callback,
151  EventsService::OPTION_END_CALLBACK => $end_callback,
152  ]);
153  } catch (\Throwable $t) {
154  $this->getLogger()->error($t);
155  }
156 
157  $now = new DateTime();
158 
159  $complete = $this->translator->translate('admin:cron:complete', [$interval, $now->format(DATE_RFC2822)]);
160  $cron_logger->notice($complete);
161 
162  if (file_exists($filename) && is_readable($filename)) {
163  return file_get_contents($filename);
164  }
165 
166  return '';
167  }
168 
178  protected function after(string $output, string $interval, \Elgg\Logger\Cron $cron_logger): void {
179  $this->getLogger()->info($output);
180 
181  try {
182  $this->events->triggerAfter('cron', $interval, new \DateTime());
183  } catch (\Throwable $t) {
184  $this->getLogger()->error($t);
185  }
186 
187  $cron_logger->close();
188  $this->rotateLogs($interval);
189  $this->logCompletion($interval);
190  }
191 
202  public function getLogs(string $interval, bool $filenames_only = false): array {
203  $fh = new \ElggFile();
204  $fh->owner_guid = elgg_get_site_entity()->guid;
205  $fh->setFilename("cron/{$interval}/dummy.log");
206 
207  $dir = pathinfo($fh->getFilenameOnFilestore(), PATHINFO_DIRNAME);
208  if (!is_dir($dir) || !is_readable($dir)) {
209  return [];
210  }
211 
212  $dh = new \DirectoryIterator($dir);
213  $files = [];
214  /* @var $file \DirectoryIterator */
215  foreach ($dh as $file) {
216  if ($file->isDot() || !$file->isFile() || $file->getExtension() !== 'log') {
217  continue;
218  }
219 
220  if ($filenames_only) {
221  $files[] = $file->getFilename();
222  } else {
223  $files[$file->getFilename()] = file_get_contents($file->getPathname());
224  }
225  }
226 
227  if ($filenames_only) {
228  natcasesort($files);
229  } else {
230  uksort($files, 'strnatcasecmp');
231  }
232 
233  return array_reverse($files);
234  }
235 
243  public function getLastCompletion(string $interval): ?DateTime {
244  $fh = new \ElggFile();
245  $fh->owner_guid = elgg_get_site_entity()->guid;
246  $fh->setFilename("cron/{$interval}.complete");
247 
248  if (!$fh->exists()) {
249  return null;
250  }
251 
252  $date = $fh->grabFile();
253  if (empty($date)) {
254  // how??
255  return null;
256  }
257 
258  try {
259  return Values::normalizeTime($date);
260  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
261  $this->getLogger()->warning($e);
262  }
263 
264  return null;
265  }
266 
275  public function getConfiguredIntervals(bool $only_names = false): array {
276  $result = $this->events->triggerResults('cron:intervals', 'system', [], $this->default_intervals);
277  if (!is_array($result)) {
278  $this->getLogger()->warning("The event 'cron:intervals', 'system' should return an array, " . gettype($result) . ' given');
279 
280  $result = $this->default_intervals;
281  }
282 
283  if ($only_names) {
284  return array_keys($result);
285  }
286 
287  return $result;
288  }
289 
298  protected function getLogFilename(string $interval, \DateTimeInterface $time): string {
299  $date = $time->format(\DateTimeInterface::ATOM);
300  $date = str_replace('+', 'p', $date);
301  $date = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $date);
302 
303  $fh = new \ElggFile();
304  $fh->owner_guid = elgg_get_site_entity()->guid;
305  $fh->setFilename("cron/{$interval}/{$date}.log");
306 
307  return $fh->getFilenameOnFilestore();
308  }
309 
317  protected function rotateLogs(string $interval): void {
318  $files = $this->getLogs($interval, true);
319  if (count($files) <= self::LOG_FILES_TO_KEEP) {
320  return;
321  }
322 
323  $fh = new \ElggFile();
324  $fh->owner_guid = elgg_get_site_entity()->guid;
325 
326  while (count($files) > self::LOG_FILES_TO_KEEP) {
327  $filename = array_pop($files);
328 
329  $fh->setFilename("cron/{$interval}/{$filename}");
330  $fh->delete();
331  }
332  }
333 
341  protected function logCompletion(string $interval): void {
342  $fh = new \ElggFile();
343  $fh->owner_guid = elgg_get_site_entity()->guid;
344  $fh->setFilename("cron/{$interval}.complete");
345 
346  try {
347  if ($fh->open('write') === false) {
348  return;
349  }
350  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
351  $this->getLogger()->warning($e);
352  return;
353  }
354 
355  $now = new DateTime();
356  $fh->write($now->format(\DateTimeInterface::ATOM));
357  $fh->close();
358  }
359 }
$params
Saves global plugin settings.
Definition: save.php:13
array __construct(protected EventsService $events, protected Translator $translator)
Constructor.
Definition: Cron.php:43
getConfiguredIntervals(bool $only_names=false)
Get the cron interval configuration.
Definition: Cron.php:275
c Accompany it with the information you received as to the offer to distribute corresponding source complete source code means all the source code for all modules it plus any associated interface definition plus the scripts used to control compilation and installation of the executable as a special the source code distributed need not include anything that is normally and so on of the operating system on which the executable unless that component itself accompanies the executable If distribution of executable or object code is made by offering access to copy from a designated then offering equivalent access to copy the source code from the same place counts as distribution of the source even though third parties are not compelled to copy the source along with the object code You may not or distribute the Program except as expressly provided under this License Any attempt otherwise to sublicense or distribute the Program is void
Definition: LICENSE.txt:215
if(!$annotation instanceof ElggAnnotation) $time
Definition: time.php:20
Events service.
trait TimeUsing
Adds methods for setting the current time (for testing)
Definition: TimeUsing.php:10
Generic interface which allows catching of all exceptions thrown in Elgg.
getLastCompletion(string $interval)
Get the time of the last completion of a cron interval.
Definition: Cron.php:243
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:256
getCurrentTime($modifier= '')
Get the (cloned) time.
Definition: TimeUsing.php:25
rotateLogs(string $interval)
Rotate the log files.
Definition: Cron.php:317
Cron.
Definition: Cron.php:18
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
Logger.
Definition: Logger.php:26
getLogs(string $interval, bool $filenames_only=false)
Get the log files for a given cron interval.
Definition: Cron.php:202
execute(string $interval,\Elgg\Logger\Cron $cron_logger, string $filename,\DateTimeImmutable $time)
Execute handlers attached to a specific cron interval.
Definition: Cron.php:130
logCompletion(string $interval)
Log the completion time of a cron interval.
Definition: Cron.php:341
after(string $output, string $interval,\Elgg\Logger\Cron $cron_logger)
Printers handler result.
Definition: Cron.php:178
elgg_get_site_entity()
Get the current site entity.
Definition: entities.php:101
getLogFilename(string $interval,\DateTimeInterface $time)
Get a filename to log in.
Definition: Cron.php:298
getLogger()
Returns logger.
Definition: Loggable.php:37
Extension of the DateTime class to support formatting a date using the locale.
Definition: DateTime.php:12
A generic parent class for cron exceptions.
$output
Definition: download.php:9
before(string $interval,\Elgg\Logger\Cron $cron_logger,\DateTimeImmutable $time)
Execute commands before cron interval is run.
Definition: Cron.php:104
run(array $intervals=null, bool $force=false)
Executes handlers for periods that have elapsed since last cron.
Definition: Cron.php:55