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);