Elgg  Version master
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 
65  foreach ($intervals as $interval) {
66  if (!array_key_exists($interval, $allowed_intervals)) {
67  throw new CronException("{$interval} is not a recognized cron interval. Please use one of the following: " . implode(', ', array_keys($allowed_intervals)));
68  }
69 
70  $cron_interval = $force ? $allowed_intervals['minute'] : $allowed_intervals[$interval];
71  $filename = $this->getLogFilename($interval, $time);
72 
73  $cron_logger = \Elgg\Logger\Cron::factory([
74  'interval' => $interval,
75  'filename' => $filename,
76  ]);
77 
78  $scheduler
79  ->call(function () use ($interval, $time, $cron_logger, $filename) {
80  return $this->execute($interval, $cron_logger, $filename, $time);
81  })
82  ->at($cron_interval)
83  ->before(function () use ($interval, $time, $cron_logger) {
84  $this->before($interval, $cron_logger, $time);
85  })
86  ->then(function ($output) use ($interval, $cron_logger) {
87  $this->after($output, $interval, $cron_logger);
88  });
89  }
90 
91  return $scheduler->run($time);
92  }
93 
103  protected function before(string $interval, \Elgg\Logger\Cron $cron_logger, \DateTime $time = null): void {
104  if (!isset($time)) {
105  $time = $this->getCurrentTime();
106  }
107 
108  try {
109  $this->events->triggerBefore('cron', $interval, $time);
110  } catch (\Throwable $t) {
111  $this->getLogger()->error($t);
112  }
113 
114  // give every period at least 'max_execution_time' (PHP ini setting)
115  set_time_limit((int) ini_get('max_execution_time'));
116 
117  $now = new DateTime();
118 
119  $cron_logger->notice($this->translator->translate('admin:cron:started', [$interval, $time->format(DATE_RFC2822)]));
120  $cron_logger->notice($this->translator->translate('admin:cron:started:actual', [$interval, $now->format(DATE_RFC2822)]));
121  }
122 
133  protected function execute(string $interval, \Elgg\Logger\Cron $cron_logger, string $filename, \DateTime $time = null): string {
134  if (!isset($time)) {
135  $time = $this->getCurrentTime();
136  }
137 
138  try {
139  $begin_callback = function (array $params) use ($cron_logger) {
140  $readable_callable = (string) elgg_extract('readable_callable', $params);
141 
142  $cron_logger->notice("Starting {$readable_callable}");
143  };
144 
145  $end_callback = function (array $params) use ($cron_logger) {
146  $readable_callable = (string) elgg_extract('readable_callable', $params);
147 
148  $cron_logger->notice("Finished {$readable_callable}");
149  };
150 
151  // for BC this needs to be a triggerResults
152  $this->events->triggerResults('cron', $interval, [
153  'time' => $time->getTimestamp(),
154  'dt' => $time,
155  'logger' => $cron_logger,
156  ], null, [
157  EventsService::OPTION_BEGIN_CALLBACK => $begin_callback,
158  EventsService::OPTION_END_CALLBACK => $end_callback,
159  ]);
160  } catch (\Throwable $t) {
161  $this->getLogger()->error($t);
162  }
163 
164  $now = new DateTime();
165 
166  $complete = $this->translator->translate('admin:cron:complete', [$interval, $now->format(DATE_RFC2822)]);
167  $cron_logger->notice($complete);
168 
169  if (file_exists($filename) && is_readable($filename)) {
170  return file_get_contents($filename);
171  }
172 
173  return '';
174  }
175 
185  protected function after(string $output, string $interval, \Elgg\Logger\Cron $cron_logger): void {
186  $this->getLogger()->info($output);
187 
188  try {
189  $this->events->triggerAfter('cron', $interval, new \DateTime());
190  } catch (\Throwable $t) {
191  $this->getLogger()->error($t);
192  }
193 
194  $cron_logger->close();
195  $this->rotateLogs($interval);
196  $this->logCompletion($interval);
197  }
198 
209  public function getLogs(string $interval, bool $filenames_only = false): array {
210  $fh = new \ElggFile();
211  $fh->owner_guid = elgg_get_site_entity()->guid;
212  $fh->setFilename("cron/{$interval}/dummy.log");
213 
214  $dir = pathinfo($fh->getFilenameOnFilestore(), PATHINFO_DIRNAME);
215  if (!is_dir($dir) || !is_readable($dir)) {
216  return [];
217  }
218 
219  $dh = new \DirectoryIterator($dir);
220  $files = [];
221  /* @var $file \DirectoryIterator */
222  foreach ($dh as $file) {
223  if ($file->isDot() || !$file->isFile()) {
224  continue;
225  }
226 
227  if ($filenames_only) {
228  $files[] = $file->getFilename();
229  } else {
230  $files[$file->getFilename()] = file_get_contents($file->getPathname());
231  }
232  }
233 
234  if ($filenames_only) {
235  natcasesort($files);
236  } else {
237  uksort($files, 'strnatcasecmp');
238  }
239 
240  return array_reverse($files);
241  }
242 
250  public function getLastCompletion(string $interval): ?DateTime {
251  $fh = new \ElggFile();
252  $fh->owner_guid = elgg_get_site_entity()->guid;
253  $fh->setFilename("cron/{$interval}.complete");
254 
255  if (!$fh->exists()) {
256  return null;
257  }
258 
259  $date = $fh->grabFile();
260  if (empty($date)) {
261  // how??
262  return null;
263  }
264 
265  try {
266  return Values::normalizeTime($date);
267  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
268  $this->getLogger()->warning($e);
269  }
270 
271  return null;
272  }
273 
282  public function getConfiguredIntervals(bool $only_names = false): array {
283  $result = $this->events->triggerResults('cron:intervals', 'system', [], $this->default_intervals);
284  if (!is_array($result)) {
285  $this->getLogger()->warning("The event 'cron:intervals', 'system' should return an array, " . gettype($result) . ' given');
286 
287  $result = $this->default_intervals;
288  }
289 
290  if ($only_names) {
291  return array_keys($result);
292  }
293 
294  return $result;
295  }
296 
305  protected function getLogFilename(string $interval, \DateTime $time = null): string {
306  if (!isset($time)) {
307  $time = $this->getCurrentTime();
308  }
309 
310  $date = $time->format(\DateTimeInterface::ATOM);
311  $date = str_replace('+', 'p', $date);
312  $date = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $date);
313 
314  $fh = new \ElggFile();
315  $fh->owner_guid = elgg_get_site_entity()->guid;
316  $fh->setFilename("cron/{$interval}/{$date}.log");
317 
318  return $fh->getFilenameOnFilestore();
319  }
320 
328  protected function rotateLogs(string $interval): void {
329  $files = $this->getLogs($interval, true);
330  if (count($files) <= self::LOG_FILES_TO_KEEP) {
331  return;
332  }
333 
334  $fh = new \ElggFile();
335  $fh->owner_guid = elgg_get_site_entity()->guid;
336 
337  while (count($files) > self::LOG_FILES_TO_KEEP) {
338  $filename = array_pop($files);
339 
340  $fh->setFilename("cron/{$interval}/{$filename}");
341  $fh->delete();
342  }
343  }
344 
352  protected function logCompletion(string $interval): void {
353  $fh = new \ElggFile();
354  $fh->owner_guid = elgg_get_site_entity()->guid;
355  $fh->setFilename("cron/{$interval}.complete");
356 
357  try {
358  if ($fh->open('write') === false) {
359  return;
360  }
361  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
362  $this->getLogger()->warning($e);
363  return;
364  }
365 
366  $now = new DateTime();
367  $fh->write($now->format(\DateTimeInterface::ATOM));
368  $fh->close();
369  }
370 }
$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:282
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:250
before(string $interval,\Elgg\Logger\Cron $cron_logger,\DateTime $time=null)
Execute commands before cron interval is run.
Definition: Cron.php:103
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:254
getLogFilename(string $interval,\DateTime $time=null)
Get a filename to log in.
Definition: Cron.php:305
getCurrentTime($modifier= '')
Get the (cloned) time.
Definition: TimeUsing.php:25
rotateLogs(string $interval)
Rotate the log files.
Definition: Cron.php:328
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:209
logCompletion(string $interval)
Log the completion time of a cron interval.
Definition: Cron.php:352
after(string $output, string $interval,\Elgg\Logger\Cron $cron_logger)
Printers handler result.
Definition: Cron.php:185
elgg_get_site_entity()
Get the current site entity.
Definition: entities.php:101
getLogger()
Returns logger.
Definition: Loggable.php:37
Extension of the DateTime class to support formatting a date using the locale.
Definition: DateTime.php:12
execute(string $interval,\Elgg\Logger\Cron $cron_logger, string $filename,\DateTime $time=null)
Execute handlers attached to a specific cron interval.
Definition: Cron.php:133
A generic parent class for cron exceptions.
$output
Definition: download.php:9
run(array $intervals=null, bool $force=false)
Executes handlers for periods that have elapsed since last cron.
Definition: Cron.php:55