Maintenance, on purpose. * - **The update check.** It is a request to the manage server, and doing it * per page load would put a network round trip in front of the backoffice. * The Maintenance page checks when it is opened, and the heartbeat response * carries the same information as a side effect. * * Where real cron does exist, it calls manage-worker.php (or the CLI) on a * timer and this all still holds — the jobs are the same code either way. */ declare(strict_types=1); /** Whether the manage client is installed and configured on this instance. */ function manage_available(): bool { return is_file(APP_ROOT . '/manage-client/config.php') && is_file(APP_ROOT . '/manage-client/lib/client.php'); } /** Load the client library. Safe to call repeatedly. */ function manage_load(): void { require_once APP_ROOT . '/manage-client/lib/client.php'; } /** * Marks the last time a kick was dispatched. Its mtime is the only thing read, * so an admin page costs one stat() when nothing is due. */ function manage_tick_file(): string { return DATA_DIR . '/manage.tick'; } function manage_state_file(): string { return DATA_DIR . '/manage/web-schedule.json'; } /** Seconds between heartbeats. */ function manage_heartbeat_interval(): int { return (int)config('manage.heartbeat_interval', 3600); } /** * Which jobs are due right now: 'backup', 'heartbeat', or neither. * * The backup side asks the client rather than deciding itself — the interval * lives in manage-client/config.php, and manageBackupCreateAutomaticIfDue() * measures it against the same backup index the Maintenance page shows. */ function manage_due(): array { if (!manage_available()) { return []; } manage_load(); $due = []; if ((int)MANAGE_BACKUP_AUTO_INTERVAL_SECONDS > 0) { $last = 0; foreach (manageBackupList() as $backup) { // Only backups this schedule made count towards it: a manual one // from the Maintenance page should not postpone the routine. if (in_array($backup['trigger'] ?? '', ['automatic', 'cron'], true)) { $last = max($last, strtotime((string)($backup['created_at'] ?? '')) ?: 0); } } if (time() - $last >= (int)MANAGE_BACKUP_AUTO_INTERVAL_SECONDS) { $due[] = 'backup'; } } $state = json_read(manage_state_file()); if (time() - (int)($state['heartbeat_at'] ?? 0) >= manage_heartbeat_interval()) { $due[] = 'heartbeat'; } return $due; } /** * Run whatever is due. Returns a line per job for the log; never throws, because * every caller is a page that has something better to do than fail over this. */ function manage_run_due(): array { if (!manage_available()) { return []; } manage_load(); $done = []; $due = manage_due(); if (in_array('backup', $due, true)) { try { // Returns null if another look at the clock says it is not due // after all — two workers racing, or a backup made in between. $record = manageBackupCreateAutomaticIfDue(); if ($record !== null) { $done[] = 'backup ' . $record['filename']; } } catch (Throwable $e) { manageClientLog('ERROR', 'Scheduled backup failed', ['error' => $e->getMessage()]); $done[] = 'backup failed: ' . $e->getMessage(); } } if (in_array('heartbeat', $due, true)) { // Records the attempt either way: an unreachable server must not turn // into a heartbeat on every single page load. json_update(manage_state_file(), function (array $state): array { $state['heartbeat_at'] = time(); return $state; }); $done[] = manageHeartbeatSendQuietly() === null ? 'heartbeat failed' : 'heartbeat'; } return $done; } /** URL of manage-worker.php on this installation. */ function manage_worker_url(): ?string { return self_url('/manage-worker.php?key=' . rawurlencode(archive_worker_key())); } /** * Called at the end of every admin page. Cheap when nothing is due: one stat() * on the tick file, and the client library is not even loaded. * * The dispatch-then-fall-back-to-inline shape is archive_kick()'s, for the same * reason — a host that blocks outbound HTTP to itself would otherwise never run * these jobs at all. Inline only happens after the page has been flushed to the * operator, so a weekly backup of a few hundred megabytes is never something * they sit and watch. */ function manage_kick(): void { $tick = manage_tick_file(); // Long enough that a click-happy session costs nothing, short enough that // a backup which failed to start is retried within the same visit. if (is_file($tick) && time() - (int)filemtime($tick) < 900) { return; } if (!manage_available() || manage_due() === []) { return; } @touch($tick); if (!function_exists('fastcgi_finish_request')) { self_dispatch(manage_worker_url(), 200); return; } @fastcgi_finish_request(); if (self_dispatch(manage_worker_url(), 2000)) { return; } // No self-dispatch on this host. The operator already has the page, so this // process does the work itself. if (session_status() === PHP_SESSION_ACTIVE) { session_write_close(); } @set_time_limit(0); manage_run_due(); }