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

License: GPL v3 webtrees major version

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

Cronjob Scheduler

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
  1. Modulordner nach modules_v4/cronjob/ kopieren (ZIP-Installation geht auch). Die Cron-Bibliothek ist im Modul enthalten — nichts zu holen.
  2. Webtrees einmal öffnen (irgendeine Seite als Admin), damit das Modul-boot() seine Tabellen erstellt.
  3. 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).
  4. Trigger-Installation (einmalig) — siehe unten.
  5. Erstellen Sie Ihren ersten Job in der Admin-UI oder konfigurieren und aktivieren Sie registrierte Jobs.

Trigger-Installation (einmalig)

Installation

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:

  1. Events, von Modulen angekündigt (Manifest-events-Abschnitt / getModuleEvents()), namespaced <module>:<name>
  2. eingebaute Pseudo-Events (cronjob:*, von den Detektoren)
  3. Route-Events (_route:*, abgeleitet aus der live Routentabelle)
  4. 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.

Events

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):

  1. die allowlisteten Core-Befehle (tree-export, tree-list, user-list, site-setting), mit ihren bekannten Parametern
  2. Befehle, von Modulen angekündigt (Manifest-commands-Abschnitt / getModuleCommands()) — mit einer Beschreibung und strukturierten Parametern
  3. 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.

Befehle

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:

FeldBedeutung
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.
Titelmenschenlesbarer Name
Triggerein 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.
Befehlein modules_v4/<module>/cli/<script>.php-Pfad oder ein erlaubter Core-Befehl.
Argumentenur einfache Optionen/Werte (z.B. --limit=5000), max. 10 Tokens
Timeout30 - 3600 s
Bei Fehler benachrichtigenOpt-in: Admin-Konten bei Job-Fehlern benachrichtigen

Verfügbare Ereignisnamen und Befehle werden als Datalists bereitgestellt, um die Dateneingabe zu erleichtern.

Neuer Cronjob

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).

Ausführungshistorie

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

  1. Guard — CliBootstrap::guard() antwortet 403, falls das Skript jemals über HTTP erreicht wird; danach lädt CliBootstrap::boot() webtrees + die Datenbank.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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>:

FormBedeutung
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:

  1. 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.
  2. 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').
  3. 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.
  4. 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):

VariableGesetzt beiWert
CRONJOB_EVENTjeder Event-Laufder Ereignisname (dient zugleich als Marker “dies ist ein Event-Lauf”)
CRONJOB_EVENT_PAYLOADEvent-Lauf mit nicht-leerem Payloadder 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):

EreignisLog-Filter (log_type, Nachricht-Präfix)Feuer bei
cronjob:log-auth-failedauth, Login failedein Login-Versuch fehlgeschlagen (falsches Passwort, unbekannter Benutzer, nicht verifiziert, nicht genehmigt, keine Session-Cookies)
cronjob:log-auth-loginauth, Login:ein Benutzer eingeloggt
cronjob:log-auth-logoutauth, Logout:ein Benutzer ausgeloggt
cronjob:log-errorerror, –eine Ausnahme erfasst und geloggt (die Nachricht ist der Trace)
cronjob:log-edit-updateedit, Update:ein Datensatz aktualisiert (GedcomRecord::updateRecord())
cronjob:log-edit-deleteedit, Delete:ein Datensatz gelöscht (GedcomRecord::deleteRecord())
cronjob:log-searchsearch, –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.

  1. 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.
  2. 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).
EreignisRoute (unter /tree/{tree}/)Payload
_route:edit-note-objectPOST edit-note-object/{xref}tree, xref
_route:update-recordPOST update-record/{xref}tree, xref
_route:update-factPOST update-fact/{xref}{/fact_id}tree, xref (, fact_id)
_route:delete-xref / _route:delete-xref-fact_idPOST delete/{xref}{/fact_id}tree, xref (, fact_id)
_route:edit-raw-xref / _route:edit-raw-xref-fact_idPOST edit-raw/{xref}{/fact_id}tree, xref (, fact_id)
_route:add-child-to-family, _route:add-spouse-to-family, _route:add-media-filePOST add-…/{xref}tree, xref
_route:add-child-to-individual, _route:add-parent-to-individual, _route:add-spouse-to-individualPOST add-…/{xref}tree, xref
_route:paste-factPOST paste-fact/{xref}tree, xref
_route:link-media-to-recordPOST 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-individualPOST link-…/{xref}tree, xref
_route:reorder-children, _route:reorder-spouses, _route:reorder-media, _route:reorder-media-files, _route:reorder-namesPOST reorder-…/{xref}tree, xref
_route:accept-xref / _route:accept-xref-changePOST accept/{xref}{/change}tree, xref (, change)
_route:reject-xref / _route:reject-xref-changePOST reject/{xref}{/change}tree, xref (, change)
_route:importPOST importtree
_route:loadPOST loadtree — die chunkweise Import-Fortsetzung feuert einmal pro Chunk
_route:merge-step1 / _route:merge-step2POST merge-step1 / merge-step2tree
_route:search-replacePOST search-replacetree
_route:renumberPOST renumbertree
_route:data-fix-update / _route:data-fix-update-allPOST data-fix/{data_fix}/update[-all]tree, data_fix
_route:accept / _route:rejectPOST accept / reject (Stück)tree
_route:change-family-membersPOST change-family-memberstree — 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
}
FrageMethode(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üsselErforderlichBedeutung
namejaSlug a-z 0-9 _ -, max. 64; gespeichert als <module>:<name>
titleneinmenschlicher Name (Default = name)
triggersjaListe von Trigger-Einträgen, mindestens einer
command_typejamodule oder core
commandjader Befehl
argsneineinfache Option/Wert-Tokens
enabledneinDefault false — entdeckte Jobs werden deaktiviert erstellt
timeout_secneinDefault 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üsselErforderlichBedeutung
namejaSlug a-z 0-9 _ - (gespeichert als <module>:<name>)
descriptionneinwird im Ereignis-Katalog angezeigt (Übersetzungsschlüssel, max. 255)
payloadneinListe 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üsselErforderlichBedeutung
commandjaein modules_v4/…/cli/*.php-Pfad oder ein erlaubter Core-Befehl, max. 255
command_typeneinmodule oder core (wird bei weggelassenem Wert aus command abgeleitet)
descriptionneinwird im Befehl-Katalog angezeigt (Übersetzungsschlüssel, max. 255)
paramsneinListe 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:

  1. Payload modules_v4/<module>/cli/<name>.logic.php — die eigentliche Logik.
  2. 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.