Elgg  Version 5.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 
37  protected EventsService $events;
38 
40 
47  public function __construct(EventsService $events, Translator $translator) {
48  $this->events = $events;
49  $this->translator = $translator;
50  }
51 
61  public function run(array $intervals = null, bool $force = false): array {
62  if (!isset($intervals)) {
63  $intervals = array_keys($this->default_intervals);
64  }
65 
66  $allowed_intervals = $this->getConfiguredIntervals();
67 
68  $scheduler = new Scheduler();
69  $time = $this->getCurrentTime();
70 
71  foreach ($intervals as $interval) {
72  if (!array_key_exists($interval, $allowed_intervals)) {
73  throw new CronException("{$interval} is not a recognized cron interval. Please use one of the following: " . implode(', ', array_keys($allowed_intervals)));
74  }
75 
76  $cron_interval = $force ? $allowed_intervals['minute'] : $allowed_intervals[$interval];
77  $filename = $this->getLogFilename($interval, $time);
78 
79  $cron_logger = \Elgg\Logger\Cron::factory([
80  'interval' => $interval,
81  'filename' => $filename,
82  ]);
83 
84  $scheduler
85  ->call(function () use ($interval, $time, $cron_logger, $filename) {
86  return $this->execute($interval, $cron_logger, $filename, $time);
87  })
88  ->at($cron_interval)
89  ->before(function () use ($interval, $time, $cron_logger) {
90  $this->before($interval, $cron_logger, $time);
91  })
92  ->then(function ($output) use ($interval, $cron_logger) {
93  $this->after($output, $interval, $cron_logger);
94  });
95  }
96 
97  return $scheduler->run($time);
98  }
99 
109  protected function before(string $interval, \Elgg\Logger\Cron $cron_logger, \DateTime $time = null): void {
110  if (!isset($time)) {
111  $time = $this->getCurrentTime();
112  }
113 
114  try {
115  $this->events->triggerBefore('cron', $interval, $time);
116  } catch (\Throwable $t) {
117  $this->getLogger()->error($t);
118  }
119 
120  // give every period at least 'max_execution_time' (PHP ini setting)
121  set_time_limit((int) ini_get('max_execution_time'));
122 
123  $now = new DateTime();
124 
125  $cron_logger->notice($this->translator->translate('admin:cron:started', [$interval, $time->format(DATE_RFC2822)]));
126  $cron_logger->notice($this->translator->translate('admin:cron:started:actual', [$interval, $now->format(DATE_RFC2822)]));
127  }
128 
139  protected function execute(string $interval, \Elgg\Logger\Cron $cron_logger, string $filename, \DateTime $time = null): string {
140  if (!isset($time)) {
141  $time = $this->getCurrentTime();
142  }
143 
144  try {
145  ob_start();
146 
147  $begin_callback = function (array $params) use ($cron_logger) {
148  $readable_callable = (string) elgg_extract('readable_callable', $params);
149 
150  $cron_logger->notice("Starting {$readable_callable}");
151  };
152 
153  $end_callback = function (array $params) use ($cron_logger) {
154  $readable_callable = (string) elgg_extract('readable_callable', $params);
155 
156  $cron_logger->notice("Finished {$readable_callable}");
157  };
158 
159  $old_stdout = $this->events->triggerResults('cron', $interval, [
160  'time' => $time->getTimestamp(),
161  'dt' => $time,
162  'logger' => $cron_logger,
163  ], '', [
164  EventsService::OPTION_BEGIN_CALLBACK => $begin_callback,
165  EventsService::OPTION_END_CALLBACK => $end_callback,
166  ]);
167 
168  $ob_output = ob_get_clean();
169 
170  if (!empty($ob_output)) {
171  elgg_deprecated_notice('Direct output (echo, print) in a CRON event will be removed, use the provided "logger"', '5.1');
172 
173  $cron_logger->notice($ob_output, ['ob_output']);
174  }
175 
176  if (!empty($old_stdout)) {
177  elgg_deprecated_notice('Output in a CRON event result will be removed, use the provided "logger"', '5.1');
178 
179  $cron_logger->notice($old_stdout, ['event_result']);
180  }
181  } catch (\Throwable $t) {
182  $ob_output = ob_get_clean();
183 
184  if (!empty($ob_output)) {
185  elgg_deprecated_notice('Direct output (echo, print) in a CRON event will be removed, use the provided "logger"', '5.1');
186 
187  $cron_logger->notice($ob_output, ['ob_output', 'throwable']);
188  }
189 
190  $this->getLogger()->error($t);
191  }
192 
193  $now = new DateTime();
194 
195  $complete = $this->translator->translate('admin:cron:complete', [$interval, $now->format(DATE_RFC2822)]);
196  $cron_logger->notice($complete);
197 
198  if (file_exists($filename) && is_readable($filename)) {
199  return file_get_contents($filename);
200  }
201 
202  return '';
203  }
204 
214  protected function after(string $output, string $interval, \Elgg\Logger\Cron $cron_logger): void {
215  $this->getLogger()->info($output);
216 
217  try {
218  $this->events->triggerAfter('cron', $interval, new \DateTime());
219  } catch (\Throwable $t) {
220  $this->getLogger()->error($t);
221  }
222 
223  $cron_logger->close();
224  $this->rotateLogs($interval);
225  $this->logCompletion($interval);
226  }
227 
238  public function getLogs(string $interval, bool $filenames_only = false): array {
239  $fh = new \ElggFile();
240  $fh->owner_guid = elgg_get_site_entity()->guid;
241  $fh->setFilename("cron/{$interval}/dummy.log");
242 
243  $dir = pathinfo($fh->getFilenameOnFilestore(), PATHINFO_DIRNAME);
244  if (!is_dir($dir) || !is_readable($dir)) {
245  return [];
246  }
247 
248  $dh = new \DirectoryIterator($dir);
249  $files = [];
250  /* @var $file \DirectoryIterator */
251  foreach ($dh as $file) {
252  if ($file->isDot() || !$file->isFile()) {
253  continue;
254  }
255 
256  if ($filenames_only) {
257  $files[] = $file->getFilename();
258  } else {
259  $files[$file->getFilename()] = file_get_contents($file->getPathname());
260  }
261  }
262 
263  if ($filenames_only) {
264  natcasesort($files);
265  } else {
266  uksort($files, 'strnatcasecmp');
267  }
268 
269  return array_reverse($files);
270  }
271 
279  public function getLastCompletion(string $interval): ?DateTime {
280  $fh = new \ElggFile();
281  $fh->owner_guid = elgg_get_site_entity()->guid;
282  $fh->setFilename("cron/{$interval}.complete");
283 
284  if (!$fh->exists()) {
285  return null;
286  }
287 
288  $date = $fh->grabFile();
289  if (empty($date)) {
290  // how??
291  return null;
292  }
293 
294  try {
295  return Values::normalizeTime($date);
296  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
297  $this->getLogger()->warning($e);
298  }
299 
300  return null;
301  }
302 
311  public function getConfiguredIntervals(bool $only_names = false): array {
312  $result = $this->events->triggerResults('cron:intervals', 'system', [], $this->default_intervals);
313  if (!is_array($result)) {
314  $this->getLogger()->warning("The event 'cron:intervals', 'system' should return an array, " . gettype($result) . ' given');
315 
316  $result = $this->default_intervals;
317  }
318 
319  if ($only_names) {
320  return array_keys($result);
321  }
322 
323  return $result;
324  }
325 
334  protected function getLogFilename(string $interval, \DateTime $time = null): string {
335  if (!isset($time)) {
336  $time = $this->getCurrentTime();
337  }
338 
339  $date = $time->format(\DateTimeInterface::ATOM);
340  $date = str_replace('+', 'p', $date);
341  $date = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $date);
342 
343  $fh = new \ElggFile();
344  $fh->owner_guid = elgg_get_site_entity()->guid;
345  $fh->setFilename("cron/{$interval}/{$date}.log");
346 
347  return $fh->getFilenameOnFilestore();
348  }
349 
357  protected function rotateLogs(string $interval): void {
358  $files = $this->getLogs($interval, true);
359  if (count($files) <= self::LOG_FILES_TO_KEEP) {
360  return;
361  }
362 
363  $fh = new \ElggFile();
364  $fh->owner_guid = elgg_get_site_entity()->guid;
365 
366  while (count($files) > self::LOG_FILES_TO_KEEP) {
367  $filename = array_pop($files);
368 
369  $fh->setFilename("cron/{$interval}/{$filename}");
370  $fh->delete();
371  }
372  }
373 
381  protected function logCompletion(string $interval): void {
382  $fh = new \ElggFile();
383  $fh->owner_guid = elgg_get_site_entity()->guid;
384  $fh->setFilename("cron/{$interval}.complete");
385 
386  try {
387  if ($fh->open('write') === false) {
388  return;
389  }
390  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
391  $this->getLogger()->warning($e);
392  return;
393  }
394 
395  $now = new DateTime();
396  $fh->write($now->format(\DateTimeInterface::ATOM));
397  $fh->close();
398  }
399 }
__construct(EventsService $events, Translator $translator)
Constructor.
Definition: Cron.php:47
$params
Saves global plugin settings.
Definition: save.php:13
elgg_deprecated_notice(string $msg, string $dep_version)
Log a notice about deprecated use of a function, view, etc.
Definition: elgglib.php:115
getConfiguredIntervals(bool $only_names=false)
Get the cron interval configuration.
Definition: Cron.php:311
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:279
Translator $translator
Definition: Cron.php:39
before(string $interval,\Elgg\Logger\Cron $cron_logger,\DateTime $time=null)
Execute commands before cron interval is run.
Definition: Cron.php:109
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:334
getCurrentTime($modifier= '')
Get the (cloned) time.
Definition: TimeUsing.php:25
rotateLogs(string $interval)
Rotate the log files.
Definition: Cron.php:357
Cron.
Definition: Cron.php:18
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
array EventsService $events
Definition: Cron.php:27
Logger.
Definition: Logger.php:25
getLogs(string $interval, bool $filenames_only=false)
Get the log files for a given cron interval.
Definition: Cron.php:238
logCompletion(string $interval)
Log the completion time of a cron interval.
Definition: Cron.php:381
after(string $output, string $interval,\Elgg\Logger\Cron $cron_logger)
Printers handler result.
Definition: Cron.php:214
elgg_get_site_entity()
Get the current site entity.
Definition: entities.php:98
getLogger()
Returns logger.
Definition: Loggable.php:37
Extension of the DateTime class to support formating 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:139
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:61