_last_run") gegen sein empfohlenes Intervall und führt * ihn bei Bedarf aus – über dieselbe run_*()-Funktion wie der "Jetzt ausführen"- * Button und der echte Cronjob. * * Nebenläufigkeitsschutz: Ein Job wird nur ausgeführt, wenn das UPDATE des * last_run-Settings tatsächlich eine Zeile ändert (atomares "Compare-and-Swap"). * Laufen zwei Anfragen gleichzeitig, "gewinnt" nur eine davon den Job. * * Kann in Einstellungen → Cron-Jobs deaktiviert werden (Setting "pseudo_cron_enabled"), * falls echte Server-Cronjobs eingerichtet sind und der eingebaute Ersatz nicht * gebraucht wird. */ /** Empfohlene Mindestabstände in Sekunden, angelehnt an die Crontab-Beispiele. */ function pseudo_cron_job_definitions(): array { return [ 'fetch_emails' => ['setting' => 'cron_fetch_emails_last_run', 'interval' => 300, 'file' => 'fetch_emails.php', 'fn' => 'run_fetch_emails'], 'check_sla' => ['setting' => 'cron_check_sla_last_run', 'interval' => 900, 'file' => 'check_sla.php', 'fn' => 'run_check_sla'], 'check_escalations' => ['setting' => 'cron_check_escalations_last_run', 'interval' => 3600, 'file' => 'check_escalations.php', 'fn' => 'run_check_escalations'], 'process_backups' => ['setting' => 'cron_process_backups_last_run', 'interval' => 3600, 'file' => 'process_backups.php', 'fn' => 'run_process_backups'], 'process_recurring' => ['setting' => 'cron_process_recurring_last_run', 'interval' => 86400, 'file' => 'process_recurring.php', 'fn' => 'run_process_recurring'], 'anonymize_data' => ['setting' => 'cron_anonymize_data_last_run', 'interval' => 86400, 'file' => 'anonymize_data.php', 'fn' => 'run_anonymization'], 'auto_close_tickets'=> ['setting' => 'cron_auto_close_tickets_last_run','interval' => 86400, 'file' => 'auto_close_tickets.php','fn' => 'run_auto_close_tickets'], ]; } /** * Versucht, die "Ausführungs-Berechtigung" für ein Setting atomar zu übernehmen: * Nur wenn der gespeicherte Zeitstempel älter als das Intervall ist, wird er auf * "jetzt" gesetzt und true zurückgegeben. Verhindert doppelte Ausführung bei * gleichzeitigen Anfragen (kein SELECT-dann-UPDATE, sondern ein atomares UPDATE). */ function pseudo_cron_try_claim(PDO $pdo, string $settingKey, int $intervalSeconds): bool { $driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); // Zeile anlegen falls noch nie vorhanden (fernes Datum = sofort fällig). if ($driver === 'sqlite') { $pdo->prepare("INSERT OR IGNORE INTO settings (setting_key, setting_value) VALUES (?, '1970-01-01 00:00:00')") ->execute([$settingKey]); } else { $pdo->prepare("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES (?, '1970-01-01 00:00:00')") ->execute([$settingKey]); } $now = date('Y-m-d H:i:s'); $threshold = date('Y-m-d H:i:s', time() - $intervalSeconds); $stmt = $pdo->prepare('UPDATE settings SET setting_value = ? WHERE setting_key = ? AND setting_value < ?'); $stmt->execute([$now, $settingKey, $threshold]); return $stmt->rowCount() > 0; } /** Prüft alle Jobs und führt fällige direkt aus. Läuft NACH dem Senden der Antwort. */ function pseudo_cron_dispatch(): void { try { $pdo = get_db(); if (get_setting('pseudo_cron_enabled', '1') !== '1') return; // Grobes Gate: höchstens alle 20 Sekunden eine echte Prüfung, um bei viel // Traffic nicht bei jeder einzelnen Anfrage 6 zusätzliche Abfragen zu machen. if (!pseudo_cron_try_claim($pdo, 'pseudo_cron_gate', 20)) return; if (!defined('DESKHUB_CRON_RUNNER')) define('DESKHUB_CRON_RUNNER', true); foreach (pseudo_cron_job_definitions() as $key => $job) { if (!pseudo_cron_try_claim($pdo, $job['setting'], $job['interval'])) continue; try { require_once __DIR__ . '/../cron/' . $job['file']; $fn = $job['fn']; $fn($pdo, false); } catch (Throwable $e) { error_log("pseudo_cron [$key]: " . $e->getMessage()); } } // Plugin- und Theme-Katalog im Hintergrund aktuell halten (alle 30 Minuten) if (pseudo_cron_try_claim($pdo, 'catalog_refresh_last_run', 1800)) { catalog_refresh_background(); } // Plugins: wiederkehrende Aufgaben (höchstens alle 5 Minuten) if (pseudo_cron_try_claim($pdo, 'plugins_cron_last_run', 300)) { plugin_hook('cron.run', ['time' => time()]); } // Verwaiste Zeiterfassungs-Sitzungen aufräumen (includes/time_tracking.php, // lebt bereits in bootstrap.php – kein require_once aus cron/ nötig). if (pseudo_cron_try_claim($pdo, 'time_tracking_sweep_last_run', 300)) { try { finalize_stale_work_sessions($pdo, false); } catch (Throwable $e) { error_log('pseudo_cron [time_tracking_sweep]: ' . $e->getMessage()); } } } catch (Throwable $e) { error_log('pseudo_cron_dispatch: ' . $e->getMessage()); } } /** * Registriert die Hintergrundprüfung für das Ende des aktuellen Requests. * Von require_staff_login() (Web) und api_require_staff() (mobile App) aufgerufen – * läuft also bei jeder authentifizierten Mitarbeiter-Anfrage, ganz ohne dass * jemand manuell etwas klicken oder ein externer Dienst konfiguriert werden muss. */ function pseudo_cron_schedule_background(): void { static $scheduled = false; if ($scheduled) return; // pro Request nur einmal registrieren $scheduled = true; if (php_sapi_name() === 'cli') return; // im echten Cron nicht nötig register_shutdown_function(function () { // Verbindung zum Client möglichst sofort beenden, damit die Prüfung // die eigentliche Seite/den API-Aufruf nicht spürbar verlangsamt. if (function_exists('fastcgi_finish_request')) { @fastcgi_finish_request(); } else { if (function_exists('ob_get_level')) { while (ob_get_level() > 0) { @ob_end_flush(); } } @flush(); } pseudo_cron_dispatch(); }); }