156 lines
7.8 KiB
PHP
156 lines
7.8 KiB
PHP
<?php
|
||
/**
|
||
* includes/pseudo_cron.php
|
||
*
|
||
* "Eingebauter" Ersatz für echte Betriebssystem-Cronjobs, für Hosting-Umgebungen
|
||
* ohne Crontab-Zugriff (z.B. viele Shared-Hosting-Pakete). Funktioniert komplett
|
||
* ohne externe Dienste: Bei jeder authentifizierten Anfrage im Mitarbeiterbereich
|
||
* (Web-Backend UND mobile App) wird nach dem Ausliefern der Antwort im Hintergrund
|
||
* geprüft, ob einer der 6 Cronjobs fällig ist – und falls ja, direkt ausgeführt.
|
||
*
|
||
* Ablauf, ohne die eigentliche Seite/den API-Request zu verlangsamen:
|
||
* 1. pseudo_cron_schedule_background() wird beim Seiten-/API-Aufruf registriert.
|
||
* 2. register_shutdown_function() sorgt dafür, dass der Code erst NACH dem Senden
|
||
* der Antwort läuft. Wo verfügbar (PHP-FPM) beendet fastcgi_finish_request()
|
||
* die Verbindung zum Browser sofort – der Nutzer merkt nichts von der Wartezeit.
|
||
* 3. pseudo_cron_dispatch() prüft für jeden Job das letzte Ausführungsdatum
|
||
* (Setting "cron_<job>_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();
|
||
}
|
||
|
||
// Backups automatisch prüfen: neue/ungeprüfte sofort, geprüfte nach 7 Tagen erneut (höchstens alle 10 Minuten, 2 je Lauf)
|
||
if (pseudo_cron_try_claim($pdo, 'cron_verify_backups_last_run', 600)) {
|
||
try {
|
||
if (!function_exists('backup_verify_pending')) require_once __DIR__ . '/backup_restore.php';
|
||
backup_verify_pending(2);
|
||
} catch (Throwable $e) {
|
||
error_log('pseudo_cron [verify_backups]: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
// 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();
|
||
});
|
||
}
|