feat: stille Datenverlust-Pfade geschlossen, gemeinsame Shell-Lib (v0.7.0)
Vor dem Rollout durchgesehen und die verbliebenen Stellen geschlossen, an denen etwas schiefgehen konnte, ohne dass es irgendwo sichtbar wurde. Datenverlust: - veraPDF: das in [verapdf].binary konfigurierte Programm wird im Preflight geprueft. Bisher galt bei falschem Pfad JEDE Datei als "nicht konform" — Ergebnis nach error/, Original geloescht (Default delete). run_verapdf() trennt jetzt ausserdem ein echtes FAIL-Urteil von einer Stoerung (VeraPdfUnavailable: nicht startbar, abgestuerzt, kein PASS/FAIL in der Ausgabe). Bei Stoerung wandern Original UND Ergebnis nach error/, das Original wird nicht entsorgt. - Gleichnamige Dateien wurden in outgoing/, error/ und beim Ordner-Upload mit abweichendem target kommentarlos ueberschrieben. Jetzt Zeitstempel daneben, mit Warnung; ProcessResult.output traegt den echten Pfad. Robustheit: - Kaputtes oder nicht lesbares TOML beim Start: Exit 2 statt Traceback. - RestartPreventExitStatus=2 in der Unit — Exit 2 (Config/Preflight) laeuft nicht mehr endlos neu, die Instanz bleibt sichtbar failed stehen. - Toter watchdog-Observer wird erkannt: Exit 3, systemd setzt den Watch neu auf. Vorher blieb die Unit "active" und verarbeitete nichts mehr. - Relative Pfade in [paths]/archive_dir/target sind ein Config-Fehler statt still unter /opt zu landen. - Fehler beim Archivieren entwertet den Durchlauf nicht mehr: Upload und Mail laufen, Sichtbarkeit ueber log.error + "OK mit Warnung"-Mail. - Nicht-PDFs in incoming/ werden beim Start-Scan gesammelt gemeldet. - Logging explizit nach stdout (die Doku versprach das schon). Struktur: - Neue lib/common.sh, von install.sh und update.sh gesourct. Die doppelte venv_is_healthy() gibt es nur noch einmal, in der gruendlichen Fassung — die schlanke in install.sh haette eine nach einem Distro-Sprung kaputte venv als gesund durchgewunken (nachgewiesen). - install.sh warnt in Containern, wenn systemd-journald nicht laeuft. Doku: Dateisystem-Festlegung (ext4/xfs/zfs, kein CIFS/NFS wegen inotify), Debian 13 in LXC auf Proxmox scheitert an journald (243/CREDENTIALS, AppArmor blockiert sd-mkdcreds) inkl. Abhilfe, echte Speicher-Messwerte, Exit-Code-Tabelle. 254 Tests gruen (vorher 152). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+269
-24
@@ -13,14 +13,17 @@ Verwandte Dokumente: [README](../README.md) · [Update](UPDATE.md) · [Debian-Ma
|
||||
| Betriebssystem | Debian 12 (bookworm) oder Debian 13 — systemd wird vorausgesetzt |
|
||||
| Python | 3.11+ (wegen `tomllib` aus der stdlib); kommt aus der Distribution |
|
||||
| Arbeitsspeicher | **mindestens 2 GB** für den produktiven Betrieb — siehe [Systemanforderungen](#systemanforderungen) |
|
||||
| Dateisystem | **ext4, xfs oder zfs**; `incoming/` **lokal**, kein CIFS/NFS — siehe [Dateisystem](#dateisystem-ext4-xfs-oder-zfs) |
|
||||
| Rechte | `root` (`sudo ./install.sh`) |
|
||||
| Netz | apt-Zugriff für die System-Pakete, PyPI-Zugriff für die venv |
|
||||
| Repo | muss dauerhaft liegen bleiben — `update.sh` kopiert daraus (s. [UPDATE.md](UPDATE.md)) |
|
||||
|
||||
Die System-Pakete installiert der Installer selbst. Die Liste steht als
|
||||
einzige Quelle in `install.sh` (Funktion `pdf_ocr_apt_packages()`, zwischen den
|
||||
Marken `# --- BEGIN apt-packages` / `# --- END apt-packages`) und wird von
|
||||
`update.sh` von dort ausgelesen:
|
||||
einzige Quelle in `lib/common.sh` (Funktion `pdf_ocr_apt_packages()`, zwischen
|
||||
den Marken `# --- BEGIN apt-packages` / `# --- END apt-packages`); `install.sh`
|
||||
sourct die Datei, und `update.sh` schneidet den Block zusätzlich noch einmal
|
||||
aus der **Repo**-Fassung heraus, damit beim Update die neue Liste gilt und
|
||||
nicht die eventuell ältere Kopie unter `/opt/pdf-ocr-hotfolder/lib/`:
|
||||
|
||||
```
|
||||
python3 python3-venv python3-pip
|
||||
@@ -30,7 +33,7 @@ ca-certificates curl
|
||||
```
|
||||
|
||||
Weitere Tesseract-Sprachpakete installiert der Installer bei Bedarf pro Instanz
|
||||
nach (siehe [OCR-Sprachen](#4-ocr-sprachen)).
|
||||
nach (siehe [OCR-Sprachen](#ocr-sprachen)).
|
||||
|
||||
Die Python-Abhängigkeiten stehen **fest gepinnt** in `requirements.txt`
|
||||
(ocrmypdf, watchdog, requests, paramiko). Warum das so ist und wie man die Pins
|
||||
@@ -47,6 +50,50 @@ Seitengröße, nicht an der Dateigröße der PDF.
|
||||
**Empfehlung: mindestens 2 GB RAM.** Für Mehr-Instanz-Betrieb oder
|
||||
`max_workers > 2` entsprechend mehr.
|
||||
|
||||
### Dateisystem: ext4, xfs oder zfs
|
||||
|
||||
Der Dienst wird **ausschließlich auf ext4, xfs oder zfs** betrieben. Andere
|
||||
Dateisysteme sind nicht vorgesehen und werden nicht getestet.
|
||||
|
||||
**`incoming/` gehört auf ein lokales, inotify-fähiges Dateisystem — kein
|
||||
CIFS/NFS-Mount.** Der Hotfolder hängt vollständig an inotify (`watchdog`
|
||||
meldet `created`, `moved`, `closed`). inotify ist ein Mechanismus des *lokalen*
|
||||
Kernels: er sieht nur Änderungen, die dieser Kernel selbst ausführt. Schreibt
|
||||
ein anderer Rechner über SMB oder NFS in ein gemountetes Verzeichnis, geht das
|
||||
am lokalen VFS vorbei und **es entsteht gar kein Event** — nicht verzögert,
|
||||
nicht unzuverlässig, sondern grundsätzlich keines.
|
||||
|
||||
Die Folge ist heimtückisch, weil nichts kaputt aussieht: Der Dienst startet,
|
||||
meldet `active (running)`, arbeitet den Bestand beim Start-Scan sauber ab — und
|
||||
bemerkt danach **keine einzige neue Datei mehr**. Es gibt keinen Fehler, keine
|
||||
Meldung, keine Mail. Erst wenn jemand die Instanz neu startet, wird der
|
||||
inzwischen angesammelte Stapel auf einmal verarbeitet.
|
||||
|
||||
Richtiger Aufbau: der Scanner schreibt über SMB/NFS auf **den Rechner, auf dem
|
||||
der Dienst läuft**, und `incoming/` liegt dort auf der lokalen Platte. Der
|
||||
Netz-Export zeigt auf dieses lokale Verzeichnis, nicht umgekehrt. Für die
|
||||
**Ausgabe** gilt die Einschränkung nicht — `outgoing/` und
|
||||
`[upload.folder].target` dürfen auf einem Netz-Share liegen, dorthin wird nur
|
||||
geschrieben.
|
||||
|
||||
### Was 2 GB tatsächlich tragen
|
||||
|
||||
Gemessen auf einem LXC-Container mit **2 GB RAM**, Debian 13 — eine
|
||||
**A4-Seite in 300 dpi** mit `deskew = true`, `oversample = 300`, `jobs = 4`:
|
||||
|
||||
| Messwert | Ergebnis |
|
||||
|----------|----------|
|
||||
| Laufzeit | **19 s** |
|
||||
| `MemoryPeak` des Dienstes (`systemctl show -p MemoryPeak`) | **380 MB** |
|
||||
| `memory.peak` des ganzen Containers | **503 MB** |
|
||||
| `oom_kill` in `/sys/fs/cgroup/memory.events` | **0** |
|
||||
|
||||
**Dieselbe Seite auf derselben Maschine mit 512 MB war genau der OOM-Kill
|
||||
unten.** Der Spitzenbedarf liegt also bei rund einem halben Gigabyte für
|
||||
*eine* Seite bei *einem* Worker — 2 GB lassen damit Luft für den
|
||||
Default `max_workers = 2`, für das Betriebssystem und für einen zweiten
|
||||
Hotfolder, sind aber keine üppige Reserve.
|
||||
|
||||
### Warum 512 MB nachweislich nicht reichen
|
||||
|
||||
Gemessen auf einem LXC-Container mit **512 MB RAM + 512 MB Swap**, Debian 13,
|
||||
@@ -141,7 +188,7 @@ Der Installer unterscheidet zwei Ebenen:
|
||||
|
||||
| Ebene | Wann | Was passiert |
|
||||
|-------|------|--------------|
|
||||
| **Basis-Install** | einmalig; erkannt an `venv` + Template-Unit | System-Pakete, Ghostscript-Check, Container-Erkennung, Default-User `pdfocr`, Code nach `/opt/pdf-ocr-hotfolder/`, venv, systemd-Template-Unit |
|
||||
| **Basis-Install** | einmalig; erkannt an `venv` + Template-Unit | System-Pakete, Ghostscript-Check, Container-Erkennung (inkl. [journald-Prüfung](#debian-13-in-lxc-auf-proxmox-journald-scheitert-243credentials)), Default-User `pdfocr`, Code **und `lib/`** nach `/opt/pdf-ocr-hotfolder/`, venv, systemd-Template-Unit |
|
||||
| **Instanz-Anlage** | bei jedem Lauf, beliebig oft | Abfragen pro Instanz, Datenverzeichnisse, `<instanz>.toml`, optionales User-Drop-in, `enable --now` |
|
||||
|
||||
Ist die Basis-Installation vorhanden, aber die venv passt nicht mehr zum
|
||||
@@ -157,10 +204,12 @@ Installer in der Schleife `Weitere Instanz anlegen? [j/N]:`.
|
||||
|
||||
## Die Abfragen pro Instanz
|
||||
|
||||
`create_instance()` stellt fünf Fragen. Alle Antworten gelten **nur für diese
|
||||
Instanz** — nichts davon ist global.
|
||||
`create_instance()` stellt fünf Fragen, in der Reihenfolge der folgenden
|
||||
Abschnitte: Instanz-Name, Basis-Pfad, Service-User, OCR-Sprachen,
|
||||
Original-Behandlung. Alle Antworten gelten **nur für diese Instanz** — nichts
|
||||
davon ist global.
|
||||
|
||||
### 1. Instanz-Name
|
||||
### Instanz-Name
|
||||
|
||||
```
|
||||
Instanz-Name (nur a-z, 0-9, -):
|
||||
@@ -171,7 +220,7 @@ Muster `^[a-z0-9][a-z0-9-]*$`. Der Name wird zum Unit-Suffix
|
||||
(`/etc/pdf-ocr-hotfolder/<name>.toml`). Existiert die Config schon, bricht die
|
||||
Anlage ab — ein versehentliches Überschreiben gibt es nicht.
|
||||
|
||||
### 2. Basis-Pfad für die Daten
|
||||
### Basis-Pfad für die Daten
|
||||
|
||||
```
|
||||
Basis-Pfad für Daten [/var/lib/pdf-ocr-hotfolder/<name>]:
|
||||
@@ -180,7 +229,7 @@ Basis-Pfad für Daten [/var/lib/pdf-ocr-hotfolder/<name>]:
|
||||
Darunter entstehen `incoming/`, `outgoing/`, `working/`, `error/` und werden auf
|
||||
den Service-User gechownt.
|
||||
|
||||
### 3. Service-User
|
||||
### Service-User
|
||||
|
||||
```
|
||||
Service-User [pdfocr]:
|
||||
@@ -198,7 +247,7 @@ Service-User [pdfocr]:
|
||||
Bei AD-Usern mit lokaler UID werden die Datei-Berechtigungen über die UID
|
||||
gesetzt — das läuft transparent.
|
||||
|
||||
### 4. OCR-Sprachen
|
||||
### OCR-Sprachen
|
||||
|
||||
```
|
||||
Tesseract-Sprachen [deu+eng]:
|
||||
@@ -236,7 +285,7 @@ Was der Installer damit macht:
|
||||
4. Ist `tesseract` gar nicht aufrufbar, wird die Prüfung übersprungen und die
|
||||
Eingabe unverändert übernommen.
|
||||
|
||||
### 5. Original archivieren?
|
||||
### Original archivieren?
|
||||
|
||||
```
|
||||
Original nach erfolgreichem OCR archivieren? [j/N]:
|
||||
@@ -436,6 +485,20 @@ Fehlt die Sektion oder einer der vier Einträge, gibt es eine deutsche
|
||||
`ConfigError`-Meldung mit Datei- und Key-Nennung und **Exit 2** — der Dienst
|
||||
startet nicht.
|
||||
|
||||
**Alle vier Pfade müssen absolut sein.** Ein relativer Pfad wird gegen das
|
||||
Arbeitsverzeichnis des *Prozesses* aufgelöst, bei der Unit also gegen
|
||||
`WorkingDirectory=/opt/pdf-ocr-hotfolder` — **nicht** gegen das Verzeichnis, in
|
||||
dem die Config liegt. `incoming = "in"` legte damit still
|
||||
`/opt/pdf-ocr-hotfolder/in` an: der Scanner schreibt woanders hin als der
|
||||
Dienst schaut, und niemand sieht einen Fehler. Seit 0.7.0 ist das ein
|
||||
Config-Fehler mit **Exit 2**. Dieselbe Regel gilt für
|
||||
[`[output].archive_dir`](#output) und
|
||||
[`[upload.folder].target`](#uploadfolder--uploadnextcloud--uploadsftp);
|
||||
`install.sh` erzeugt ohnehin nur absolute Pfade.
|
||||
|
||||
`incoming` muss außerdem auf einem **lokalen** Dateisystem liegen — siehe
|
||||
[Dateisystem](#dateisystem-ext4-xfs-oder-zfs).
|
||||
|
||||
### `[ocr]`
|
||||
|
||||
| Key | Default | Bedeutung |
|
||||
@@ -467,17 +530,34 @@ ocrmypdf-Default greift. Siehe auch
|
||||
| `name_mode` | `"prefix"` | `prefix` → `OCR_scan.pdf`, `suffix` → `scan_OCR.pdf` (vor der Extension), `none` → unverändert |
|
||||
| `name_tag` | `"OCR_"` | verbatim eingefügter String; leer wirkt wie `none` |
|
||||
| `original_on_success` | `"delete"` | `delete` oder `archive` — Installer fragt das ab |
|
||||
| `archive_dir` | `""` | absoluter Pfad, **Pflicht** bei `archive`; Namenskollision → Zeitstempel-Suffix |
|
||||
| `archive_dir` | `""` | absoluter Pfad (relativ wird abgelehnt), **Pflicht** bei `archive`; Namenskollision → Zeitstempel-Suffix |
|
||||
|
||||
Ein Tippfehler in `name_mode` oder `original_on_success` führt beim Start zum
|
||||
Abbruch mit **Exit 2**, nicht erst bei der ersten Datei.
|
||||
|
||||
**Kollisionen überschreiben nichts.** Liegt im Ziel bereits eine Datei
|
||||
desselben Namens, wird die neue mit Zeitstempel danebengelegt
|
||||
(`scan.pdf` → `scan_20260923-081500.pdf`; bei zwei Dateien innerhalb derselben
|
||||
Sekunde zusätzlich mit Zähler), und es gibt eine Warnung im Journal. Das gilt
|
||||
seit 0.7.0 einheitlich für **`outgoing/`, das Archiv, `error/` und den
|
||||
Ordner-Upload** — vorher ersetzte der zweite Durchlauf das Ergebnis des ersten
|
||||
kommentarlos. Die E-Mail-Benachrichtigung und die Upload-Ziele nennen den
|
||||
tatsächlich geschriebenen Namen.
|
||||
|
||||
**Scheitert das Entsorgen des Originals** (Platte voll, Verzeichnis
|
||||
read-only), gilt der Durchlauf trotzdem als Erfolg: das fertige PDF liegt
|
||||
bereits in `outgoing/` und wird normal ausgeliefert. Es gibt aber eine
|
||||
`ERROR`-Zeile im Journal, und die Mail geht als **„OK mit Warnung"** raus —
|
||||
auch bei `[notify.email].on = "errors"`. Das Original bleibt dann in
|
||||
`working/` liegen und wird beim nächsten Start **erneut** durch das OCR
|
||||
geschickt; es gehört von Hand aufgeräumt und die Ursache behoben.
|
||||
|
||||
### `[verapdf]`
|
||||
|
||||
| Key | Default | Bedeutung |
|
||||
|-----|---------|-----------|
|
||||
| `enabled` | `false` | PDF/A-Validierung per veraPDF-CLI |
|
||||
| `binary` | `/opt/verapdf/verapdf` | Pfad zum veraPDF-Binary |
|
||||
| `binary` | `/opt/verapdf/verapdf` | Pfad zum veraPDF-Binary, oder ein nackter Name, der im `PATH` gesucht wird |
|
||||
| `flavour` | `"1b"` | PDF/A-Flavour |
|
||||
|
||||
veraPDF startet eine JVM und ist entsprechend teuer — nur einschalten, wenn die
|
||||
@@ -485,6 +565,27 @@ Validierung wirklich gebraucht wird. Bei FAIL wandert das OCR-Ergebnis nach
|
||||
`error/`; das Original folgt `original_on_success` (bei `archive` bleibt es also
|
||||
erhalten).
|
||||
|
||||
**Mit `enabled = true` wird `binary` im Preflight geprüft** (seit 0.7.0). Zeigt
|
||||
der Pfad nicht auf ein vorhandenes, ausführbares Programm, startet der Dienst
|
||||
gar nicht erst (**Exit 2**), und `--check-config` meldet denselben Fehler.
|
||||
`--check-config` zeigt Binary und Flavour außerdem in der Übersicht an.
|
||||
|
||||
> ⚠️ **Warum das eine harte Sperre ist.** Bis 0.6.3 war ein Tippfehler in
|
||||
> `binary` der gefährlichste Fehler des ganzen Dienstes: `run_verapdf()` fand
|
||||
> das Programm für **jede** Datei nicht, wertete das als „nicht konform",
|
||||
> schob das OCR-Ergebnis nach `error/` — und entsorgte das Original laut
|
||||
> `original_on_success`, beim Default `delete` also die Vorlage. Scan für Scan
|
||||
> verschwanden so die Originale, während die Unit als `active (running)`
|
||||
> dastand.
|
||||
|
||||
**Störung ist kein FAIL.** Lässt sich veraPDF im laufenden Betrieb nicht mehr
|
||||
befragen — Programm verschwunden, JVM startet nicht, Timeout (300 s pro Datei),
|
||||
oder die Ausgabe enthält weder `PASS` noch `FAIL` —, ist das **kein Urteil über
|
||||
die PDF**. In diesem Fall wandern **Original und OCR-Ergebnis** nach `error/`,
|
||||
und das Original wird **weder gelöscht noch archiviert**, unabhängig von
|
||||
`original_on_success`. Im Journal steht die Ursache samt Hinweis auf
|
||||
`--check-config`.
|
||||
|
||||
### `[upload.folder]` / `[upload.nextcloud]` / `[upload.sftp]`
|
||||
|
||||
Beliebig viele Ziele gleichzeitig aktivierbar. Sind alle aus, bleibt das fertige
|
||||
@@ -492,7 +593,7 @@ PDF einfach in `outgoing/` liegen.
|
||||
|
||||
| Sektion | Keys |
|
||||
|---------|------|
|
||||
| `[upload.folder]` | `enabled`, `target` — leer heißt `[paths].outgoing`, dann No-op |
|
||||
| `[upload.folder]` | `enabled`, `target` — leer heißt `[paths].outgoing`, dann No-op; sonst **absoluter** Pfad (relativ wird abgelehnt, Exit 2) |
|
||||
| `[upload.nextcloud]` | `enabled`, `url`, `username`, `password`, `remote_path`, `verify_ssl` |
|
||||
| `[upload.sftp]` | `enabled`, `host`, `port`, `username`, `key_file`, `password`, `remote_path` |
|
||||
|
||||
@@ -500,6 +601,10 @@ Schlägt mindestens ein Ziel fehl, zählt das als Fehler und die Mail geht als
|
||||
FEHLER raus — das PDF bleibt aber **bewusst in `outgoing/`** liegen, das OCR
|
||||
selbst war ja erfolgreich.
|
||||
|
||||
Liegt im `target` von `[upload.folder]` schon eine gleichnamige Datei, wird sie
|
||||
seit 0.7.0 **nicht mehr ersetzt**, sondern die Kopie mit Zeitstempel
|
||||
danebengelegt (samt Warnung) — dieselbe Regel wie in [`[output]`](#output).
|
||||
|
||||
### `[notify.email]`
|
||||
|
||||
| Key | Default | Bedeutung |
|
||||
@@ -521,6 +626,36 @@ ins journal.
|
||||
|
||||
---
|
||||
|
||||
## Exit-Codes
|
||||
|
||||
Der Dienst und die CLI benutzen vier Codes. `systemctl status` und
|
||||
`journalctl` zeigen sie als `status=<n>`.
|
||||
|
||||
| Exit | Bedeutung | Startet systemd neu? | Was zu tun ist |
|
||||
|------|-----------|----------------------|----------------|
|
||||
| **0** | regulärer Stopp (SIGTERM/SIGINT); bei `--once`: alles verarbeitet, auch „nichts da"; bei `--check-config`: Config sauber | — | nichts |
|
||||
| **1** | nur im Einmal-Betrieb: mindestens eine PDF ist fehlgeschlagen. Bei `--check-config`: Config nutzbar, aber mit **Warnungen** | — | `error/` ansehen bzw. Warnungen nachziehen |
|
||||
| **2** | **Config- oder Preflight-Fehler** — kaputtes/unlesbares TOML, fehlender Pflicht-Key, relativer Pfad, ungültige `[output]`-Werte, fehlendes `tesseract`/`gs`, betroffene Ghostscript-Version, nicht aufrufbares veraPDF | **nein** — `RestartPreventExitStatus=2` in der Unit | Config korrigieren, mit `--check-config` gegenprüfen, dann `systemctl start` |
|
||||
| **3** | **Der Verzeichnis-Watch ist gestorben** — es würden keine neuen Dateien mehr erkannt | **ja**, und genau darum geht es | meist nichts; häuft es sich, `fs.inotify.max_user_watches` und den Mount von `incoming/` prüfen |
|
||||
|
||||
**Zu Exit 2:** Ein Neustart heilt einen Config-Fehler nicht. Ohne
|
||||
`RestartPreventExitStatus=2` startete `Restart=on-failure` die Instanz endlos
|
||||
im 5-Sekunden-Takt neu (das Start-Rate-Limit greift bei `RestartSec=5` nie).
|
||||
Seit 0.7.0 bleibt sie stattdessen sichtbar `failed` stehen — das ist gewollt
|
||||
und soll beim Nachsehen auffallen.
|
||||
|
||||
**Zu Exit 3:** Stirbt der watchdog-Observer im Betrieb (erschöpftes
|
||||
`fs.inotify.max_user_watches`, ersetztes oder neu gemountetes Verzeichnis),
|
||||
blieb die Unit früher `active (running)` und verarbeitete stumm nichts mehr —
|
||||
für einen Hotfolder der schlechteste denkbare Zustand. Der Dienst prüft den
|
||||
Observer jetzt sekündlich mit, loggt eine `ERROR`-Zeile mit den möglichen
|
||||
Ursachen und beendet sich mit 3, damit systemd ihn neu startet und der Watch
|
||||
neu aufgesetzt wird. Ein einzelnes Vorkommnis ist damit selbstheilend; ein
|
||||
steigendes `systemctl show -p NRestarts` ist der Hinweis, dass man nachsehen
|
||||
sollte.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tesseract findet die Sprache nicht
|
||||
@@ -547,21 +682,65 @@ das Archiv, falls konfiguriert):
|
||||
sudo chown -R DOMAIN\\scanuser:DOMAIN\\scangroup /var/lib/pdf-ocr-hotfolder/<instanz>
|
||||
```
|
||||
|
||||
### veraPDF-Validierung schlägt immer fehl
|
||||
### veraPDF: Dienst startet nicht / Dateien landen in `error/`
|
||||
|
||||
`[verapdf].binary` prüfen. Wenn die Validierung nicht zwingend gebraucht wird:
|
||||
`enabled = false`.
|
||||
Seit 0.7.0 prüft der Preflight `[verapdf].binary`. Startet die Instanz mit
|
||||
**Exit 2** nicht mehr, nachdem vorher „alles lief", ist das die gute Nachricht:
|
||||
der Pfad war schon vorher falsch, nur hat es bisher niemand gemerkt. Vorher
|
||||
wurde jede PDF als ungültig gewertet und das Original laut
|
||||
`original_on_success` entsorgt.
|
||||
|
||||
```bash
|
||||
ls -l /opt/verapdf/verapdf # vorhanden? ausführbar (chmod +x)?
|
||||
```
|
||||
|
||||
Korrigieren — oder, wenn die Validierung nicht zwingend gebraucht wird,
|
||||
`[verapdf].enabled = false` setzen. Landen **Original und OCR-Ergebnis
|
||||
gemeinsam** in `error/`, war veraPDF im laufenden Betrieb nicht mehr
|
||||
ansprechbar; das Original ist dann unangetastet, siehe
|
||||
[`[verapdf]`](#verapdf).
|
||||
|
||||
### Dienst startet nicht (Exit 2)
|
||||
|
||||
Exit 2 heißt immer: Config oder Preflight. Die Ursache steht im journal und
|
||||
ausführlicher in:
|
||||
Exit 2 heißt immer: Config oder Preflight — siehe [Exit-Codes](#exit-codes).
|
||||
Die Instanz bleibt bewusst `failed` stehen und wird **nicht** neu gestartet.
|
||||
Die Ursache steht im journal und ausführlicher in:
|
||||
|
||||
```bash
|
||||
cd /opt/pdf-ocr-hotfolder && sudo ./venv/bin/python -m pdf_ocr_hotfolder \
|
||||
--check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml
|
||||
```
|
||||
|
||||
Typische Fälle: kaputtes TOML (die Meldung nennt Zeile und Spalte, sofern der
|
||||
Interpreter sie liefert), ein relativer Pfad in `[paths]`,
|
||||
`[output].archive_dir` oder `[upload.folder].target`, ein leeres `archive_dir`
|
||||
bei `original_on_success = "archive"`, ein nicht aufrufbares veraPDF oder eine
|
||||
betroffene Ghostscript-Version.
|
||||
|
||||
### Dienst läuft, verarbeitet aber nichts mehr
|
||||
|
||||
`systemctl status` sagt `active (running)`, in `incoming/` stapeln sich die
|
||||
PDFs, im Journal passiert nichts. Drei Ursachen, in dieser Reihenfolge prüfen:
|
||||
|
||||
1. **`incoming/` liegt auf einem CIFS/NFS-Mount.** Dann liefert inotify
|
||||
grundsätzlich keine Events — der Dienst verarbeitet nur noch beim Start.
|
||||
`findmnt -T /var/lib/pdf-ocr-hotfolder/<instanz>/incoming` zeigt den Typ;
|
||||
Hintergrund und richtiger Aufbau unter
|
||||
[Dateisystem](#dateisystem-ext4-xfs-oder-zfs).
|
||||
2. **Der Verzeichnis-Watch ist gestorben.** Seit 0.7.0 fällt das auf: der
|
||||
Dienst beendet sich mit [Exit 3](#exit-codes) und systemd startet ihn neu.
|
||||
Im Journal steht die `ERROR`-Zeile, `systemctl show -p NRestarts` steigt.
|
||||
Häuft sich das, ist meist das inotify-Limit erschöpft:
|
||||
```bash
|
||||
cat /proc/sys/fs/inotify/max_user_watches
|
||||
```
|
||||
3. **Es sind gar keine PDFs.** Dateien ohne `.pdf`-Endung werden ignoriert.
|
||||
Beim Start-Scan meldet der Dienst sie seit 0.7.0 als Sammelzeile mit Anzahl
|
||||
und bis zu drei Beispielnamen:
|
||||
```bash
|
||||
journalctl -u pdf-ocr-hotfolder@<instanz> | grep 'ohne .pdf-Endung'
|
||||
```
|
||||
|
||||
### Dienst startet nicht (203/EXEC)
|
||||
|
||||
Der Interpreter der venv ist weg — fast immer nach einem Distributions-Upgrade.
|
||||
@@ -589,9 +768,13 @@ journald ist damit ein blinder Fleck: jede Fehlersuche läuft ins Leere, und der
|
||||
Ausfall sieht aus wie ein stummer Dienst. **Deshalb vor jeder Fehlersuche
|
||||
zuerst journald prüfen**, nicht erst, wenn nichts anderes mehr passt.
|
||||
|
||||
Das ist **kein** generelles LXC-Muster. Von zwei Testcontainern war genau einer
|
||||
betroffen; auf dem zweiten lief journald einwandfrei. Es handelt sich um einen
|
||||
Schaden auf dieser einen Maschine, nicht um eine Eigenschaft von Containern.
|
||||
Die häufigste Ursache ist **kein** Schaden an dieser einen Maschine, sondern
|
||||
systematisch: **Debian 13 in einer LXC auf Proxmox** — siehe den nächsten
|
||||
Abschnitt. Auf Debian 12 tritt sie nicht auf.
|
||||
|
||||
`install.sh` warnt beim Erstinstall in einem Container von sich aus, wenn
|
||||
`systemd-journald` nicht läuft, nennt den Drop-in-Befehl und fragt, ob
|
||||
fortgefahren werden soll.
|
||||
|
||||
**Notbehelf, solange journald nicht zu retten ist:** die Instanz einmal im
|
||||
Vordergrund laufen lassen — dann geht die Ausgabe direkt ins Terminal, am
|
||||
@@ -610,6 +793,67 @@ Das `cd` ist zwingend: das Paket wird nicht pip-installiert, sondern nach
|
||||
Bestand abarbeiten und dann aussteigen will, hängt `--once` an (siehe
|
||||
[Manueller Lauf](#manueller-lauf-one-shot)).
|
||||
|
||||
### Debian 13 in LXC auf Proxmox: journald scheitert (243/CREDENTIALS)
|
||||
|
||||
Verifizierter Befund. Er betrifft **jede** Debian-13-LXC auf Proxmox 8.4, nicht
|
||||
nur eine einzelne Maschine — und er trifft nicht nur dieses Tool, sondern
|
||||
alles, was auf dem Container-journal aufsetzt.
|
||||
|
||||
**Symptom im Container:**
|
||||
|
||||
```bash
|
||||
systemctl status systemd-journald
|
||||
# ● systemd-journald.service - Journal Service
|
||||
# Active: failed
|
||||
# Process: ... (code=exited, status=243/CREDENTIALS)
|
||||
# Main PID exited, status=243/CREDENTIALS
|
||||
|
||||
journalctl -u pdf-ocr-hotfolder@<instanz>
|
||||
# No journal files were found.
|
||||
```
|
||||
|
||||
**Ursache.** Ab systemd 255 (Debian 13 liefert **257**) setzt die
|
||||
journald-Unit `ImportCredential=journal.*`. Zum Einlesen dieser Credentials
|
||||
startet systemd den Hilfsprozess `(sd-mkdcreds)`, und der **mountet** dafür.
|
||||
Genau diesen Mount verbietet das AppArmor-Profil des Proxmox-Hosts. Im Log des
|
||||
**Hosts** steht dazu:
|
||||
|
||||
```
|
||||
apparmor="DENIED" operation="mount" profile="lxc-<id>_</var/lib/lxc>" name="/dev/" comm="(sd-mkdcreds)"
|
||||
```
|
||||
|
||||
Debian 12 hat systemd 252, kennt `ImportCredential` in dieser Unit nicht und
|
||||
ist deshalb **nicht** betroffen. Das Upgrade 12 → 13 ist damit der Auslöser,
|
||||
nicht der Container an sich.
|
||||
|
||||
**Dieselbe Ursache legt weitere Units lahm** — beobachtet bei
|
||||
`systemd-logind`, `systemd-networkd`, `console-getty` und
|
||||
`systemd-tmpfiles-setup`. Wer nur journald repariert, hat die übrigen noch vor
|
||||
sich; ein Blick auf `systemctl --failed` lohnt sich.
|
||||
|
||||
**Abhilfe im Container** (reboot-fest verifiziert) — `ImportCredential` wird
|
||||
per Drop-in auf leer gesetzt und damit abgeschaltet:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/systemd/system/systemd-journald.service.d
|
||||
printf '[Service]\nImportCredential=\n' | \
|
||||
sudo tee /etc/systemd/system/systemd-journald.service.d/no-credentials.conf
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart systemd-journald
|
||||
sudo journalctl --flush
|
||||
```
|
||||
|
||||
Danach `systemctl status systemd-journald` (muss `active (running)` sein) und
|
||||
`journalctl -u pdf-ocr-hotfolder@<instanz>` gegenprüfen. Für die anderen
|
||||
betroffenen Units gilt dasselbe Muster mit deren Unit-Namen.
|
||||
|
||||
> **Der saubere Weg liegt host-seitig.** Das Drop-in kuriert das Symptom im
|
||||
> Container. Richtig behoben wird es auf dem Proxmox-Host: Update von
|
||||
> `pve-container`/`lxc-pve` auf eine Fassung mit passenden AppArmor-Regeln —
|
||||
> oder, als grobes Mittel, `lxc.apparmor.profile: unconfined` in der
|
||||
> Container-Config, was die AppArmor-Isolation dieses Containers allerdings
|
||||
> komplett aufgibt.
|
||||
|
||||
### Dienst bricht mitten in der Verarbeitung weg
|
||||
|
||||
Datei bleibt in `working/`, kein Traceback, `NRestarts` steigt: das ist fast
|
||||
@@ -629,4 +873,5 @@ cd /opt/pdf-ocr-hotfolder && sudo -u pdfocr ./venv/bin/python -m pdf_ocr_hotfold
|
||||
```
|
||||
|
||||
Exit-Code: `0` = alles verarbeitet (auch "nichts da"), `1` = mindestens eine
|
||||
Datei fehlgeschlagen, `2` = Config- oder Preflight-Fehler.
|
||||
Datei fehlgeschlagen, `2` = Config- oder Preflight-Fehler. Exit 3 gibt es hier
|
||||
nicht — der gehört zum Dauerbetrieb, siehe [Exit-Codes](#exit-codes).
|
||||
|
||||
+2
-2
@@ -226,11 +226,11 @@ auch für die Abhängigkeiten, die ocrmypdf 17 zusätzlich mitbringt (`pydantic`
|
||||
```
|
||||
3. Dort `update.sh --rebuild-venv` fahren, damit die Pakete wirklich frisch
|
||||
aufgelöst werden.
|
||||
4. `pytest` muss grün bleiben (152 Tests).
|
||||
4. `pytest` muss grün bleiben (254 Tests).
|
||||
5. Eine echte PDF durchschieben — die Test-Suite mockt ocrmypdf, ein Major-Sprung
|
||||
fällt dort also nicht auf. Der [Rauchtest](UPDATE.md#rauchtest) in `update.sh`
|
||||
macht genau das automatisch.
|
||||
5. Erst dann committen und auf die produktiven Systeme geben.
|
||||
6. Erst dann committen und auf die produktiven Systeme geben.
|
||||
|
||||
Der ocrmypdf-Sprung 16 → 17 ist ein **Major-Sprung** und gehört in einen eigenen
|
||||
Vorgang mit eigenem Test, nicht in ein OS-Upgrade.
|
||||
|
||||
+34
-3
@@ -28,16 +28,24 @@ sudo ./update.sh --no-smoke-test # ohne Rauchtest durchlaufen
|
||||
es den gespeicherten Pfad aus `/opt/pdf-ocr-hotfolder/.repo_path` — **das Repo
|
||||
muss also liegen bleiben**, das Tool kopiert daraus.
|
||||
|
||||
`install.sh` und `update.sh` teilen sich seit 0.7.0 die Datei `lib/common.sh`
|
||||
(Log-Funktionen, Root-Prüfung, Layout-Pfade, apt-Paketliste, venv-Prüfung).
|
||||
`update.sh` sourct bevorzugt die Fassung **neben sich** im Repo und fällt auf
|
||||
die installierte unter `/opt/pdf-ocr-hotfolder/lib/` zurück; fehlt sie
|
||||
überall, bricht es sofort ab statt mitten im Lauf. Die apt-Paketliste schneidet
|
||||
es zusätzlich noch einmal per `sed` aus der Repo-Fassung heraus — beim Update
|
||||
soll die neue Liste gelten, nicht die eventuell ältere installierte Kopie.
|
||||
|
||||
## Was das Skript tut — in dieser Reihenfolge
|
||||
|
||||
| # | Schritt | Anmerkung |
|
||||
|---|---------|-----------|
|
||||
| 1 | **Instanzen erfassen** | aktiv / kaputt / bewusst gestoppt, siehe [unten](#instanz-erfassung) |
|
||||
| 2 | **System-Pakete abgleichen** | Liste wird aus `install.sh` extrahiert, `apt-get install` ist idempotent |
|
||||
| 2 | **System-Pakete abgleichen** | Liste wird aus `lib/common.sh` des Repos extrahiert, `apt-get install` ist idempotent |
|
||||
| 3 | **venv prüfen** | passt sie noch zum System-Python? Läuft **vor** dem Stoppen, damit man es früh sieht |
|
||||
| 4 | **Instanzen stoppen** | nur die, die vorher liefen oder kaputt waren |
|
||||
| 5 | **Backup** | [Inhalt und Ort](#backup) |
|
||||
| 6 | **Code kopieren** | `pdf_ocr_hotfolder/`, `requirements.txt`, `VERSION`, `config.example.toml`, `.repo_path` |
|
||||
| 6 | **Code kopieren** | `pdf_ocr_hotfolder/`, **`lib/`**, `requirements.txt`, `VERSION`, `config.example.toml`, `.repo_path` |
|
||||
| 7 | **Dependencies** | `pip install --upgrade -r requirements.txt` — oder venv-Neubau, falls nötig. Versionssprünge der gepinnten Pakete werden [benannt](#versionssprünge-der-kernabhängigkeiten) |
|
||||
| 8 | **systemd-Units** | Template-Unit aus dem Repo, LXC-Drop-in nachziehen, `daemon-reload` |
|
||||
| 9 | **Berechtigungen** | Code gehört dem primären User (i.d.R. `pdfocr`) |
|
||||
@@ -114,7 +122,7 @@ Vor dem ersten Eingriff auf der Platte schreibt `update.sh` ein Archiv:
|
||||
|
||||
| Enthalten | Nicht enthalten |
|
||||
|-----------|-----------------|
|
||||
| `/opt/pdf-ocr-hotfolder/` (Code) | die **venv** (`venv/`, `venv.old-*`) |
|
||||
| `/opt/pdf-ocr-hotfolder/` (Code **inkl. `lib/`**) | die **venv** (`venv/`, `venv.old-*`) |
|
||||
| `/etc/pdf-ocr-hotfolder/` (alle Instanz-Configs) | die **Datenverzeichnisse** `/var/lib/pdf-ocr-hotfolder/` |
|
||||
| Template-Unit `pdf-ocr-hotfolder@.service` | `__pycache__`, `*.pyc` |
|
||||
| alle Drop-in-Verzeichnisse `…@*.service.d` | |
|
||||
@@ -303,6 +311,15 @@ validiert die `[output]`-Sektion.
|
||||
| **1** | Config nutzbar, aber mit **Warnungen** | Kein Abbruchgrund, der Dienst läuft. Die Warnungen aber **nachziehen** — sie nennen entweder einen Key, dessen Bedeutung sich geändert hat, oder einen Eintrag, der ins Leere läuft (s. [Config-Drift](#config-drift-nach-einem-update)) |
|
||||
| **2** | Config **unbrauchbar** — der Dienst würde nicht starten | Sofort korrigieren. Die Instanz gilt als **nicht** erfolgreich aktualisiert, das Update endet mit Exit 1 |
|
||||
|
||||
`--check-config` selbst kennt nur 0/1/2. Der **Dienst** kennt seit 0.7.0
|
||||
zusätzlich **Exit 3** (Verzeichnis-Watch gestorben, Neustart erwünscht) — und
|
||||
die Unit startet bei **Exit 2** absichtlich **nicht** mehr neu
|
||||
(`RestartPreventExitStatus=2`), die Instanz bleibt sichtbar `failed` stehen.
|
||||
Für das Update heißt das: eine Instanz mit Config-Fehler verschwindet nicht
|
||||
mehr in einem stillen 5-Sekunden-Crash-Loop, sondern fällt in der
|
||||
Zusammenfassung auf. Die vollständige Tabelle:
|
||||
[INSTALLATION.md](INSTALLATION.md#exit-codes).
|
||||
|
||||
Kennt der installierte Code `--check-config` noch nicht (Update von einem Stand
|
||||
vor 0.6.0), erkennt `update.sh` das an der argparse-Meldung, überspringt die
|
||||
Prüfung mit einer Warnung und läuft weiter.
|
||||
@@ -361,6 +378,20 @@ dem Fall mit Exit 2 ab; ab Ghostscript 10.02.1 ist PDF/A unproblematisch.
|
||||
> `requirements.txt` pinnt darum 17.x, und der Preflight prüft beides zusammen.
|
||||
> Hintergrund: [INSTALLATION.md](INSTALLATION.md#ghostscript-bug-auf-debian-12).
|
||||
|
||||
### Relative Pfade — Fehler seit 0.7.0
|
||||
|
||||
Bis 0.6.3 wurde ein relativer Pfad in `[paths]`, `[output].archive_dir` oder
|
||||
`[upload.folder].target` klaglos angenommen und gegen das `WorkingDirectory`
|
||||
der Unit aufgelöst, also unter `/opt/pdf-ocr-hotfolder/`. Seit 0.7.0 ist das
|
||||
ein **Config-Fehler mit Exit 2**: `--check-config` meldet ihn beim Update, und
|
||||
der Dienst startet nicht.
|
||||
|
||||
Das ist der einzige Fall, in dem ein Update von 0.6.x eine bisher „laufende"
|
||||
Instanz stoppen kann. Er ist gewollt — eine solche Instanz schrieb an einer
|
||||
Stelle, an der niemand sie gesucht hat. Abhilfe: den Pfad absolut eintragen
|
||||
und, falls dort Dateien liegen, den Inhalt von `/opt/pdf-ocr-hotfolder/<pfad>`
|
||||
vorher herüberholen.
|
||||
|
||||
### Unbekannte Keys
|
||||
|
||||
Einträge, die zu keiner Sektion und keinem Key gehören, werden beim Laden
|
||||
|
||||
Reference in New Issue
Block a user