manage-activity.php 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. <?php
  2. // Cron replacement: the periodic manage-client tasks, triggered by admin
  3. // activity instead of a scheduler.
  4. //
  5. // This deployment usually has no cron, so the two jobs that would otherwise run
  6. // from manage-client/bin/manage-client.php happen when an admin loads the
  7. // dashboard: the automatic backup once its interval has passed, and the status
  8. // report to the manage server once its own, much shorter interval has passed.
  9. //
  10. // Both are throttled and neither may break the page it runs on, so nothing in
  11. // here throws.
  12. require_once __DIR__ . "/../manage-client/lib/client.php";
  13. // How long an admin session may pass without the manage server hearing from
  14. // this instance. Short enough that the server dashboard stays meaningful,
  15. // long enough that it is not one request per admin page view.
  16. if (!defined("MANAGE_ACTIVITY_HEARTBEAT_INTERVAL")) {
  17. define("MANAGE_ACTIVITY_HEARTBEAT_INTERVAL", 3600);
  18. }
  19. function manageActivityStateFile(): string
  20. {
  21. // Next to the client's own log, in data/manage/ - gitignored and blocked
  22. // from the web by the .htaccess in the project root.
  23. return dirname((string) MANAGE_LOG_FILE) . "/activity.json";
  24. }
  25. function manageActivityReadState(): array
  26. {
  27. $file = manageActivityStateFile();
  28. if (!is_file($file) || !is_readable($file)) {
  29. return [];
  30. }
  31. $decoded = json_decode((string) file_get_contents($file), true);
  32. return is_array($decoded) ? $decoded : [];
  33. }
  34. function manageActivityWriteState(array $state): void
  35. {
  36. $file = manageActivityStateFile();
  37. $dir = dirname($file);
  38. if (!is_dir($dir) && !mkdir($dir, 02775, true) && !is_dir($dir)) {
  39. return;
  40. }
  41. @file_put_contents($file, json_encode($state, JSON_PRETTY_PRINT), LOCK_EX);
  42. }
  43. /**
  44. * Sends the status report if the interval has passed.
  45. *
  46. * The timestamp is written before the request, not after: a manage server that
  47. * hangs must not turn every following page load into another attempt.
  48. *
  49. * @return bool whether a heartbeat was attempted
  50. */
  51. function manageActivityHeartbeatIfDue(bool $force = false): bool
  52. {
  53. $interval = (int) MANAGE_ACTIVITY_HEARTBEAT_INTERVAL;
  54. if (!$force && $interval <= 0) {
  55. return false;
  56. }
  57. if (!manageClientConfigured()) {
  58. return false;
  59. }
  60. $state = manageActivityReadState();
  61. $last = (int) ($state["last_heartbeat_at"] ?? 0);
  62. if (!$force && $last > 0 && time() - $last < $interval) {
  63. return false;
  64. }
  65. $state["last_heartbeat_at"] = time();
  66. manageActivityWriteState($state);
  67. manageHeartbeatSendQuietly();
  68. return true;
  69. }
  70. /**
  71. * Runs the due jobs for one admin page view.
  72. *
  73. * @return array{message: string, type: string} empty message when nothing happened
  74. */
  75. function manageActivityRunDueTasks(): array
  76. {
  77. $result = ["message" => "", "type" => ""];
  78. $backup = null;
  79. try {
  80. $backup = manageBackupCreateAutomaticIfDue();
  81. if ($backup !== null) {
  82. $result = [
  83. "message" => "Automatisches Backup wurde erstellt: " . $backup["filename"] . ".",
  84. "type" => "success",
  85. ];
  86. }
  87. } catch (Throwable $exception) {
  88. $result = [
  89. "message" => "Automatisches Backup konnte nicht erstellt werden: " .
  90. $exception->getMessage(),
  91. "type" => "warning",
  92. ];
  93. }
  94. // A fresh backup is worth reporting right away, so the server dashboard
  95. // does not show a stale "last backup" for up to another interval.
  96. manageActivityHeartbeatIfDue($backup !== null);
  97. return $result;
  98. }