diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java
index 762dead..2329fec 100644
--- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java
@@ -553,7 +553,8 @@ public final class GuiConfigurationEditorWorkspace {
() -> this.manualFileCopyPort,
() -> this.historicalDocumentContextPort,
this::editorSourceFolder,
- this::editorTargetFolder);
+ this::editorTargetFolder,
+ effectiveContext.configurationFileLockPort());
this.historyTab = new de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryTab(
effectiveContext.historyOverviewPort(),
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java
index a4463b5..65a91d3 100644
--- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java
@@ -19,6 +19,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorSt
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerControlUseCase;
+import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationFileLockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
import de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort;
@@ -62,6 +63,13 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
* was successfully wired at startup. An empty value means scheduler control is not
* available in this startup context (e.g., no valid configuration was loaded at startup).
*
+ * The optional {@code configurationFileLockPort} is present when the GUI can acquire an
+ * OS-level exclusive lock on the configuration file before a manual batch run. When present,
+ * it is acquired by the {@link de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunCoordinator}
+ * on the worker thread before each run and released in a finally block. An empty value means
+ * no locking is performed (e.g., no valid configuration was loaded at startup, or locking is
+ * not required in this context).
+ *
* All ports and services are supplied by Bootstrap so that the GUI adapter does not need to
* know about provider-specific HTTP details or adapter wiring.
*/
@@ -91,7 +99,8 @@ public record GuiStartupContext(
GuiPromptEditorPortFactory promptEditorPortFactory,
GuiCreateNewDatabasePort createNewDatabasePort,
Optional applicationContextError,
- Optional schedulerControlUseCase) {
+ Optional schedulerControlUseCase,
+ Optional configurationFileLockPort) {
/**
* Creates a fully wired startup context.
@@ -175,6 +184,7 @@ public record GuiStartupContext(
createNewDatabasePort = Objects.requireNonNull(createNewDatabasePort,
"createNewDatabasePort must not be null");
schedulerControlUseCase = schedulerControlUseCase == null ? Optional.empty() : schedulerControlUseCase;
+ configurationFileLockPort = configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort;
}
/**
@@ -242,7 +252,78 @@ public record GuiStartupContext(
historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetDocumentStatusPort,
deleteDocumentHistoryPort, promptEditorPortFactory, createNewDatabasePort,
- applicationContextError, Optional.empty());
+ applicationContextError, Optional.empty(), Optional.empty());
+ }
+
+ /**
+ * Backward-compatible constructor that fills {@code configurationFileLockPort} with
+ * {@link Optional#empty()}.
+ *
+ * Preserves existing callers that were written before the configuration file lock port
+ * was added.
+ *
+ * @param initialState initial editor state; must not be {@code null}
+ * @param startupNotice optional startup notice; {@code null} becomes empty
+ * @param configurationFileLoader file-loading callback; must not be {@code null}
+ * @param configurationFileWriter file-writing callback; must not be {@code null}
+ * @param modelCatalogPort port for retrieving AI model lists; must not be {@code null}
+ * @param apiKeyResolutionPort port for resolving API key provenance; must not be {@code null}
+ * @param providerTechnicalTestService service for provider-specific technical checks; must not be {@code null}
+ * @param pathCheckPort port for filesystem path accessibility checks; must not be {@code null}
+ * @param technicalTestOrchestrator orchestrator for the full technical test run; must not be {@code null}
+ * @param correctionExecutionService service for executing confirmed corrective actions; must not be {@code null}
+ * @param batchRunLauncher bridge that executes a regular batch run; must not be {@code null}
+ * @param miniRunLauncher bridge that executes a targeted mini-run; must not be {@code null}
+ * @param resetDocumentStatusPort bridge that resets document status; must not be {@code null}
+ * @param manualFileRenamePort bridge that renames a target file; must not be {@code null}
+ * @param manualFileCopyPort bridge that copies a source file; must not be {@code null}
+ * @param historicalDocumentContextPort bridge for historical processing context; must not be {@code null}
+ * @param applicationVersion resolved application version string; {@code null} defaults to {@code "dev"}
+ * @param promptEditorPort bridge zum Prompt-Editor-Use-Case; must not be {@code null}
+ * @param historyOverviewPort bridge for history overview; must not be {@code null}
+ * @param historyDetailsPort bridge for history details; must not be {@code null}
+ * @param historyResetDocumentStatusPort bridge for history reset; must not be {@code null}
+ * @param deleteDocumentHistoryPort bridge for history deletion; must not be {@code null}
+ * @param promptEditorPortFactory factory for prompt editor ports; must not be {@code null}
+ * @param createNewDatabasePort bridge for new database creation; must not be {@code null}
+ * @param applicationContextError optional error from context init; {@code null} becomes empty
+ * @param schedulerControlUseCase optional scheduler control use case; {@code null} becomes empty
+ */
+ public GuiStartupContext(
+ GuiConfigurationEditorState initialState,
+ Optional startupNotice,
+ GuiConfigurationFileLoader configurationFileLoader,
+ GuiConfigurationFileWriter configurationFileWriter,
+ AiModelCatalogPort modelCatalogPort,
+ ApiKeyResolutionPort apiKeyResolutionPort,
+ ProviderTechnicalTestService providerTechnicalTestService,
+ PathCheckPort pathCheckPort,
+ TechnicalTestOrchestrator technicalTestOrchestrator,
+ CorrectionExecutionService correctionExecutionService,
+ GuiBatchRunLauncher batchRunLauncher,
+ GuiMiniRunLauncher miniRunLauncher,
+ GuiResetDocumentStatusPort resetDocumentStatusPort,
+ GuiManualFileRenamePort manualFileRenamePort,
+ GuiManualFileCopyPort manualFileCopyPort,
+ GuiHistoricalDocumentContextPort historicalDocumentContextPort,
+ String applicationVersion,
+ GuiPromptEditorPort promptEditorPort,
+ GuiHistoryOverviewPort historyOverviewPort,
+ GuiHistoryDetailsPort historyDetailsPort,
+ GuiHistoryResetDocumentStatusPort historyResetDocumentStatusPort,
+ GuiDeleteDocumentHistoryPort deleteDocumentHistoryPort,
+ GuiPromptEditorPortFactory promptEditorPortFactory,
+ GuiCreateNewDatabasePort createNewDatabasePort,
+ Optional applicationContextError,
+ Optional schedulerControlUseCase) {
+ this(initialState, startupNotice, configurationFileLoader, configurationFileWriter,
+ modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
+ technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
+ miniRunLauncher, resetDocumentStatusPort, manualFileRenamePort, manualFileCopyPort,
+ historicalDocumentContextPort, applicationVersion, promptEditorPort,
+ historyOverviewPort, historyDetailsPort, historyResetDocumentStatusPort,
+ deleteDocumentHistoryPort, promptEditorPortFactory, createNewDatabasePort,
+ applicationContextError, schedulerControlUseCase, Optional.empty());
}
/**
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java
index 364f485..7474ec4 100644
--- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunCoordinator.java
@@ -21,9 +21,12 @@ import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
import de.gecheckt.pdf.umbenenner.application.port.in.HistoricalDocumentContext;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
import de.gecheckt.pdf.umbenenner.application.port.in.RunSummary;
+import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationFileLockException;
+import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationFileLockPort;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
import javafx.application.Platform;
+import javafx.scene.control.Alert;
/**
* Coordinates a single batch run (regular or targeted mini-run) triggered from the
@@ -115,6 +118,7 @@ public final class GuiBatchRunCoordinator {
private final Consumer fxDispatcher;
private final Listener listener;
private final GuiHistoricalDocumentContextPort historicalDocumentContextPort;
+ private final Optional configurationFileLockPort;
private final AtomicReference activeWorker = new AtomicReference<>();
private final AtomicBoolean cancellationRequested = new AtomicBoolean();
@@ -176,6 +180,33 @@ public final class GuiBatchRunCoordinator {
defaultThreadFactory(), defaultFxDispatcher(), listener, historicalDocumentContextPort);
}
+ /**
+ * Creates the coordinator with all ports and the configuration file lock port, using
+ * the default worker-thread factory and JavaFX Application Thread dispatcher.
+ *
+ * This constructor is intended for production wiring in {@code GuiBatchRunTab} where
+ * the lock port is supplied by Bootstrap.
+ *
+ * @param launcher bridge to Bootstrap for regular batch runs; must not be null
+ * @param miniRunLauncher bridge to Bootstrap for targeted mini-runs; must not be null
+ * @param resetPort bridge to Bootstrap for status-reset-only operations; must
+ * not be null
+ * @param listener GUI listener invoked on the FX thread; must not be null
+ * @param historicalDocumentContextPort port for resolving historical context; must not be null
+ * @param configurationFileLockPort optional OS-lock on the configuration file; when present,
+ * acquired before each run; {@code null} is treated as empty
+ */
+ public GuiBatchRunCoordinator(GuiBatchRunLauncher launcher,
+ GuiMiniRunLauncher miniRunLauncher,
+ GuiResetDocumentStatusPort resetPort,
+ Listener listener,
+ GuiHistoricalDocumentContextPort historicalDocumentContextPort,
+ Optional configurationFileLockPort) {
+ this(launcher, miniRunLauncher, resetPort,
+ defaultThreadFactory(), defaultFxDispatcher(), listener,
+ historicalDocumentContextPort, configurationFileLockPort);
+ }
+
/**
* Creates the coordinator with custom hooks for the worker-thread factory and the
* UI-thread dispatcher.
@@ -205,22 +236,25 @@ public final class GuiBatchRunCoordinator {
}
/**
- * Creates the coordinator with all ports, custom thread factory, FX dispatcher and
- * historical file name port.
+ * Creates the coordinator with all ports, custom thread factory, FX dispatcher,
+ * historical file name port, and an optional configuration file lock port.
*
* This is the canonical constructor. All other constructors delegate here.
*
- * @param launcher bridge to Bootstrap for regular batch runs; must not be null
- * @param miniRunLauncher bridge to Bootstrap for targeted mini-runs; must not be null
- * @param resetPort bridge to Bootstrap for status-reset-only operations; must
- * not be null
- * @param threadFactory factory returning a ready-to-start worker thread; must not
- * be null
- * @param fxDispatcher dispatcher that schedules a runnable on the JavaFX Application
- * Thread; must not be null
- * @param listener GUI listener; must not be null
- * @param historicalDocumentContextPort port for resolving the historical AI-proposed filename for
- * skipped documents; must not be null
+ * @param launcher bridge to Bootstrap for regular batch runs; must not be null
+ * @param miniRunLauncher bridge to Bootstrap for targeted mini-runs; must not be null
+ * @param resetPort bridge to Bootstrap for status-reset-only operations; must
+ * not be null
+ * @param threadFactory factory returning a ready-to-start worker thread; must not
+ * be null
+ * @param fxDispatcher dispatcher that schedules a runnable on the JavaFX Application
+ * Thread; must not be null
+ * @param listener GUI listener; must not be null
+ * @param historicalDocumentContextPort port for resolving the historical AI-proposed filename for
+ * skipped documents; must not be null
+ * @param configurationFileLockPort optional OS-lock on the configuration file; when present,
+ * acquired before each run and released in a finally block;
+ * {@code null} is treated as empty
*/
public GuiBatchRunCoordinator(GuiBatchRunLauncher launcher,
GuiMiniRunLauncher miniRunLauncher,
@@ -228,7 +262,8 @@ public final class GuiBatchRunCoordinator {
Function threadFactory,
Consumer fxDispatcher,
Listener listener,
- GuiHistoricalDocumentContextPort historicalDocumentContextPort) {
+ GuiHistoricalDocumentContextPort historicalDocumentContextPort,
+ Optional configurationFileLockPort) {
this.launcher = Objects.requireNonNull(launcher, "launcher must not be null");
this.miniRunLauncher = Objects.requireNonNull(miniRunLauncher, "miniRunLauncher must not be null");
this.resetPort = Objects.requireNonNull(resetPort, "resetPort must not be null");
@@ -237,6 +272,37 @@ public final class GuiBatchRunCoordinator {
this.listener = Objects.requireNonNull(listener, "listener must not be null");
this.historicalDocumentContextPort = Objects.requireNonNull(
historicalDocumentContextPort, "historicalDocumentContextPort must not be null");
+ this.configurationFileLockPort =
+ configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort;
+ }
+
+ /**
+ * Backward-compatible constructor that omits the configuration file lock port.
+ *
+ * Preserves existing callers that were written before the lock port was added.
+ * Delegates to the canonical constructor with {@code configurationFileLockPort} empty.
+ *
+ * @param launcher bridge to Bootstrap for regular batch runs; must not be null
+ * @param miniRunLauncher bridge to Bootstrap for targeted mini-runs; must not be null
+ * @param resetPort bridge to Bootstrap for status-reset-only operations; must
+ * not be null
+ * @param threadFactory factory returning a ready-to-start worker thread; must not
+ * be null
+ * @param fxDispatcher dispatcher that schedules a runnable on the JavaFX Application
+ * Thread; must not be null
+ * @param listener GUI listener; must not be null
+ * @param historicalDocumentContextPort port for resolving the historical AI-proposed filename for
+ * skipped documents; must not be null
+ */
+ public GuiBatchRunCoordinator(GuiBatchRunLauncher launcher,
+ GuiMiniRunLauncher miniRunLauncher,
+ GuiResetDocumentStatusPort resetPort,
+ Function threadFactory,
+ Consumer fxDispatcher,
+ Listener listener,
+ GuiHistoricalDocumentContextPort historicalDocumentContextPort) {
+ this(launcher, miniRunLauncher, resetPort, threadFactory, fxDispatcher, listener,
+ historicalDocumentContextPort, Optional.empty());
}
/**
@@ -437,46 +503,82 @@ public final class GuiBatchRunCoordinator {
LOG.info("GUI-Verarbeitungslauf: Worker-Thread gestartet für Konfiguration {}.",
configFilePath);
observerSummary.set(null);
- BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
- BatchRunCancellationToken token = cancellationRequested::get;
- GuiBatchRunLaunchOutcome outcome;
- try {
- outcome = launcher.launch(configFilePath, observer, token);
- if (outcome == null) {
- outcome = GuiBatchRunLaunchOutcome.failedAfterStart(
- "Launcher hat kein Ergebnis geliefert.");
+
+ if (configurationFileLockPort.isPresent()) {
+ try {
+ configurationFileLockPort.get().acquireLock();
+ } catch (ConfigurationFileLockException e) {
+ LOG.warn("GUI-Verarbeitungslauf: Konfigurationsdatei gesperrt – Lauf abgebrochen: {}",
+ e.getMessage());
+ fxDispatcher.accept(() -> showLockErrorAlert());
+ finishRun(GuiBatchRunLaunchOutcome.rejected(
+ "Konfigurationsdatei gesperrt – Lauf wurde abgebrochen."));
+ return;
}
- } catch (RuntimeException e) {
- LOG.error("GUI-Verarbeitungslauf: Unerwarteter Fehler im Worker-Thread: {}",
- e.getMessage(), e);
- outcome = GuiBatchRunLaunchOutcome.failedAfterStart(
- "Unerwarteter technischer Fehler: "
- + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
}
- finishRun(outcome);
+
+ try {
+ BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
+ BatchRunCancellationToken token = cancellationRequested::get;
+ GuiBatchRunLaunchOutcome outcome;
+ try {
+ outcome = launcher.launch(configFilePath, observer, token);
+ if (outcome == null) {
+ outcome = GuiBatchRunLaunchOutcome.failedAfterStart(
+ "Launcher hat kein Ergebnis geliefert.");
+ }
+ } catch (RuntimeException e) {
+ LOG.error("GUI-Verarbeitungslauf: Unerwarteter Fehler im Worker-Thread: {}",
+ e.getMessage(), e);
+ outcome = GuiBatchRunLaunchOutcome.failedAfterStart(
+ "Unerwarteter technischer Fehler: "
+ + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
+ }
+ finishRun(outcome);
+ } finally {
+ configurationFileLockPort.ifPresent(ConfigurationFileLockPort::releaseLock);
+ }
}
private void executeMiniRun(Path configFilePath, Set fingerprintFilter) {
LOG.info("GUI-Mini-Verarbeitungslauf: Worker-Thread gestartet für {} Dokument(e), "
+ "Konfiguration {}.", fingerprintFilter.size(), configFilePath);
observerSummary.set(null);
- BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
- BatchRunCancellationToken token = cancellationRequested::get;
- GuiBatchRunLaunchOutcome outcome;
- try {
- outcome = miniRunLauncher.launch(configFilePath, fingerprintFilter, observer, token);
- if (outcome == null) {
- outcome = GuiBatchRunLaunchOutcome.failedAfterStart(
- "Mini-Run-Launcher hat kein Ergebnis geliefert.");
+
+ if (configurationFileLockPort.isPresent()) {
+ try {
+ configurationFileLockPort.get().acquireLock();
+ } catch (ConfigurationFileLockException e) {
+ LOG.warn("GUI-Mini-Verarbeitungslauf: Konfigurationsdatei gesperrt – Lauf abgebrochen: {}",
+ e.getMessage());
+ fxDispatcher.accept(() -> showLockErrorAlert());
+ finishRun(GuiBatchRunLaunchOutcome.rejected(
+ "Konfigurationsdatei gesperrt – Mini-Lauf wurde abgebrochen."));
+ return;
}
- } catch (RuntimeException e) {
- LOG.error("GUI-Mini-Verarbeitungslauf: Unerwarteter Fehler im Worker-Thread: {}",
- e.getMessage(), e);
- outcome = GuiBatchRunLaunchOutcome.failedAfterStart(
- "Unerwarteter technischer Fehler im Mini-Lauf: "
- + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
}
- finishRun(outcome);
+
+ try {
+ BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
+ BatchRunCancellationToken token = cancellationRequested::get;
+ GuiBatchRunLaunchOutcome outcome;
+ try {
+ outcome = miniRunLauncher.launch(configFilePath, fingerprintFilter, observer, token);
+ if (outcome == null) {
+ outcome = GuiBatchRunLaunchOutcome.failedAfterStart(
+ "Mini-Run-Launcher hat kein Ergebnis geliefert.");
+ }
+ } catch (RuntimeException e) {
+ LOG.error("GUI-Mini-Verarbeitungslauf: Unerwarteter Fehler im Worker-Thread: {}",
+ e.getMessage(), e);
+ outcome = GuiBatchRunLaunchOutcome.failedAfterStart(
+ "Unerwarteter technischer Fehler im Mini-Lauf: "
+ + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
+ }
+ finishRun(outcome);
+ } finally {
+ configurationFileLockPort.ifPresent(ConfigurationFileLockPort::releaseLock);
+ }
}
private void executeReset(Path configFilePath, Set fingerprints) {
@@ -611,6 +713,19 @@ public final class GuiBatchRunCoordinator {
historicalContext);
}
+ private static void showLockErrorAlert() {
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setTitle("Verarbeitungslauf nicht möglich");
+ alert.setHeaderText("Konfigurationsdatei gesperrt");
+ alert.setContentText(
+ "Der Verarbeitungslauf konnte nicht gestartet werden, da die "
+ + "Konfigurationsdatei nicht gesperrt werden konnte.\n\n"
+ + "Mögliche Ursache: Der automatische Scheduler ist aktiv oder "
+ + "ein anderer Prozess hält die Datei belegt.\n\n"
+ + "Bitte stoppen Sie den Scheduler und versuchen Sie es erneut.");
+ alert.showAndWait();
+ }
+
private static GuiHistoricalDocumentContextPort noOpHistoricalDocumentContextPort() {
return (configPath, fingerprint) -> Optional.empty();
}
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java
index 9269cf2..4cf4695 100644
--- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java
@@ -41,6 +41,7 @@ import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameSourceFile
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileRenameSuccess;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
import de.gecheckt.pdf.umbenenner.application.port.in.RunSummary;
+import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationFileLockPort;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiTooltipTexts;
@@ -231,7 +232,8 @@ public final class GuiBatchRunTab {
/**
* Erstellt den Verarbeitungslauf-Tab mit allen Verarbeitungs-, Mini-Lauf- und
- * Rücksetz-Fähigkeiten sowie dem Dateiname-Editor und der PDF-Vorschau.
+ * Rücksetz-Fähigkeiten sowie dem Dateiname-Editor, der PDF-Vorschau und einem
+ * optionalen OS-Lock auf die Konfigurationsdatei.
*
* @param launcherSupplier Supplier für den aktiven Batch-Lauf-Launcher;
* darf nicht null sein
@@ -256,6 +258,9 @@ public final class GuiBatchRunTab {
* darf leeres Optional zurückliefern
* @param targetFolderSupplier Supplier für den konfigurierten Zielordner als
* Pfad-String; darf leeres Optional zurückliefern
+ * @param configurationFileLockPort optionaler OS-Lock auf die Konfigurationsdatei;
+ * wird vor jedem Lauf erworben und danach freigegeben;
+ * {@code null} wird als leer behandelt
*/
public GuiBatchRunTab(Supplier launcherSupplier,
Supplier miniRunLauncherSupplier,
@@ -267,7 +272,8 @@ public final class GuiBatchRunTab {
Supplier manualFileCopyPortSupplier,
Supplier historicalDocumentContextPortSupplier,
Supplier> sourceFolderSupplier,
- Supplier> targetFolderSupplier) {
+ Supplier> targetFolderSupplier,
+ Optional configurationFileLockPort) {
Objects.requireNonNull(launcherSupplier, "launcherSupplier must not be null");
Objects.requireNonNull(miniRunLauncherSupplier, "miniRunLauncherSupplier must not be null");
Objects.requireNonNull(resetPortSupplier, "resetPortSupplier must not be null");
@@ -286,6 +292,8 @@ public final class GuiBatchRunTab {
this.targetFolderSupplier = Objects.requireNonNull(
targetFolderSupplier, "targetFolderSupplier must not be null");
+ Optional effectiveLockPort =
+ configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort;
this.coordinator = new GuiBatchRunCoordinator(
(configPath, observer, token) ->
launcherSupplier.get().launch(configPath, observer, token),
@@ -294,7 +302,8 @@ public final class GuiBatchRunTab {
(configPath, fingerprints) ->
resetPortSupplier.get().reset(configPath, fingerprints),
new CoordinatorListener(),
- historicalDocumentContextPortSupplier.get());
+ historicalDocumentContextPortSupplier.get(),
+ effectiveLockPort);
this.tab.setClosable(false);
this.tab.setContent(buildContent());
@@ -313,6 +322,51 @@ public final class GuiBatchRunTab {
updateButtonStates();
}
+ /**
+ * Rückwärtskompatible Variante ohne OS-Lock auf die Konfigurationsdatei.
+ *
+ * Alle bestehenden Aufrufer, die vor der Lock-Port-Erweiterung erstellt wurden,
+ * nutzen diesen Konstruktor. Er delegiert an den kanonischen Konstruktor mit
+ * {@code configurationFileLockPort = Optional.empty()}.
+ *
+ * @param launcherSupplier Supplier für den aktiven Batch-Lauf-Launcher;
+ * darf nicht null sein
+ * @param miniRunLauncherSupplier Supplier für den Mini-Lauf-Launcher;
+ * darf nicht null sein
+ * @param resetPortSupplier Supplier für den Rücksetz-Port;
+ * darf nicht null sein
+ * @param configPathSupplier Supplier für den letzten gespeicherten
+ * Konfigurationspfad; darf null zurückliefern
+ * @param savedConfigurationReadyCheck Prüfung vor jedem Startversuch; darf nicht
+ * null sein
+ * @param onRunStateChanged Callback wenn das Lauf-Flag kippt; darf nicht
+ * null sein
+ * @param manualFileRenamePortSupplier Supplier für den manuellen Umbenennungs-Port;
+ * darf nicht null sein
+ * @param manualFileCopyPortSupplier Supplier für den manuellen Kopier-Port;
+ * darf nicht null sein
+ * @param historicalDocumentContextPortSupplier Supplier für den historischen Kontext-Port;
+ * darf nicht null sein
+ * @param sourceFolderSupplier Supplier für den konfigurierten Quellordner
+ * @param targetFolderSupplier Supplier für den konfigurierten Zielordner
+ */
+ public GuiBatchRunTab(Supplier launcherSupplier,
+ Supplier miniRunLauncherSupplier,
+ Supplier resetPortSupplier,
+ Supplier configPathSupplier,
+ BooleanSupplier savedConfigurationReadyCheck,
+ Runnable onRunStateChanged,
+ Supplier manualFileRenamePortSupplier,
+ Supplier manualFileCopyPortSupplier,
+ Supplier historicalDocumentContextPortSupplier,
+ Supplier> sourceFolderSupplier,
+ Supplier> targetFolderSupplier) {
+ this(launcherSupplier, miniRunLauncherSupplier, resetPortSupplier, configPathSupplier,
+ savedConfigurationReadyCheck, onRunStateChanged, manualFileRenamePortSupplier,
+ manualFileCopyPortSupplier, historicalDocumentContextPortSupplier,
+ sourceFolderSupplier, targetFolderSupplier, Optional.empty());
+ }
+
/**
* Rückwärtskompatible Variante für Aufrufer ohne Mini-Lauf- oder Rücksetz-Fähigkeiten.
*
diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java
index c2757ba..f7a64d8 100644
--- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java
+++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java
@@ -78,6 +78,7 @@ import de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalDocumentC
import de.gecheckt.pdf.umbenenner.application.port.out.ActiveDatabaseContextPort;
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
import de.gecheckt.pdf.umbenenner.application.port.out.BatchRunTrigger;
+import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationFileLockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.BatchRunTriggerResult;
import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary;
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
@@ -974,8 +975,10 @@ public class BootstrapRunner {
GuiPromptEditorPort promptEditorPort = buildGuiPromptEditorPort(
loadedState.values().promptTemplateFile());
Optional contextError = initializeApplicationRunContext(configPath);
+ Optional guiRunLockPort = Optional.empty();
if (contextError.isEmpty()) {
tryInitializeScheduler(configPath);
+ guiRunLockPort = Optional.of(new FileChannelConfigurationAccessAdapter(configPath));
}
Optional schedulerUseCase =
guiSchedulerUseCase.map(s -> (SchedulerControlUseCase) s);
@@ -986,7 +989,7 @@ public class BootstrapRunner {
historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort,
this::buildGuiPromptEditorPort, createNewDatabasePort, contextError,
- schedulerUseCase);
+ schedulerUseCase, guiRunLockPort);
} catch (GuiConfigurationLoadException e) {
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
e.getMessage(), e);