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  $this->events->trigger('cron', $interval, [
152  'time' => $time->getTimestamp(),
153  'dt' => $time,
154  'logger' => $cron_logger,
155  ], [
156  EventsService::OPTION_BEGIN_CALLBACK => $begin_callback,
157  EventsService::OPTION_END_CALLBACK => $end_callback,
158  ]);
159  } catch (\Throwable $t) {
160  $this->getLogger()->error($t);
161  }
162 
163  $now = new DateTime();
164 
165  $complete = $this->translator->translate('admin:cron:complete', [$interval, $now->format(DATE_RFC2822)]);
166  $cron_logger->notice($complete);
167 
168  if (file_exists($filename) && is_readable($filename)) {
169  return file_get_contents($filename);
170  }
171 
172  return '';
173  }
174 
184  protected function after(string $output, string $interval, \Elgg\Logger\Cron $cron_logger): void {
185  $this->getLogger()->info($output);
186 
187  try {
188  $this->events->triggerAfter('cron', $interval, new \DateTime());
189  } catch (\Throwable $t) {
190  $this->getLogger()->error($t);
191  }
192 
193  $cron_logger->close();
194  $this->rotateLogs($interval);
195  $this->logCompletion($interval);
196  }
197 
208  public function getLogs(string $interval, bool $filenames_only = false): array {
209  $fh = new \ElggFile();
210  $fh->owner_guid = elgg_get_site_entity()->guid;
211  $fh->setFilename("cron/{$interval}/dummy.log");
212 
213  $dir = pathinfo($fh->getFilenameOnFilestore(), PATHINFO_DIRNAME);
214  if (!is_dir($dir) || !is_readable($dir)) {
215  return [];
216  }
217 
218  $dh = new \DirectoryIterator($dir);
219  $files = [];
220  /* @var $file \DirectoryIterator */
221  foreach ($dh as $file) {
222  if ($file->isDot() || !$file->isFile()) {
223  continue;
224  }
225 
226  if ($filenames_only) {
227  $files[] = $file->getFilename();
228  } else {
229  $files[$file->getFilename()] = file_get_contents($file->getPathname());
230  }
231  }
232 
233  if ($filenames_only) {
234  natcasesort($files);
235  } else {
236  uksort($files, 'strnatcasecmp');
237  }
238 
239  return array_reverse($files);
240  }
241 
249  public function getLastCompletion(string $interval): ?DateTime {
250  $fh = new \ElggFile();
251  $fh->owner_guid = elgg_get_site_entity()->guid;
252  $fh->setFilename("cron/{$interval}.complete");
253 
254  if (!$fh->exists()) {
255  return null;
256  }
257 
258  $date = $fh->grabFile();
259  if (empty($date)) {
260  // how??
261  return null;
262  }
263 
264  try {
265  return Values::normalizeTime($date);
266  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
267  $this->getLogger()->warning($e);
268  }
269 
270  return null;
271  }
272 
281  public function getConfiguredIntervals(bool $only_names = false): array {
282  $result = $this->events->triggerResults('cron:intervals', 'system', [], $this->default_intervals);
283  if (!is_array($result)) {
284  $this->getLogger()->warning("The event 'cron:intervals', 'system' should return an array, " . gettype($result) . ' given');
285 
286  $result = $this->default_intervals;
287  }
288 
289  if ($only_names) {
290  return array_keys($result);
291  }
292 
293  return $result;
294  }
295 
304  protected function getLogFilename(string $interval, \DateTime $time = null): string {
305  if (!isset($time)) {
306  $time = $this->getCurrentTime();
307  }
308 
309  $date = $time->format(\DateTimeInterface::ATOM);
310  $date = str_replace('+', 'p', $date);
311  $date = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $date);
312 
313  $fh = new \ElggFile();
314  $fh->owner_guid = elgg_get_site_entity()->guid;
315  $fh->setFilename("cron/{$interval}/{$date}.log");
316 
317  return $fh->getFilenameOnFilestore();
318  }
319 
327  protected function rotateLogs(string $interval): void {
328  $files = $this->getLogs($interval, true);
329  if (count($files) <= self::LOG_FILES_TO_KEEP) {
330  return;
331  }
332 
333  $fh = new \ElggFile();
334  $fh->owner_guid = elgg_get_site_entity()->guid;
335 
336  while (count($files) > self::LOG_FILES_TO_KEEP) {
337  $filename = array_pop($files);
338 
339  $fh->setFilename("cron/{$interval}/{$filename}");
340  $fh->delete();
341  }
342  }
343 
351  protected function logCompletion(string $interval): void {
352  $fh = new \ElggFile();
353  $fh->owner_guid = elgg_get_site_entity()->guid;
354  $fh->setFilename("cron/{$interval}.complete");
355 
356  try {
357  if ($fh->open('write') === false) {
358  return;
359  }
360  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
361  $this->getLogger()->warning($e);
362  return;
363  }
364 
365  $now = new DateTime();
366  $fh->write($now->format(\DateTimeInterface::ATOM));
367  $fh->close();
368  }
369 }
$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:281
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:249
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:304
getCurrentTime($modifier= '')
Get the (cloned) time.
Definition: TimeUsing.php:25
rotateLogs(string $interval)
Rotate the log files.
Definition: Cron.php:327
Cron.
Definition: Cron.php:18
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
Logger.
Definition: Logger.php:25
getLogs(string $interval, bool $filenames_only=false)
Get the log files for a given cron interval.
Definition: Cron.php:208
logCompletion(string $interval)
Log the completion time of a cron interval.
Definition: Cron.php:351
after(string $output, string $interval,\Elgg\Logger\Cron $cron_logger)
Printers handler result.
Definition: Cron.php:184
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