#89: Log-Datei landet im MSI-Betrieb verlaesslich auf der Platte

Log4j2 referenziert nun ${sys:log.directory} mit nutzerschreibbarem
Fallback (~/pdf-umbenenner/logs). Die System-Property wird vor dem
ersten Logger-Zugriff aus der aktiven Konfigurationsdatei gesetzt
(EarlyLogDirectoryInitializer), damit Log4j2 bereits bei der
Erstinitialisierung den korrekten Pfad kennt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 17:52:35 +02:00
parent 479d176536
commit 6a5ae4e7b0
3 changed files with 101 additions and 5 deletions
@@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import de.gecheckt.pdf.umbenenner.bootstrap.startup.CliArgumentParser; import de.gecheckt.pdf.umbenenner.bootstrap.startup.CliArgumentParser;
import de.gecheckt.pdf.umbenenner.bootstrap.startup.EarlyLogDirectoryInitializer;
import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArguments; import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArguments;
import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArgumentsParseResult; import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArgumentsParseResult;
@@ -28,18 +29,22 @@ import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArgumentsParseResult;
*/ */
public class PdfUmbenennerApplication { public class PdfUmbenennerApplication {
private static final Logger LOG = LogManager.getLogger(PdfUmbenennerApplication.class);
/** /**
* Application entry point. * Application entry point.
* <p> * <p>
* Parses the command-line arguments and delegates to {@link BootstrapRunner}. * Parses the command-line arguments and delegates to {@link BootstrapRunner}.
* If the arguments cannot be parsed, an error is logged and the process exits * If the arguments cannot be parsed, an error is logged and the process exits
* with code 1 before any further initialisation takes place. * with code 1 before any further initialisation takes place.
* <p>
* Vor jeder Logger-Nutzung wird {@link EarlyLogDirectoryInitializer} aufgerufen,
* damit die Property {@code log.directory} aus der aktiven Konfigurationsdatei
* bereits vor der einmaligen Log4j2-Initialisierung gesetzt ist.
* *
* @param args command-line arguments; see class JavaDoc for supported options * @param args command-line arguments; see class JavaDoc for supported options
*/ */
public static void main(String[] args) { public static void main(String[] args) {
EarlyLogDirectoryInitializer.applyFromArgs(args);
Logger LOG = LogManager.getLogger(PdfUmbenennerApplication.class);
LOG.info("Starting PDF Umbenenner application..."); LOG.info("Starting PDF Umbenenner application...");
try { try {
StartupArgumentsParseResult parseResult = new CliArgumentParser().parse(args); StartupArgumentsParseResult parseResult = new CliArgumentParser().parse(args);
@@ -0,0 +1,87 @@
package de.gecheckt.pdf.umbenenner.bootstrap.startup;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
/**
* Liest die Log-Verzeichnis-Angabe so früh wie möglich aus der aktiven Konfigurationsdatei
* und setzt sie als System-Property {@code log.directory}, bevor Log4j2 zum ersten Mal
* initialisiert wird.
* <p>
* Hintergrund: {@code log4j2.xml} referenziert {@code ${sys:log.directory}} für den
* Pfad der Rolling-File-Datei. Da Log4j2 sich beim ersten {@code LogManager}-Aufruf
* einmalig konfiguriert, muss die System-Property bereits vorher gesetzt sein. Greift
* die Property nicht, fällt {@code log4j2.xml} auf ein nutzerschreibbares
* Default-Verzeichnis zurück, damit auch im MSI-Betrieb (Arbeitsverzeichnis unter
* {@code Program Files}, typischerweise nicht beschreibbar) eine Log-Datei entsteht.
* <p>
* Diese Klasse vermeidet bewusst jede Logger-Nutzung und schluckt sämtliche Fehler:
* Sie soll niemals den Programmstart verhindern, sondern lediglich einen frühen
* Best-Effort-Hinweis an Log4j2 liefern.
*/
public final class EarlyLogDirectoryInitializer {
private static final String SYSTEM_PROPERTY_KEY = "log.directory";
private static final String CONFIG_OPTION = "--config";
private static final Path DEFAULT_CONFIG_PATH = Paths.get("config/application.properties");
private static final String CONFIG_PROPERTY_KEY = "log.directory";
private EarlyLogDirectoryInitializer() {
// utility
}
/**
* Versucht, aus der aktiven Konfigurationsdatei den Wert von {@code log.directory}
* zu lesen, und setzt ihn als System-Property, sofern er ein nicht-leerer String ist.
* <p>
* Greift {@code --config <pfad>} auf, ansonsten {@code config/application.properties}
* relativ zum Arbeitsverzeichnis. Ist kein Wert ableitbar, bleibt die System-Property
* unverändert; in diesem Fall greift der in {@code log4j2.xml} hinterlegte Fallback.
*
* @param args Kommandozeilenargumente, dürfen {@code null} sein
*/
public static void applyFromArgs(String[] args) {
try {
if (System.getProperty(SYSTEM_PROPERTY_KEY) != null
&& !System.getProperty(SYSTEM_PROPERTY_KEY).isBlank()) {
return;
}
Path configPath = resolveConfigPath(args);
if (configPath == null || !Files.isRegularFile(configPath)) {
return;
}
Properties properties = new Properties();
try (InputStream in = Files.newInputStream(configPath)) {
properties.load(in);
}
String value = properties.getProperty(CONFIG_PROPERTY_KEY);
if (value != null && !value.isBlank()) {
System.setProperty(SYSTEM_PROPERTY_KEY, value.trim());
}
} catch (IOException | RuntimeException ignored) {
// bewusst still: Log4j2-Fallback aus log4j2.xml übernimmt ansonsten
}
}
private static Path resolveConfigPath(String[] args) {
if (args != null) {
for (int i = 0; i < args.length - 1; i++) {
if (CONFIG_OPTION.equals(args[i])) {
String raw = args[i + 1];
if (raw != null && !raw.isBlank()) {
try {
return Paths.get(raw);
} catch (RuntimeException ignored) {
return null;
}
}
}
}
}
return DEFAULT_CONFIG_PATH;
}
}
@@ -6,9 +6,13 @@
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
</Console> </Console>
<!-- Rolling file appender for logs in ./logs/ directory --> <!-- Rolling file appender. Honours the system property log.directory which
<RollingFile name="File" fileName="logs/pdf-umbenenner.log" is set very early in the bootstrap from the active configuration file.
filePattern="logs/pdf-umbenenner-%d{yyyy-MM-dd}-%i.log.gz"> The fallback intentionally points to a guaranteed-writable user-scoped
directory so MSI installs (where the working directory typically lives
under Program Files and is not user-writable) still produce log files. -->
<RollingFile name="File" fileName="${sys:log.directory:-${sys:user.home}/pdf-umbenenner/logs}/pdf-umbenenner.log"
filePattern="${sys:log.directory:-${sys:user.home}/pdf-umbenenner/logs}/pdf-umbenenner-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
<Policies> <Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/> <TimeBasedTriggeringPolicy interval="1" modulate="true"/>