Das ist eine für den Ausdruck optimierte Ansicht des gesamten Kapitels inkl. Unterseiten.
Druckvorgang starten.
Zur Standardansicht zurückkehren.
Cronjob
Cron-Job-Scheduler-Dienst für webtrees

Ein Cron-Job-Scheduler-Dienst für webtrees: plane Wartungs-/Batch-Jobs mit Standard-Cron-Ausdrücken, beobachte deren Ausführungshistorie in der Admin-UI und trigger sie manuell. Das Modul ist der Dienst; das OS (klassisches Cron oder ein systemd-Timer) muss nur ein kleines Skript einmal pro Minute aufrufen.
- Job-Registry + Ausführungshistorie in zwei eigenen Tabellen (
cj_job, cj_run) - Admin-UI (Steuerungspanel): Job-Liste, Create/Edit-Formular, Ausführungshistorie, jetzt ausführen, aktivieren/deaktivieren, löschen
- Isolierte Ausführung: jeder Job läuft als separater PHP-Kindprozess (
proc_open, kein Shell), mit pro-Job-Timeout und erfasster Ausgabe - Selbst-Registrierung: andere Module können ihre Jobs anbieten (ein
cron-jobs.php
-Manifest oder eine getCronJobs()-Methode); sie erscheinen hier, deaktiviert erstellt und bereit zum Aktivieren - Ereignisgesteuerte Jobs: Jobs können auf Ereignisse statt auf Zeitpläne reagieren — über einen token-geschützten Webhook, einen direkten
EventQueue::push()-Aufruf, die eingebauten Pseudo-Event-Poller oder Route-Events - Fehlerbenachrichtigung: pro-Job-Opt-in zur Benachrichtigung der Admin-Konten bei Job-Fehlern
- Offline-Bewusstsein: während
data/offline.txt existiert, wird der Tick vor jedem Datenbankzugriff übersprungen - Keine Core-Änderungen, keine Änderungen an anderen Modulen

Ursprünglich entwickelt als Support-Modul für LinkEnhancer, um dessen Indizes aktuell zu halten.
Support
1 - Einrichtung
Installation und Voraussetzungen
Was wir zum Installieren dieses Moduls brauchen und wie wir es korrekt einrichten.
1.1 - Voraussetzungen
Systemvoraussetzungen für das Cronjob-Modul
- webtrees 2.2.x (PHP 8.3+)
- PHP CLI mit
proc_open verfügbar (Standard) - Ein Trigger: klassisches Cron, ein systemd-Timer, eine Windows Task Scheduler-Aufgabe, oder der eingebaute Watch-Daemon (kein OS-Timer nötig — siehe “Trigger-Installation”)
1.2 - Installation
Schritt-für-Schritt-Installation und Trigger-Setup
- Modulordner nach
modules_v4/cronjob/ kopieren (ZIP-Installation geht auch). Die Cron-Bibliothek ist im Modul enthalten — nichts zu holen. - Webtrees einmal öffnen (irgendeine Seite als Admin), damit das Modul-
boot() seine Tabellen erstellt. - Trigger installieren (klassisches Cron, systemd-Timer oder Windows Task Scheduler — die Admin-Seite des Moduls generiert den Block für Ihr OS mit den Pfaden dieser Installation bereits ausgefüllt).
- Trigger-Installation (einmalig) — siehe unten.
- Erstellen Sie Ihren ersten Job in der Admin-UI oder konfigurieren und aktivieren Sie registrierte Jobs.
Trigger-Installation (einmalig)

Wichtig — PHP-CLI-Binary: Die Installation-Akkordeon auf der Admin-Seite zeigt, welcher PHP-CLI-Interpreter derzeit aufgelöst wird (Badge: auto-erkannt / manueller Pfad) und erlaubt das Festpinnen eines manuellen Pfads.
Es ist wichtig zu prüfen, ob Ihr Webserver eine PHP-CLI-Binary (SAPI) bereitstellt und ob das Modul korrekt konfiguriert ist, um sie zu verwenden. Andernfalls wird Tick nicht funktionieren.
Der Tick ist so konzipiert, dass er alle 60 Sekunden läuft. Er erfasst Cron-Jobs, Commands und Events, triggert Events und verarbeitet die Task-Queue.
Wählen Sie eine der auf der Admin-Seite gezeigten Optionen:
Option A — klassisches Cron
Eine Zeile in der Crontab des Webserver-Nutzers, läuft jede Minute:
* * * * * cd "/path/to/webtrees" && mkdir -p data/cronjob && "/usr/bin/php" modules_v4/cronjob/cli/tick.php cron:tick >> "/path/to/webtrees/data/cronjob/tick.log" 2>&1
Option B — systemd-Timer
Zwei Unit-Dateien (werden auch auf der Admin-Seite angezeigt):
# /etc/systemd/system/cronjob-tick.service
[Unit]
Description=webtrees cronjob module tick (runs due maintenance jobs)
[Service]
Type=oneshot
User=<webserver-user>
WorkingDirectory="/path/to/webtrees"
ExecStart="/usr/bin/php" ./modules_v4/cronjob/cli/tick.php cron:tick
# /etc/systemd/system/cronjob-tick.timer
[Unit]
Description=Run the webtrees cronjob tick every minute
[Timer]
OnBootSec=1min
OnCalendar=*:*:00
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now cronjob-tick.timer
Option C — Watch-Daemon (kein OS-Timer)
Klicken Sie Start watch auf der Admin-Seite. Ein Resident-Prozess führt den Tick alle 60 Sekunden aus, und ein Page-Load-Watchdog startet ihn neu, falls er stirbt. Verwenden Sie es, wenn der Host kein Cron und keinen systemd-Zugriff hat, oder für Tests — auf Windows-Hosts ist dies die einfachste Option.
stateDiagram-v2
[*] --> Stopped: (initial)
Stopped --> Running: Watch starten (Admin-UI)
Running --> TickLoop: tick alle 60 s
TickLoop --> TickLoop: Job ausführen
TickLoop --> Dead: ⚠ Prozess stirbt
Dead --> Running: 🔄 Watchdog respawn<br/>(nächster Page-Load<br/>Cooldown 5 min)
Running --> Stopped: Watch stoppen
Dead --> Stopped: Watch stoppen<br/>(kein Respawn)
note right of Running
🔒 Hält data/cronjob/watch.lock
🆔 PID sichtbar in Admin-UI
🛡 Überlebt FPM-Worker-Recycle (setsid)
end note
Option D — Windows Task Scheduler
Die Admin-Seite generiert das XML mit bereits ausgefüllten Pfaden. Speichern Sie es als wt-cronjob-tick.xml (UTF-8) und führen Sie schtasks /Create /F /XML wt-cronjob-tick.xml einmal in einer erhöhten Eingabeaufforderung aus.
1.3 - Admin-Einstellungen
Webtrees Modul-Einstellungen
Verfügbare Optionen auf der Admin-Seite:
Ereignis-Katalog
Die Admin-Seite (Events-Abschnitt) und die Ereignisnamen-Vorschläge im Job-Formular werden von einer Ereignis-Inventur (cj_event_catalog, vom Tick synchronisiert wie die Job-Registry) gespeist. Sie mischt in dieser Prioritätsreihenfolge:
- Events, von Modulen angekündigt (Manifest-
events-Abschnitt / getModuleEvents()), namespaced <module>:<name> - eingebaute Pseudo-Events (
cronjob:*, von den Detektoren) - Route-Events (
_route:*, abgeleitet aus der live Routentabelle) - Ereignisnamen, die derzeit abgehört werden von einem aktiven Event-Job (Quelle
listening) — dies deckt Webhook-only-Namen ab
Jeder Eintrag trägt seine Quelle, eine Beschreibung (als Übersetzungsschlüssel gespeichert, nur zur Render-Zeit übersetzt) und die bekannten Payload-Parameter-Namen. Für Route-Events wird der Payload aus dem Routenpfad abgeleitet: die {param}-Platzhalter in der Reihenfolge ihres Auftretens, wobei {tree} sich zu tree + tree_name ausdehnt (spiegelnd, was der live Event-Payload trägt). Für Pseudo-Events kommen beide vom Detektor (description() / payloadKeys()). Die vollständige Liste ist auch das, was das “Ereignisname”-Datalist im Job-Formular bietet. Der Katalog wird als Client-seitige DataTables (Sortieren/Filtern/Paginieren) im Events-Abschnitt gerendert.

Befehls-Katalog
Die Befehls-Vorschläge im Job-Formular und die Admin-Seite (Events-Abschnitt) werden von einer Befehl-Inventur (cj_command_catalog, vom Tick synchronisiert wie die anderen) gespeist. Sie mischt in dieser Prioritätsreihenfolge (erste Quelle gewinnt pro Befehl):
- die allowlisteten Core-Befehle (
tree-export, tree-list, user-list, site-setting), mit ihren bekannten Parametern - Befehle, von Modulen angekündigt (Manifest-
commands-Abschnitt / getModuleCommands()) — mit einer Beschreibung und strukturierten Parametern - Modul-CLI-Skripte, gefunden per Glob (
modules_v4/<module>/cli/*.php), außer für ein Modul, das Befehle angekündigt hat (das Modul kuriert seine eigene Liste, daher werden seine internen Skripte nicht angeboten)
Jeder Eintrag trägt seinen Typ (module / core), seine Quelle, eine Beschreibung (Übersetzungsschlüssel, zur Render-Zeit übersetzt) und die verfügbaren Parameter (strukturiert: Name, optional, Default, Beschreibung). Im Job-Formular zeigt die Eingabe eines Befehls, der mit einem angekündigten Befehl übereinstimmt, seine Parameter als Hinweis unter dem Feld.

Fehlerbenachrichtigung
Ein Job kann als Bei Fehler benachrichtigen markiert werden. Wenn ein solcher Job fehlschlägt (nicht-null Exit oder Timeout), werden die Administrator-Konten der Site über webtrees’ eigenes MessageService::deliverMessage() alarmiert — das über das interne Nachrichtensystem und/oder E-Mail entsprechend der eigenen Kontaktmethode-Präferenz jedes Empfängers liefert. Es werden keine Adressen im Modul gespeichert und kein Kanal ist hartkodiert: die Empfänger sind die Admin-Benutzerkonten, und wie sie erreicht werden, folgt den Nachrichten-Einstellungen der Site.
Die Benachrichtigung trägt den Job-Namen, Status, Exit-Code, Dauer und einen kurzen Schwanz der erfassten Ausgabe. Sie läuft im Tick (keine Web-Anfrage), daher trägt sie keinen Link über den Text hinaus. Ein Benachrichtigungsproblem bricht nie den Tick.
2 - Verwendung
Jobs erstellen, Ausführungshistorie und Admin-UI
Job erstellen
Steuerungspanel → Cron Job Scheduler → Neuer Job:
| Feld | Bedeutung |
|---|
| Name (Slug) | technischer Bezeichner, a-z 0-9 _ -. Nur-lesen für durch Module angebotene Jobs (deren <module>:<name>-Schlüssel ist fix); editierbar für selbst erstellte Jobs. |
| Titel | menschenlesbarer Name |
| Trigger | ein oder mehrere Zeit-Trigger (Cron-Zeitpläne) und/oder Ereignis-Trigger — der Job feuert, wenn irgendein Trigger feuert. Mindestens einer ist erforderlich. |
| Cron-Ausdruck | (Zeit-Trigger) Standard-5-Feld-Cron oder ein Makro (@daily, @weekly, …). Zeiten sind UTC. |
| Ereignisname | (Ereignis-Trigger) das Ereignis, auf das dieser Job reagiert. Slug oder <domain>:slug, max. 64 Zeichen. |
| Befehl | ein modules_v4/<module>/cli/<script>.php-Pfad oder ein erlaubter Core-Befehl. |
| Argumente | nur einfache Optionen/Werte (z.B. --limit=5000), max. 10 Tokens |
| Timeout | 30 - 3600 s |
| Bei Fehler benachrichtigen | Opt-in: Admin-Konten bei Job-Fehlern benachrichtigen |
Verfügbare Ereignisnamen und Befehle werden als Datalists bereitgestellt, um die Dateneingabe zu erleichtern.

Der Jetzt ausführen-Button stellt den Job für den nächsten Tick in die Warteschlange (≤ 60 s). Die Verlauf-Seite zeigt Status, Exit-Code, Dauer, den auslösenden Trigger und die erfasste Ausgabe (letzte 64 KB) der letzten 25 Läufe pro Job (plus 30-Tage-globale Aufbewahrung).

Die Job-Tabelle ist ein Client-seitiger DataTable (Suchfeld, sortierbare Spalten, Paginierung, Status im Browser gespeichert). Die Spalten Aktiviert und Benachrichtigung können an Ort und Stelle umgeschaltet werden. Pro Zeile: Duplizieren und Zurücksetzen (stellt modulangebotene Standardwerte wieder her).
3 - Der Tick
Wie ’tickt’ das Modul
Der Tick ist das Herz des Moduls: einmal pro Minute prüft er alle aktivierten Jobs, entscheidet, was fällig ist, und führt es aus. Weder das webtrees-Core noch das Betriebssystem planen selbst — sie starten nur tick.php
.
flowchart TD
subgraph Triggers["`⏱ Auslöser *(once per minute)*`"]
CRON["`🕑 OS cron / systemd<br/>*every minute*`"]
WATCH["`👁 watch-Daemon<br/>*(in-module)*`"]
end
subgraph Guard["🛡 Guard"]
TP["`📄 cli/tick.php · **cron:tick**<br/>*guard(): 403 over HTTP*`"]
OFF{"`⚠ site offline?<br/>*data/offline.txt*`"}
BOOT["`🚀 boot()<br/>*webtrees + DB*`"]
end
subgraph Selection["🔎 Arbeit auswählen"]
TIME["`📅 time-based jobs<br/>**next_run_at due** *(UTC)*`"]
CE[("`📦 cj_event<br/>*Event-Warteschlange*`")]
DRAIN["`🧮 drain queue<br/>*coalesced · 60 s budget*`"]
end
subgraph Execution["`⚙ Ausführung *(isolated child)*`"]
RUN["`🧪 JobRunner<br/>**proc_open**`"]
LIM["`⏲ timeout 30–3600 s<br/>last **64 KB** · exit code`"]
end
subgraph Storage["💾 Speicherung"]
CJRUN[("`cj_run<br/>*Laufverlauf*`")]
MARK["`🏷 tick.last<br/>*stale > 300 s → tick_stale*`"]
end
CRON --> Guard
WATCH --> Guard
TP --> OFF
OFF -->|"offline"| SKIP["⏭ Tick überspringen"]
OFF -->|"online"| BOOT
BOOT --> TIME
BOOT --> DRAIN
CE --> DRAIN
TIME --> Execution
DRAIN --> Execution
RUN --> LIM
LIM --> CJRUN
BOOT --> MARK
classDef sourceStyle fill:#e3f2fd,stroke:#1565C0,stroke-width:2px
classDef queueStyle fill:#fff3e0,stroke:#E65100,stroke-width:2px
classDef tickStyle fill:#e8f5e9,stroke:#2E7D32,stroke-width:2px
classDef execStyle fill:#fce4ec,stroke:#C2185B,stroke-width:2px
classDef storageStyle fill:#f5f5f5,stroke:#616161,stroke-width:1px
class CRON,WATCH sourceStyle
class OFF,CE queueStyle
class TP,TIME,DRAIN tickStyle
class RUN,LIM execStyle
class CJRUN,MARK,SKIP storageStyle
Was einen Tick startet
Einen Trigger wählen (einmalig):
- OS-Cron —
* * * * * vom webtrees-Root aus (siehe Installation). - systemd-Timer — die generierte Unit von der Modul-Admin-Seite.
- watch-Daemon — der residente Prozess im Modul, der
tick.php
alle 60 s neu startet und FPM-Worker-Recycling überlebt (Installation, die auch das Lifecycle-Diagramm des Daemons trägt).
Alle drei führen denselben Befehl aus: php modules_v4/cronjob/cli/tick.php cron:tick.
Was ein Tick tut
- Guard —
CliBootstrap::guard() antwortet 403, falls das Skript jemals über HTTP erreicht wird; danach lädt CliBootstrap::boot() webtrees + die Datenbank. - Offline-Check — wenn
data/offline.txt existiert, wird der Tick komplett übersprungen, vor jedem Datenbankzugriff — z. B. während eines webtrees-Updates, wenn Schema und Code im Fluss sind. - Arbeit auswählen — zwei Quellen, in einem Durchlauf kombiniert:
- zeitbasierte Jobs, deren
next_run_at verstrichen ist (Cron-Zeitpläne, UTC); die fälligen Trigger werden relativ zum letzten Lauf berechnet (bzw. zur Erzeugung bei nie gelaufenen Jobs). - Event-Jobs — die
cj_event-Warteschlange wird von der ältesten zuerst geleert, gekoppelt auf einen Lauf pro Ereignisnamen, innerhalb eines 60-Sekunden-Budgets. Event-Jobs erhalten den Ereignisnamen und das Payload über die Umgebungsvariablen CRONJOB_EVENT / CRONJOB_EVENT_PAYLOAD. Siehe Event-Pipeline.
- Jeden Job isoliert ausführen —
JobRunner validiert den Befehl (ein modules_v4/<module>/cli/<script>.php-Pfad oder einer der vier erlaubten Core-Befehle; höchstens 10 Argumente) und startet ihn als Kindprozess mit hartem Timeout (30–3600 s). Es werden nur die letzten 64 KB der Ausgabe erfasst; ein Job, der seinen Timeout erreicht, wird beendet (proc_terminate) und als Exit -9 gemeldet. - Protokollieren — jeder Lauf wird in
cj_run geschrieben (Status, Exit-Code, Dauer, der auslösende Trigger, erfasste Ausgabe) — das ist die Ausführungshistorie. Event-gesteuerte Läufe werden zusätzlich über cj_event_run verknüpft. - Marker — der Tick schreibt
data/cronjob/tick.last. Ist dieser Marker älter als 300 s, meldet das Modul sich im Admin-Status als tick_stale.
Manuelle Ausführung
Den Tick kann man auch manuell ausführen — --dry-run, --job=<name>, --strict und --full-output sind auf der Seite CLI-Skripte beschrieben. Achtung: --full-output kann personenbezogene Daten offenlegen — nur verwenden, wenn der Ausgabeort vertrauenswürdig ist.
4 - Ereignisse
Ereignisgesteuerte Jobs, Webhooks, Pseudo-Events und Route-Events
flowchart TD
subgraph Sources["📡 Event-Quellen"]
WH["`🔗 Webhook<br/>**GET + Token**`"]
DC["`📞 Direkter Aufruf<br/>**EventQueue::push**`"]
PE["📋 Pseudo-Events<br/>Log-Tabelle-Poller"]
RE["🔀 Route-Events<br/>POST auf mutierende Routes"]
end
subgraph Queue["⏳ Queue"]
CE[("`📦 cj_event<br/>**DB-Warteschlange**`")]
end
subgraph Tick["`⏱ Tick *(every 60 s)*`"]
MATCH["Events matchen<br/>coalesced: 1 per name"]
BUDGET["`⏲ **60 s** Wall-Clock-Budget`"]
end
subgraph Execution["⚙ Ausführung"]
JOB["`Job-Skript<br/>child process via **proc_open**`"]
ENV["🌐 CRONJOB_EVENT<br/>CRONJOB_EVENT_PAYLOAD"]
end
subgraph Storage["💾 Speicherung"]
RUN[("cj_run<br/>Laufverlauf")]
ER[("cj_event_run<br/>Verknüpfung")]
end
WH --> Queue
DC --> Queue
PE --> Queue
RE --> Queue
CE --> Tick
MATCH --> BUDGET
BUDGET --> JOB
JOB --> ENV
JOB --> RUN
JOB --> ER
classDef sourceStyle fill:#e3f2fd,stroke:#1565C0,stroke-width:2px
classDef queueStyle fill:#fff3e0,stroke:#E65100,stroke-width:2px
classDef tickStyle fill:#e8f5e9,stroke:#2E7D32,stroke-width:2px
classDef execStyle fill:#fce4ec,stroke:#C2185B,stroke-width:2px
classDef storageStyle fill:#f5f5f5,stroke:#616161,stroke-width:1px
class WH,DC,PE,RE sourceStyle
class CE queueStyle
class MATCH,BUDGET tickStyle
class JOB,ENV execStyle
class RUN,ER storageStyle
Neben Zeitplänen kann ein Job durch ein Ereignis getriggert werden. Webtrees hat kein Ereignissystem, daher sind Ereignisse einfache DB-Zeilen (eine kleine cj_event-Queue), die der Tick einmal pro Minute leert. Ein ereignisgesteuerter Job läuft für ein gequeuedes Ereignis, dessen Namen seinen Ereignisnamen entspricht — koalesziert: pro Tick wird höchstens das neueste Ereignis pro Namen geleert (ein Lauf pro Namen pro Tick), und ältere ausstehende Duplikate des Namens werden verworfen — eine Flut identischer Events queue nie eine Flut von Job-Läufen (Job-Skripte sind per Konvention idempotent/inkrementell).
Benennungsschema (analog zu Job-Namen). Ereignisnamen sind Slugs (a-z 0-9 _ -, max. 64 Zeichen insgesamt), optional namespaced <domain>:<slug>:
| Form | Bedeutung |
|---|
einfacher Slug (z.B. index-dirty) | eigene Events — Webhook oder direkter EventQueue::push() |
cronjob:<slug> | eingebaute Pseudo-Events (Log-Tabelle-Poller) |
_route:<slug> | Route-getriggerte Events (siehe unten) |
<module>:<slug> | von einem Modul angekündigte Events (siehe “Events ankündigen”) |
Ein Ereignis zu queueen hat vier Quellen:
- Webhook (externe Systeme). GET den auf der Admin-Seite gezeigten Endpunkt (Abschnitt “Event webhook”):
GET /module/_cronjob_/Event?event=<name>&data=<json> mit dem Token im X-Cronjob-Token-Header. data ist ein optionales JSON-Objekt, das mit dem Ereignis gespeichert wird. Der Endpunkt erfordert das Token, ist rate-limited (20/min pro Site) und akzeptiert nur Ereignisse, während das Modul aktiviert ist. Es ist ein GET, kein POST: webtrees lehnt POSTs ohne Session-CSRF-Token ab, den ein externer Aufrufender nicht senden kann. - Direkter Aufruf (eigene Module). Aus PHP-Code in einem anderen Modul:
\Schwendinger\Webtrees\Module\Cronjob\Services\EventQueue::push('index-dirty', ['tree' => 'X']); — dies erfordert, dass das Cronjob-Modul installiert ist; schützen Sie es mit class_exists('Schwendinger\Webtrees\Module\Cronjob\Services\EventQueue'). - Pseudo-Events (eingebaute Poller). Webtrees hat keinen Event-Bus, daher werden Core-Aktionen (Logins, fehlgeschlagene Logins, Logouts, Datensatz-Edits, Fehler, Suchen) durch Polling der webtrees-Log-Tabelle beobachtet. Aktivieren Sie den angebotenen
cronjob:pseudo-events-Job und das Modul liest die neuen Log-Einträge höchstens alle 5 Minuten, wobei pro Eintrag ein Event gequeued wird. - Route-Events (eingebaut, request-getriggert). Ein kuratierter Satz mutierender Editor-Routes unter
/tree/{tree}/ (Record-spezifische Routes mit {xref} im Pfad, plus eine kleine Tree-Level-Allowlist) queue sein Ereignis sofort, direkt in der Anfrage, die die Änderung vorgenommen hat.
Der Tick gleicht jedes ausstehende Ereignis mit den aktivierten Jobs ab, die einen Ereignis-Trigger für diesen Namen tragen (ein Job kann mehrere Ereignis-Trigger haben oder keiner), führt sie aus und markiert dann das Ereignis als verarbeitet (einmal verbraucht — auch bei Fehler, damit ein fehlerhafter Job dasselbe Ereignis nicht ewig neu ausführt). Der Drain ist begrenzt: er nimmt das neueste Ereignis pro Namen (Koaleszierung, siehe oben) und stoppt nach einem 60-s-Wall-Clock-Budget, damit eine gesättigte Queue den Rest des Ticks nicht verhungern lässt — die verbleibenden Events setzen einfach mit dem nächsten Tick fort. Jeder verbrauchende Lauf wird zusätzlich über die cj_event_run-Kreuztabelle mit dem Ereignis verknüpft (ein Ereignis kann mehrere Jobs triggern), und die Ausführungshistorie zeigt, welches Ereignis einen Lauf ausgelöst hat. Verarbeitete Events werden nach 7 Tagen gelöscht (ihre cj_event_run-Links werden per FK-Cascade entfernt); Events ohne passenden Job werden verworfen, damit die Queue nicht wächst.
Ereignis-Payload im Kindprozess. Event-Läufe übergeben das Ereignis dem Job-Skript über die Prozessumgebung (das argv ist für die statischen, strikt validierten Argumente des Jobs reserviert):
| Variable | Gesetzt bei | Wert |
|---|
CRONJOB_EVENT | jeder Event-Lauf | der Ereignisname (dient zugleich als Marker “dies ist ein Event-Lauf”) |
CRONJOB_EVENT_PAYLOAD | Event-Lauf mit nicht-leerem Payload | der Event-Payload als JSON (Route-Parameter, Log-Tabelle-Einträge, Webhook-data) |
$event = getenv('CRONJOB_EVENT'); // false bei Zeitplan-/manuellen Läufen
$payload = json_decode((string) getenv('CRONJOB_EVENT_PAYLOAD'), true) ?: [];
$xref = $payload['xref'] ?? ''; // z.B. von einem _route:* Event
- Variable nicht vorhanden = kein Payload. Der Payload wird beim Queueen auf 4000 Bytes gekürzt; ein JSON, das mitten durchgeschnitten ist, dekodiert nicht mehr, in dem Fall ist
CRONJOB_EVENT_PAYLOAD gar nicht gesetzt (nie leer, nie gebrochenes JSON). - Behandeln Sie den Payload als nicht vertrauenswürdige Eingabe — vor allem für Webhook-
data, dessen Absender extern ist. Validieren Sie Werte (XREFs, Namen, …) vor der Verwendung. - Der Payload ist unabhängig vom Befehlstyp: Modul-CLI-Skripte und Core-Befehle (die die Variablen einfach ignorieren) funktionieren beide bei Event-Läufen.
- Der Payload ist auch in der Ausführungshistorie (Ereignis-Detail) sichtbar, unabhängig vom Kindprozess.
4.1 - Pseudo-Events
Log-Tabelle-Polling
Webtrees hat keinen Event-Bus, daher werden Core-Aktionen (Logins, fehlgeschlagene Logins, Logouts, Datensatz-Edits, Fehler, Suchen) durch Polling der webtrees-Log-Tabelle (app/Log.php, Tabelle log) detektiert und pro neuem passenden Log-Eintrag ein Ereignis ausgelöst. Es ist standardmäßig deaktiviert: nichts läuft, bis der angebotene cronjob:pseudo-events-Job aktiviert wird.
Eingebaute Events (ein cj_event pro neuer Log-Zeile, Payload = die Zeile):
| Ereignis | Log-Filter (log_type, Nachricht-Präfix) | Feuer bei |
|---|
cronjob:log-auth-failed | auth, Login failed | ein Login-Versuch fehlgeschlagen (falsches Passwort, unbekannter Benutzer, nicht verifiziert, nicht genehmigt, keine Session-Cookies) |
cronjob:log-auth-login | auth, Login: | ein Benutzer eingeloggt |
cronjob:log-auth-logout | auth, Logout: | ein Benutzer ausgeloggt |
cronjob:log-error | error, – | eine Ausnahme erfasst und geloggt (die Nachricht ist der Trace) |
cronjob:log-edit-update | edit, Update: | ein Datensatz aktualisiert (GedcomRecord::updateRecord()) |
cronjob:log-edit-delete | edit, Delete: | ein Datensatz gelöscht (GedcomRecord::deleteRecord()) |
cronjob:log-search | search, – | eine Suche ausgeführt (eine Log-Zeile pro durchsuchtem Baum) |
Payload: log_id, log_time, log_message (auf 250 Zeichen gekürzt, UTF-8-safe) plus gedcom_id / user_id, wenn die Log-Zeile sie trägt. Die Nachrichten-Präfixe sind die hartkodierten englischen Strings des aktuellen Cores; ein Core-Update, das sie ändert, stoppt das passende Ereignis still (kein Fallback per Design) — die Event-Katalog-Beschreibungen und die test-pseudo-events-Suite fixieren die aktuelle Zuordnung.
So läuft es
- Ein Trigger, ein Cooldown. Die Detektion läuft nur vom angebotenen
cronjob:pseudo-events-Job (*/5 * * * * per Default), im Kindprozess des Ticks — isoliert von jeder Web-Anfrage und durch den Job-Timeout begrenzt, sodass ein schwerer Detektor nie eine Page-Load blockieren oder verlangsamen kann. Ein 5-Minuten-Cooldown (ein data/ mtime-Marker) plus ein Lock halten wiederholte Läufe günstig und idempotent; der Cooldown begrenzt auch einen häufigeren Job-Cron. - Vom Job aktiviert, pro Ereignis gated. Das Aktivieren des
cronjob:pseudo-events-Jobs schaltet die Detektion ein (es gibt keinen separaten Schalter — der Tick führt nur aktivierte Jobs aus). Ein Detektor, dessen Ereignis kein aktiver ereignisgesteuerter Job abhört, wird komplett übersprungen — keine Abfrage, kein Push. - Checkpoint + Backpressure. Jeder Detektor erinnert sich an die höchste
log_id, die er verarbeitet hat (Statusdatei). Höchstens 200 Zeilen werden pro Lauf pro Detektor ausgelöst; der Checkpoint rückt nur bis zur letzten ausgelösten Zeile vor, sodass eine Flut über mehrere Läufe abgearbeitet wird — kein Datenverlust, nur Latenz. - Baseline beim ersten Lauf. Die erste Detektion erfasst nur die aktuelle max
log_id und löst nichts aus, sodass das Aktivieren des Jobs nicht rückwirkend auf bestehende Daten feuert. (Erneutes Abhören eines zuvor nicht abgehörten Events holt seinen Backlog nach, begrenzt wie oben.) - Reset, nicht neu feuern, bei Tabellenschrumpfung. Wenn die Tabelle unter einen Checkpoint schrumpfte (TRUNCATE / DB-Restore — einfache Zeilend deletions können das nicht, MySQL Auto-Increment läuft nach Deletes weiter), baselined der Detektor still neu.
- Offline-sicher & isoliert. Übersprungen, während
data/offline.txt existiert; ein kaputter Detektor bricht nie die anderen; die Detektion ist serialisiert (flock) und der Status wird atomar nach data/cronjob/pseudo-events.json persistiert.
Die Log-Tabelle ist die Quelle der Wahrheit: unter einer Flut führt die Koaleszierung des Ticks einen Job einmal pro Ereignisnamen pro Tick mit dem neuesten Payload aus — ältere Zeilen werden nicht neu zugestellt, aber sie bleiben in der Log-Tabelle, und log_id im Payload ermöglicht einem Job, der Vollständigkeit braucht, den Bereich selbst neu zu lesen.
Wenn ein Ereignisname verschwindet, markiert die Admin-Tabelle solche Trigger mit dem unknown event-Badge.
Detektierte Events gehen in dieselbe cj_event-Queue wie Webhooks und werden vom normalen Event-Drain des Ticks konsumiert. Um darauf zu reagieren, erstellen Sie einen ereignisgesteuerten Job, dessen Ereignisname passt (z.B. bei cronjob:log-auth-failed Alarm, bei cronjob:log-edit-update neu indexieren).
4.2 - Route-Events
request-getriggert
Ein ergänzender, sofortiger Detektionsmechanismus: anstatt Zustand nach Zeitplan zu pollen, beobachtet das Modul die HTTP-Anfrage selbst. Wenn eine Anfrage an eine abgebildete mutierende Route erfolgreich bearbeitet wurde (2xx/3xx), queue sie das passende Ereignis in dieselbe cj_event-Queue — ein abhörender Event-Job reagiert dann beim nächsten Tick (~60 s), ohne Polling-Latenz.
Welche Routes abgebildet sind — zwei Regeln, beide abgeleitet aus der live webtrees-Routentabelle. Gemeinsame Voraussetzungen: Pfad beginnt mit /tree/{tree}/, der Handler ist im webtrees-Request-Handler-Namespace (RequestHandlers in 2.2.6, Controllers in 2.3), und die Anfrage ist eine POST-Mutation. In 2.2.6 wird dies bei der Abbildung erzwungen (GET-Seiten-Routes werden nicht abgebildet); in 2.3 trägt die Route keine HTTP-Methode, daher erfolgt die POST-Prüfung zur Auslösezeit und das GET-Formular-Load derselben Route wird ignoriert.
- Record-Routes (regelbasiert — Core-Updates werden automatisch übernommen): der Pfad enthält
{xref} (den Ziel-Datensatz) und der Handler-Klassenname beginnt mit Edit, Delete, Add, Create, Link, Paste, Reorder oder Pending. Der Ereignisname ist das dritte Pfad-Segment, namespaced _route:<segment>; wenn mehrere abgebildete Routes ein Segment teilen (delete, edit-raw, accept, reject), werden die folgenden Pfad-Parameter angehängt, Bindestrich-getrennt. - Tree-Level-Routes (kleine kuratierte Allowlist): die signifikanten baumweiten Mutationen, die kein
{xref} in der URL tragen (Import, Merge, Renumber, …). Jeder Allowlist-Eintrag hat einen expliziten Ereignisnamen. Neue baumweite Mutationen in einem Core-Update brauchen ein Modul-Update (die Record-Route-Regel bleibt voll automatisch).
| Ereignis | Route (unter /tree/{tree}/) | Payload |
|---|
_route:edit-note-object | POST edit-note-object/{xref} | tree, xref |
_route:update-record | POST update-record/{xref} | tree, xref |
_route:update-fact | POST update-fact/{xref}{/fact_id} | tree, xref (, fact_id) |
_route:delete-xref / _route:delete-xref-fact_id | POST delete/{xref}{/fact_id} | tree, xref (, fact_id) |
_route:edit-raw-xref / _route:edit-raw-xref-fact_id | POST edit-raw/{xref}{/fact_id} | tree, xref (, fact_id) |
_route:add-child-to-family, _route:add-spouse-to-family, _route:add-media-file | POST add-…/{xref} | tree, xref |
_route:add-child-to-individual, _route:add-parent-to-individual, _route:add-spouse-to-individual | POST add-…/{xref} | tree, xref |
_route:paste-fact | POST paste-fact/{xref} | tree, xref |
_route:link-media-to-record | POST link-media-to-record/{xref} | tree, xref — das Media-Objekt; der Ziel-Datensatz ist ein Formularfeld, nicht in der URL |
_route:link-child-to-family, _route:link-spouse-to-individual | POST link-…/{xref} | tree, xref |
_route:reorder-children, _route:reorder-spouses, _route:reorder-media, _route:reorder-media-files, _route:reorder-names | POST reorder-…/{xref} | tree, xref |
_route:accept-xref / _route:accept-xref-change | POST accept/{xref}{/change} | tree, xref (, change) |
_route:reject-xref / _route:reject-xref-change | POST reject/{xref}{/change} | tree, xref (, change) |
_route:import | POST import | tree |
_route:load | POST load | tree — die chunkweise Import-Fortsetzung feuert einmal pro Chunk |
_route:merge-step1 / _route:merge-step2 | POST merge-step1 / merge-step2 | tree |
_route:search-replace | POST search-replace | tree |
_route:renumber | POST renumber | tree |
_route:data-fix-update / _route:data-fix-update-all | POST data-fix/{data_fix}/update[-all] | tree, data_fix |
_route:accept / _route:reject | POST accept / reject (Stück) | tree |
_route:change-family-members | POST change-family-members | tree — der Familien-XREF ist ein Formularfeld |
Insgesamt 38 Events (27 Record-Routes + 11 Tree-Level-Routes). Der Payload enthält immer tree (ID) + tree_name plus alle erkannten Route-Parameter. Record-Routes (mit {xref}) tragen zusätzlich change_pending (bool): ob die Änderung nach dieser Anfrage noch in der change-Tabelle aussteht (die Auto-Akzept-Präferenz des Benutzers ist aus) oder bereits angewendet (Auto-Akzept an). Eine ausstehende Änderung wird angewendet, wenn ein Admin sie akzeptiert — was ihre eigenen _route:accept*-Events feuert. Tree-Level-Routes schreiben direkt (keine change-Zeilen) und tragen kein solches Flag.
Breaking Change (diese Version). Record-Routes ohne {xref} in der URL — alle _route:create-*-Events plus _route:add-unlinked-individual — werden nicht mehr abgebildet: sie tragen keinen Ziel-Datensatz. Jobs, die auf diese Events hören, feuern nie wieder; die Admin-Tabelle markiert solche Trigger mit einem unknown route event-Badge (eine generische Prüfung aller _route:*-Trigger aller Jobs gegen die live Map), sodass das Umrichten sichtbar statt still ist.
_route:accept* / _route:reject* feuern, wenn eine ausstehende Änderung akzeptiert oder abgelehnt wird — die Akzeptierung einer ausstehenden Löschung ist der Moment, in dem der Datensatz tatsächlich entfernt wird.
So funktioniert es
- Doppelt gated, standardmäßig aus. Ein Event feuert nur, während der Admin-Schalter “Route-Events” (Events-Abschnitt) an ist und mindestens ein aktiver ereignisgesteuerter Job auf diesen Ereignisnamen hört. Eine nicht abgebildete Anfrage kostet nichts (eine In-Memory-Suche); nur eine erfolgreiche Anfrage auf einer abgebildeten Route macht zwei winzige indexierte DB-Lesevorgänge.
- Transaktional. Der
cj_event-Insert läuft in der gleichen DB-Transaktion wie die Mutation (das Modul-Middleware sitzt innerhalb der webtrees-Request-Transaktion), damit das gequeue Event mit der Änderung zusammen committet oder zurückgerollt wird — kein Drift, kein Doppel-Feuer bei fehlgeschlagenem Speichern. - Blockiert nie. Die Detektion läuft nach dem Handler, ist exceptions-umhüllt und kann die Antwort nie verändern.
- Robust gegenüber Core-Upgrades. Die Map wird aus der Routentabelle neu aufgebaut (geschlüsselt nach Handler-Klassen-Strings, kein
::class), sodass ein umbenannter/entfernter Handler einfach aufhört zu matchen statt zu fatale. Wenn die Routentabelle nicht verfügbar ist (z.B. CLI), hält eine statische Fallback-Map mit der Note-Route den Piloten am Laufen. Die Record-Route-Regel ist voll automatisch; nur die kleine Tree-Level-Allowlist ist kuratiert (siehe oben), und Jobs, die auf ein nicht mehr existierendes Ereignis zeigen — ein entferntes Route-Event oder ein entferntes eingebautes Pseudo-Event — werden in der Admin-Tabelle markiert (unknown event-Badge).
Um darauf zu reagieren, erstellen Sie einen ereignisgesteuerten Job, dessen Ereignisname passt (z.B. _route:update-record) und aktivieren Sie den “Route-Events”-Schalter. Beachten Sie die Überlappung mit den cronjob:log-edit-update / cronjob:log-edit-delete-Pollern: jede Record-Edit/Delete wird auch in die Log-Tabelle geschrieben, sodass das passende Log-Event beim nächsten Poll ebenfalls folgt — wählen Sie den Mechanismus, dessen Latenz/Präzision zum Job passt (Route-Events sind quasi-sofort und pro Objekt präzise, und tragen change_pending; Log-Poller hängen ~5 min hinterher, fangen aber auch Änderungen außerhalb der Web-UI).
5 - Howto
Wie können andere benutzerdefinierte Module dieses Cronjob-Modul nutzen?
Es können Informationen zum Status des Dienstes oder zu verwalteten Aufträgen abgefragt werden.
Außerdem ist es möglich, eigene CLI-Skripte zu registrieren, die von diesem Dienst verwaltet und ausgeführt werden sollen.
Einzelheiten können den folgenden Abschnitten entnommen werden.
5.1 - Service API
Nutzung des Cronjob-Dienstes aus anderen Modulen
Benachbarte Module fragen den Status des Moduls über eine stabile, schreibgeschützte Schnittstelle (CronjobService) ab, anstatt auf dessen interne Dienste zurückzugreifen. Überprüfen Sie zunächst die Verfügbarkeit –
das Modul ist möglicherweise nicht installiert, und die Klasse kann nur automatisch geladen werden, wenn die Datei
module.php
geladen wurde:
<?php
use Schwendinger\Webtrees\Module\Cronjob\Services\CronjobService;
if (class_exists(CronjobService::class) && CronjobService::isFunctional()) {
$status = CronjobService::jobStatus('mymodule:backup'); // null = unknown job
$age = CronjobService::lastTickAge(); // seconds since last tick
$listened = CronjobService::listenedEvents(); // event name => job names
}
| Frage | Methode(n) |
|---|
| Ist der Dienst funktionsfähig? | isFunctional(), problems() (Codes: module_disabled, not_migrated, cron_library_missing, tick_stale) |
| Welche Funktionen sind aktiviert? | features() (watch, pseudo_events, route_events), watchStatus() |
| Wann wurde der letzte Tick ausgeführt? | lastTick() / lastTickAge() – beide Trigger-Typen (Marker-Datei data/cronjob/tick.last) |
| Ist ein Job aktiviert / wann wurde er zuletzt ausgeführt? | isJobEnabled($name), jobStatus($name), lastRun($name) – Jobnamen sind mit einem Namespace versehen (<Modul>:<Name>) |
| Auf welche Ereignisse wird gewartet? | listenedEvents(), listenersFor($event); Kataloge: eventCatalog(), pseudoEvents(), routeEvents() |
| Aktionen (die einzigen beiden) | runJobNow($name) (wird beim nächsten Tick in die Warteschlange gestellt), pushEvent($name, $payload) (Ereigniswarteschlange) |
Vertrag: Schreibgeschützte Methoden lösen niemals einen Fehler aus, wenn das Schema fehlt (sichere Standardwerte),
Beschreibungen sind Quelltext-Strings (werden niemals vorab übersetzt), und lastRun() schließt bewusst
die Job-Ausgabe aus, da diese personenbezogene Daten enthalten kann.
5.2 - Selbst-Registrierung
Wie andere Module ihre Jobs dem Cronjob-Modul anbieten
Andere Module können ihre geplanten Jobs anbieten, damit sie in der Admin-UI erscheinen, ohne dass jemand die Registry manuell editiert. Dazu können zwei Wege beschritten werden (ein Modul kann beide verwenden) - beide werden automatisch bei jedem Tick entdeckt, wobei dies nur für aktivierte Module gilt:
Marker-Methode
getCronJobs(): array in der Modul-module.php
-Klasse, die dasselbe Format zurückgibt. Entsprechend: getModuleEvents(): array, getModuleCommands(): array.
Manifest-Datei (empfohlen)
Bereitstellung einer Datei im Modulverzeichnis (modules_v4/<module>/cron-jobs.php), die ein Array von Job-Spezifikationen zurückgibt:
<?php
// modules_v4/linkenhancer/cron-jobs.php
declare(strict_types=1);
return [
[
'name' => 'link-index',
'title' => 'Update the link index',
'triggers' => [
['type' => 'time', 'cron' => '*/30 * * * *'],
],
'command_type' => 'module',
'command' => 'modules_v4/linkenhancer/cli/build-link-index.php',
'args' => '--limit=5000',
],
];
Übersetzbare title / description-Texte in einem Manifest sollten in MoreI18N::translate() (Identitäts-Markierung) gesetzt werden,
damit xgettext sie aufnimmt — die Übersetzung erfolgt zur Render-Zeit, nie beim laden des Manifests.
Job-Spezifikations-Felder
| Schlüssel | Erforderlich | Bedeutung |
|---|
name | ja | Slug a-z 0-9 _ -, max. 64; gespeichert als <module>:<name> |
title | nein | menschlicher Name (Default = name) |
triggers | ja | Liste von Trigger-Einträgen, mindestens einer |
command_type | ja | module oder core |
command | ja | der Befehl |
args | nein | einfache Option/Wert-Tokens |
enabled | nein | Default false — entdeckte Jobs werden deaktiviert erstellt |
timeout_sec | nein | Default 300, Bereich 30 - 3600 |
Sobald ein entdeckter Job eingefügt ist, ist er unter Admin-Kontrolle: spätere Ticks überschreiben ihn nie.
Ereignisse ankündigen
Dasselbe Manifest kann auch Ereignisse ankündigen, die das Modul auslöst (damit sie im
Ereignis-Katalog und in den Vorschlägen des Job-Formulars erscheinen). Anstelle einer
einfachen Job-Liste kann das Manifest ein assoziatives Array mit optionalen jobs- und
events-Listen zurückgeben (eine einfache Liste bedeutet weiterhin „nur Jobs"):
<?php
// modules_v4/linkenhancer/cron-jobs.php
declare(strict_types=1);
return [
'jobs' => [
[
'name' => 'link-index',
'triggers' => [
['type' => 'time', 'cron' => '*/30 * * * *'],
// A job may combine several triggers:
// ['type' => 'event', 'event' => 'linkenhancer:index-dirty'],
],
'command_type' => 'module',
'command' => 'modules_v4/linkenhancer/cli/build-link-index.php',
],
],
'events' => [
['name' => 'index-dirty', 'description' => 'The link index is out of date.', 'payload' => ['tree']],
],
];
- Ein angekündigtes Ereignis ist namespaced
<module>:<name> (wie die angebotenen Job-Schlüssel). - Der
event-Wert eines Ereignis-Triggers, der keinen Doppelpunkt trägt, wird automatisch
namespaced <module>:<event>; ein Name, der bereits einen Doppelpunkt hat (z.B.
cronjob:log-edit-update), bleibt unverändert. - Entsprechende Marker-Methode:
getModuleEvents(): array (dasselbe Event-Spec-Format).
Event-Spezifikations-Felder
| Schlüssel | Erforderlich | Bedeutung |
|---|
name | ja | Slug a-z 0-9 _ - (gespeichert als <module>:<name>) |
description | nein | wird im Ereignis-Katalog angezeigt (Übersetzungsschlüssel, max. 255) |
payload | nein | Liste von Payload-Parameter-Namen (max. 8), wird im Katalog angezeigt |
Befehle ankündigen
Dasselbe Manifest kann auch Befehle ankündigen, die ein Job ausführen kann, mit ihren
verfügbaren Parametern. Eine einfache Manifest-Liste bedeutet weiterhin „nur Jobs"; um
Befehle anzukündigen, gibt das Manifest die assoziative Form mit einer commands-Liste zurück:
// modules_v4/linkenhancer/cron-jobs.php
declare(strict_types=1);
return [
'jobs' => [ /* … */ ],
'commands' => [
[
'command' => 'modules_v4/linkenhancer/cli/build-link-index.php',
// 'command_type' => 'module', // optional - derived from the command value
'description' => 'Rebuild the link index incrementally.',
'params' => [
['name' => '--limit', 'optional' => true, 'default' => '5000', 'description' => 'max records per run'],
['name' => '--since', 'optional' => true, 'description' => 'only changes since this timestamp'],
],
],
],
];
Command-Spezifikations-Felder
| Schlüssel | Erforderlich | Bedeutung |
|---|
command | ja | ein modules_v4/…/cli/*.php-Pfad oder ein erlaubter Core-Befehl, max. 255 |
command_type | nein | module oder core (wird bei weggelassenem Wert aus command abgeleitet) |
description | nein | wird im Befehl-Katalog angezeigt (Übersetzungsschlüssel, max. 255) |
params | nein | Liste von Parameter-Deskriptoren (max. 8) |
Jeder Parameter-Deskriptor hat name (das Option-Token, z.B. --limit), optional
(Bool, Default true — auf false setzen für eine erforderliche Option), default
(ein optionaler Wert) und description (optional). Entsprechende Marker-Methode:
getModuleCommands(): array (dasselbe Command-Spec-Format).
Kurations-Regel: Ein Modul, das Befehle ankündigt, kuratiert seine eigene Befehlsliste —
die glob-basierte Entdeckung listet seine Skripte nicht mehr auf, sodass interne Skripte
(tick.php
, watch.php
, wrap.php
, smoke-job.php
) nicht als Job-Befehle angeboten
werden. Ein Modul, das nichts ankündigt, fällt auf die glob-Entdeckung seiner
cli/-Skripte zurück (abwärtskompatibel).
6 - CLI-Skripte
Command-Line-Skripte und Wartung
Skripte
php modules_v4/cronjob/cli/tick.php # alle fälligen Jobs ausführen
php modules_v4/cronjob/cli/tick.php --dry-run # anzeigen, was ausgeführt würde
php modules_v4/cronjob/cli/tick.php --job=<name> # einen bestimmten Job jetzt ausführen
php modules_v4/cronjob/cli/tick.php --strict # Exit 1 wenn ein Job fehlgeschlagen ist
php modules_v4/cronjob/cli/tick.php --full-output # vollständige Job-Ausgabe echo (Debugging!)
php modules_v4/cronjob/cli/watch.php # Watch-Daemon starten
Exit-Codes: 0 = Tick lief (Job-Fehler werden in der DB protokolliert), 1 = Umgebungsproblem oder (mit --strict) mindestens ein Job fehlgeschlagen.
Log-Hygiene: stdout trägt nur Summary-Zeilen. Die vollständige Job-Ausgabe liegt in der Ausführungshistorie (nur Admin). --full-output niemals in einen weltlesbaren Ort umleiten.
Für die Fehlersuche unterstützt das tick.php
Skript auch die Parameter -v und -vv, damit die Konsolen-Ausgabe ausführlicher ausfällt.
Acceptance-Test
php modules_v4/cronjob/cli/smoke-job.php # zeigt ok, Exit 0
php modules_v4/cronjob/cli/smoke-job.php --sleep=10 # lebt 10 s (Timeout-Test)
Erstellen Sie einen Job mit Befehl modules_v4/cronjob/cli/smoke-job.php, klicken Sie Jetzt ausführen, warten Sie eine Minute — die Verlauf-Seite sollte eine grüne ok-Zeile zeigen.
Konventionen für Job-Skripte
Neue Job-Skripte in anderen Modulen müssen diese Konventionen einhalten:
- Ort
modules_v4/<module>/cli/, aufgerufen als php modules_v4/<module>/cli/<script>.php - Beginnen mit
require autoload.php und CliBootstrap::guard() - Idempotent und inkrementell (
--limit, --since), batched - Exit-Code
0 bei Erfolg - Keine interaktive Eingabe; alles über CLI-Optionen
- Template-Dateien sind mit
_ Präfix versehen (von der Job-Entdeckung ausgeschlossen) - Eigene CLI-Skripte können leicht auf folgende Weise begonnen werden:
- einer Vorlage:
_template-maintenance.php
– diese in das Verzeichnis cli/ des eigenen Moduls kopieren, umbenennen und entsprechend anpassen - oder, falls das Modul bereits von Cronjobs abhängt, kann dieser Bootstrap-Vorgang stattdessen
an den gemeinsamen CLI-Wrapper delegiert werden
Wartungsskripte schreiben
Die Vorlage verwenden
_template-maintenance.php
ist eine Vorlage zum Kopieren und Einfügen für länger laufende Wartungsaufträge.
Die Konventionen:
- SAPI-Guard: Jedes Skript beginnt mit
CliBootstrap::guard() – das Verzeichnis ist über eine URL erreichbar, der Guard gibt den Status 403 zurück, anstatt das Skript auszuführen. - Bootstrap: Verwende
CliBootstrap::boot() – die zentrale CLI-Bootstrap-Sequenz (App-Bootstrap, i18n, Konfiguration, Datenbank) mit einem harten Fehler bei fehlender Konfiguration oder fehlender Datenbankverbindung. - Argumente: einfaches
$argv (--limit=N, --help). - Sperre:
flock() auf data/<module>/<name>.lock, damit sich überlappende Cron-Läufe nicht kollidieren; eine gehaltene Sperre ist kein Fehler (Beendigungscode 0). - Idempotente Batches: Gestalten Sie den Job so, dass ein Lauf in etwa 5–10 Minuten abgeschlossen ist und der nächste Lauf dort fortgesetzt wird, wo der letzte aufgehört hat.
- Protokollierung: einfache Zeilen in der Standardausgabe (Cron-E-Mail / Protokolldatei). Geben Sie niemals personenbezogene Daten aus – Zähler und XREFs, niemals Namen.
- Exit-Codes:
0 = ok (einschließlich „nichts zu tun“, „Sperre gehalten“), 1 = Fehler.
CLI-Wrapper verwenden
Ein Job-Skript, das webtrees + Datenbank braucht, kann den Bootstrap an den gemeinsamen Wrapper des Cronjob-Moduls delegieren:
- Payload
modules_v4/<module>/cli/<name>.logic.php — die eigentliche Logik. - Stub
modules_v4/<module>/cli/<name>.php — worauf die Job-Zeile zeigt:
<?php
declare(strict_types=1);
$wrapper = __DIR__ . '/../../cronjob/cli/wrap.php';
if (is_file($wrapper)) {
require $wrapper;
} else {
fwrite(STDERR, "cronjob module required for this job\n");
exit(1);
}
wrap.php
bootstrapt webtrees + DB und inkludiert dann das Payload (Geschwisterdatei mit .logic.php-Erweiterung). Der Payload-Pfad wird aus dem Entry-Skript abgeleitet, nie aus einem Argument genommen.
7 - Sicherheit
Sicherheitsmodell und Befehlsvalidierung
- Nie eine Shell. Befehle werden via
proc_open mit einem argv-Array ausgeführt. - Befehls-Whitelist. Modulskripte müssen
modules_v4/<module>/cli/<script>.php entsprechen, existieren und sich in einem modules_v4/<module>/cli/-Verzeichnis auflösen. Core-Befehle sind auf die Read-only/Export-Allowlist beschränkt. - Argument-Validierung. Jedes Argument-Token muss eine einfache Option oder ein Wert sein — keine Shell-Metazeichen, keine Leerzeichen in Tokens, max. 10 Tokens.
- Arbeitsverzeichnisse. Modulskripte laufen vom webtrees-Root; Core-Befehle laufen von
data/. - Keine sensiblen Daten in stdout. Der Tick gibt nur Zusammenfassungen aus; die vollständige Ausgabe bleibt in der Ausführungshistorie (admin-only).
- UI-Aktionen sind alle als
*Admin* benannt (nur Admin, CSRF-geschützt). - Notaus-Schalter: Modul deaktivieren — der Tick tut dann nichts.
- Der Tick ist Single-Instance (flock), und der Kindprozess eines Jobs wird nach dem Timeout hart getötet (
SIGKILL). - Bündelung von Ereigniswarteschlangen. Eine Flut identischer Ereignisse wird zusammengeführt zu einem Joblauf.
- Webhook-Token nur im Header. Das Token wird ausschließlich im
X-Cronjob-Token-Header gesendet.