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:
2026-09-23 00:59:17 +02:00
parent 305454eeb5
commit cd803a3dfe
28 changed files with 2902 additions and 286 deletions
+269 -24
View File
@@ -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).