2026-01-04 17:24:19 +01:00
|
|
|
"""
|
|
|
|
|
FOP Worker Pool - Persistente JVM-Prozesse für schnelle PDF-Generierung.
|
|
|
|
|
|
|
|
|
|
Eliminiert JVM-Startup-Overhead durch Vorinitialisierung von N Worker-Prozessen.
|
|
|
|
|
Jeder Worker läuft als Daemon und verarbeitet mehrere FO→PDF Transformationen nacheinander.
|
|
|
|
|
"""
|
|
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
import glob
|
2026-01-04 17:24:19 +01:00
|
|
|
import logging
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
from worker_pool_base import BaseWorkerPool, _CLASSPATH_SEP
|
2026-01-06 20:58:37 +01:00
|
|
|
|
2026-01-04 17:24:19 +01:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
# Java-Worker-Code (wird zur Laufzeit kompiliert)
|
|
|
|
|
FOP_WORKER_JAVA = """
|
|
|
|
|
import org.apache.fop.apps.*;
|
|
|
|
|
import org.xml.sax.SAXException;
|
|
|
|
|
import javax.xml.transform.*;
|
|
|
|
|
import javax.xml.transform.sax.SAXResult;
|
|
|
|
|
import javax.xml.transform.stream.StreamSource;
|
|
|
|
|
import java.io.*;
|
|
|
|
|
import java.net.URI;
|
|
|
|
|
|
|
|
|
|
public class FopWorker {
|
|
|
|
|
public static void main(String[] args) {
|
|
|
|
|
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
|
|
|
|
|
String line;
|
|
|
|
|
|
|
|
|
|
System.err.println("FopWorker starting...");
|
|
|
|
|
System.err.flush();
|
|
|
|
|
|
|
|
|
|
// Create FopFactory once and reuse (major performance boost!)
|
|
|
|
|
FopFactory fopFactory = null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Check if config file is provided as first argument
|
|
|
|
|
if (args.length > 0 && !args[0].isEmpty()) {
|
|
|
|
|
File configFile = new File(args[0]);
|
|
|
|
|
if (configFile.exists()) {
|
|
|
|
|
System.err.println("Loading FOP config: " + configFile.getAbsolutePath());
|
|
|
|
|
fopFactory = FopFactory.newInstance(configFile);
|
|
|
|
|
} else {
|
|
|
|
|
System.err.println("Config file not found, using default configuration");
|
|
|
|
|
fopFactory = FopFactory.newInstance(new File(".").toURI());
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
System.err.println("No config file specified, using default FOP configuration");
|
|
|
|
|
fopFactory = FopFactory.newInstance(new File(".").toURI());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
System.err.println("FopWorker started and ready");
|
|
|
|
|
System.err.flush();
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
System.err.println("FATAL: Failed to initialize FopFactory: " + e.getMessage());
|
|
|
|
|
e.printStackTrace(System.err);
|
|
|
|
|
System.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
while ((line = reader.readLine()) != null) {
|
|
|
|
|
System.err.println("DEBUG: Received line: " + line.substring(0, Math.min(100, line.length())));
|
|
|
|
|
System.err.flush();
|
|
|
|
|
|
|
|
|
|
if ("EXIT".equals(line.trim())) {
|
|
|
|
|
System.err.println("FopWorker exiting");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Parse job
|
|
|
|
|
System.err.println("DEBUG: Parsing job...");
|
|
|
|
|
System.err.flush();
|
|
|
|
|
|
|
|
|
|
String[] parts = line.split("\\t");
|
|
|
|
|
System.err.println("DEBUG: Parts count: " + parts.length);
|
|
|
|
|
System.err.flush();
|
|
|
|
|
|
|
|
|
|
if (parts.length < 2) {
|
|
|
|
|
System.out.println("ERROR: Invalid job format");
|
|
|
|
|
System.out.flush();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String inputFo = parts[0];
|
|
|
|
|
String outputPdf = parts[1];
|
|
|
|
|
|
|
|
|
|
System.err.println("DEBUG: Input FO: " + inputFo);
|
|
|
|
|
System.err.println("DEBUG: Output PDF: " + outputPdf);
|
|
|
|
|
System.err.flush();
|
|
|
|
|
|
|
|
|
|
// Create FOUserAgent for this transformation
|
|
|
|
|
FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
|
|
|
|
|
|
|
|
|
|
// Note: Event Listener für detailliertes Error-Logging könnte hier hinzugefügt werden,
|
|
|
|
|
// aber ist nicht kritisch - Fehler werden durch Exceptions gefangen
|
|
|
|
|
|
|
|
|
|
// Create output stream
|
|
|
|
|
File outputFile = new File(outputPdf);
|
|
|
|
|
outputFile.getParentFile().mkdirs();
|
|
|
|
|
OutputStream out = new BufferedOutputStream(new FileOutputStream(outputFile));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
System.err.println("DEBUG: Creating Fop instance...");
|
|
|
|
|
System.err.flush();
|
|
|
|
|
|
|
|
|
|
// Create Fop instance
|
|
|
|
|
Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, out);
|
|
|
|
|
|
|
|
|
|
System.err.println("DEBUG: Setting up transformer...");
|
|
|
|
|
System.err.flush();
|
|
|
|
|
|
|
|
|
|
// Setup Transformer
|
|
|
|
|
TransformerFactory transformerFactory = TransformerFactory.newInstance();
|
|
|
|
|
Transformer transformer = transformerFactory.newTransformer();
|
|
|
|
|
|
|
|
|
|
// Setup input and output
|
|
|
|
|
Source src = new StreamSource(new File(inputFo));
|
|
|
|
|
Result res = new SAXResult(fop.getDefaultHandler());
|
|
|
|
|
|
|
|
|
|
System.err.println("DEBUG: Running FOP transformation...");
|
|
|
|
|
System.err.flush();
|
|
|
|
|
|
|
|
|
|
// Run transformation
|
|
|
|
|
transformer.transform(src, res);
|
|
|
|
|
|
|
|
|
|
System.err.println("DEBUG: FOP transformation completed");
|
|
|
|
|
System.err.flush();
|
|
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
out.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transformation erfolgreich
|
|
|
|
|
System.out.println("OK");
|
|
|
|
|
System.out.flush();
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
System.err.println("DEBUG: Job processing exception: " + e.getClass().getName());
|
|
|
|
|
System.err.flush();
|
|
|
|
|
e.printStackTrace(System.err);
|
|
|
|
|
|
|
|
|
|
String errorMsg = e.getMessage();
|
|
|
|
|
if (errorMsg == null || errorMsg.isEmpty()) {
|
|
|
|
|
errorMsg = e.getClass().getSimpleName();
|
|
|
|
|
}
|
|
|
|
|
System.out.println("ERROR: " + errorMsg);
|
|
|
|
|
System.out.flush();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
System.err.println("FopWorker I/O error: " + e.getMessage());
|
|
|
|
|
e.printStackTrace(System.err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
class FopWorkerPool(BaseWorkerPool):
|
2026-01-04 17:24:19 +01:00
|
|
|
"""
|
|
|
|
|
Pool von lang-laufenden JVM-Prozessen für Apache FOP PDF-Generierung.
|
|
|
|
|
|
|
|
|
|
Eliminiert JVM-Startup-Overhead durch Wiederverwendung von N Worker-Prozessen.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
num_workers: int,
|
|
|
|
|
java_vm_path: Path,
|
|
|
|
|
apache_fop_dir: Path,
|
|
|
|
|
fop_config_file: Optional[Path] = None,
|
|
|
|
|
log_dir: Optional[Path] = None,
|
|
|
|
|
):
|
2026-03-09 19:49:57 +01:00
|
|
|
super().__init__(num_workers, java_vm_path, log_dir)
|
2026-01-04 17:24:19 +01:00
|
|
|
self.apache_fop_dir = apache_fop_dir
|
|
|
|
|
self.fop_config_file = fop_config_file
|
|
|
|
|
self.fop_classpath: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
self._build_fop_classpath()
|
|
|
|
|
self._compile_worker_class()
|
|
|
|
|
self._start_workers()
|
|
|
|
|
logger.info(f"FopWorkerPool initialisiert mit {num_workers} Workern")
|
|
|
|
|
|
|
|
|
|
def _build_fop_classpath(self):
|
|
|
|
|
"""Erstellt den Classpath für Apache FOP."""
|
|
|
|
|
all_jars = glob.glob(str(self.apache_fop_dir / "build" / "*.jar"))
|
|
|
|
|
lib_dir = self.apache_fop_dir / "lib"
|
|
|
|
|
if lib_dir.exists() and lib_dir.is_dir():
|
|
|
|
|
all_jars.extend(glob.glob(str(lib_dir / "*.jar")))
|
|
|
|
|
|
|
|
|
|
if not all_jars:
|
|
|
|
|
raise RuntimeError(f"Keine FOP JAR-Dateien gefunden in {self.apache_fop_dir}")
|
|
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
self.fop_classpath = _CLASSPATH_SEP.join(all_jars)
|
2026-01-04 17:24:19 +01:00
|
|
|
logger.debug(f"FOP Classpath: {len(all_jars)} JARs")
|
|
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
# --- Abstrakte Properties ---
|
2026-01-04 17:24:19 +01:00
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
@property
|
|
|
|
|
def _pool_name(self) -> str:
|
|
|
|
|
return "FOP"
|
2026-01-04 17:24:19 +01:00
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
@property
|
|
|
|
|
def _java_source_code(self) -> str:
|
|
|
|
|
return FOP_WORKER_JAVA
|
2026-01-04 17:24:19 +01:00
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
@property
|
|
|
|
|
def _java_class_name(self) -> str:
|
|
|
|
|
return "FopWorker"
|
2026-01-04 17:24:19 +01:00
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
@property
|
|
|
|
|
def _temp_dir_prefix(self) -> str:
|
|
|
|
|
return "fop_worker_"
|
2026-01-04 17:24:19 +01:00
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
@property
|
|
|
|
|
def _worker_init_sleep(self) -> float:
|
|
|
|
|
return 0.2 # FOP braucht etwas länger zum Initialisieren
|
2026-01-04 17:24:19 +01:00
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
# --- Abstrakte Methoden ---
|
2026-01-04 17:24:19 +01:00
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
def _get_classpath(self) -> str:
|
|
|
|
|
return self.fop_classpath
|
2026-01-06 20:58:37 +01:00
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
def _build_worker_cmd(self, full_classpath: str) -> list[str]:
|
|
|
|
|
cmd = [str(self.java_vm_path), "-cp", full_classpath, "FopWorker"]
|
|
|
|
|
if self.fop_config_file and self.fop_config_file.exists():
|
|
|
|
|
cmd.append(str(self.fop_config_file))
|
|
|
|
|
return cmd
|
2026-01-04 17:24:19 +01:00
|
|
|
|
2026-03-09 19:49:57 +01:00
|
|
|
def _stderr_log_name(self, i: int) -> str:
|
|
|
|
|
return f"fop_worker_{i}_stderr.log"
|
|
|
|
|
|
|
|
|
|
# --- FOP-spezifische Job-Methode ---
|
2026-01-04 17:24:19 +01:00
|
|
|
|
|
|
|
|
def build_pdf(self, input_fo: Path, output_pdf: Path) -> tuple[bool, str]:
|
|
|
|
|
"""
|
|
|
|
|
Generiert PDF aus FO-Datei mit einem Worker aus dem Pool.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
input_fo: Pfad zur FO-Eingabedatei
|
|
|
|
|
output_pdf: Pfad zur PDF-Ausgabedatei
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
|
|
|
|
|
"""
|
2026-03-09 19:49:57 +01:00
|
|
|
worker_idx = self._acquire_worker()
|
2026-01-04 17:24:19 +01:00
|
|
|
try:
|
|
|
|
|
worker = self.workers[worker_idx]
|
|
|
|
|
|
|
|
|
|
if worker.poll() is not None:
|
2026-03-09 19:49:57 +01:00
|
|
|
stderr_content = self._read_stderr_log(worker_idx)
|
|
|
|
|
error_msg = f"FOP Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}"
|
2026-01-04 17:24:19 +01:00
|
|
|
logger.error(error_msg)
|
|
|
|
|
return False, error_msg
|
|
|
|
|
|
|
|
|
|
job = f"{input_fo}\t{output_pdf}\n"
|
|
|
|
|
logger.debug(f"Sende FOP-Job an Worker {worker_idx}: {input_fo.name} → {output_pdf.name}")
|
|
|
|
|
worker.stdin.write(job)
|
|
|
|
|
worker.stdin.flush()
|
|
|
|
|
|
|
|
|
|
response = worker.stdout.readline().strip()
|
|
|
|
|
logger.debug(f"FOP Worker {worker_idx} Antwort: '{response}'")
|
|
|
|
|
|
|
|
|
|
if response == "OK":
|
|
|
|
|
return True, "Erfolgreich"
|
|
|
|
|
elif response.startswith("ERROR:"):
|
2026-03-09 19:49:57 +01:00
|
|
|
return False, f"FOP-Fehler: {response[6:].strip()}"
|
|
|
|
|
elif not response:
|
|
|
|
|
stderr_content = self._read_stderr_log(worker_idx, tail=500)
|
|
|
|
|
return False, f"FOP Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}"
|
2026-01-04 17:24:19 +01:00
|
|
|
else:
|
|
|
|
|
return False, f"Unerwartete Antwort: {response}"
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Fehler bei FOP Worker {worker_idx}: {e}")
|
|
|
|
|
return False, f"Worker-Fehler: {str(e)}"
|
|
|
|
|
finally:
|
|
|
|
|
self.worker_locks[worker_idx].release()
|