Erwirb Config-Lock vor manuellem Verarbeitungslauf in der GUI

GuiBatchRunCoordinator erwirbt vor jedem Verarbeitungslauf (regulär und
Mini-Lauf) einen exklusiven OS-Lock auf die Konfigurationsdatei via
ConfigurationFileLockPort. Bei ConfigurationFileLockException wird ein
deutscher Fehlerdialog angezeigt und der Lauf abgebrochen. In finally
wird der Lock immer freigegeben.

GuiStartupContext erhält das 27. Feld configurationFileLockPort;
BootstrapRunner befüllt es mit einem FileChannelConfigurationAccessAdapter
wenn eine Konfigurationsdatei geladen wurde. GuiBatchRunTab und
GuiConfigurationEditorWorkspace reichen den Port durch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 15:11:55 +02:00
parent ce87b0bbec
commit 74e825d1f4
5 changed files with 305 additions and 51 deletions
@@ -553,7 +553,8 @@ public final class GuiConfigurationEditorWorkspace {
() -> this.manualFileCopyPort, () -> this.manualFileCopyPort,
() -> this.historicalDocumentContextPort, () -> this.historicalDocumentContextPort,
this::editorSourceFolder, this::editorSourceFolder,
this::editorTargetFolder); this::editorTargetFolder,
effectiveContext.configurationFileLockPort());
this.historyTab = new de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryTab( this.historyTab = new de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryTab(
effectiveContext.historyOverviewPort(), effectiveContext.historyOverviewPort(),
@@ -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.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult; 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.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.AiModelCatalogPort;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor; import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
import de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort; 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 * 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). * available in this startup context (e.g., no valid configuration was loaded at startup).
* <p> * <p>
* 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).
* <p>
* All ports and services are supplied by Bootstrap so that the GUI adapter does not need to * 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. * know about provider-specific HTTP details or adapter wiring.
*/ */
@@ -91,7 +99,8 @@ public record GuiStartupContext(
GuiPromptEditorPortFactory promptEditorPortFactory, GuiPromptEditorPortFactory promptEditorPortFactory,
GuiCreateNewDatabasePort createNewDatabasePort, GuiCreateNewDatabasePort createNewDatabasePort,
Optional<String> applicationContextError, Optional<String> applicationContextError,
Optional<SchedulerControlUseCase> schedulerControlUseCase) { Optional<SchedulerControlUseCase> schedulerControlUseCase,
Optional<ConfigurationFileLockPort> configurationFileLockPort) {
/** /**
* Creates a fully wired startup context. * Creates a fully wired startup context.
@@ -175,6 +184,7 @@ public record GuiStartupContext(
createNewDatabasePort = Objects.requireNonNull(createNewDatabasePort, createNewDatabasePort = Objects.requireNonNull(createNewDatabasePort,
"createNewDatabasePort must not be null"); "createNewDatabasePort must not be null");
schedulerControlUseCase = schedulerControlUseCase == null ? Optional.empty() : schedulerControlUseCase; schedulerControlUseCase = schedulerControlUseCase == null ? Optional.empty() : schedulerControlUseCase;
configurationFileLockPort = configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort;
} }
/** /**
@@ -242,7 +252,78 @@ public record GuiStartupContext(
historicalDocumentContextPort, applicationVersion, promptEditorPort, historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetDocumentStatusPort, historyOverviewPort, historyDetailsPort, historyResetDocumentStatusPort,
deleteDocumentHistoryPort, promptEditorPortFactory, createNewDatabasePort, deleteDocumentHistoryPort, promptEditorPortFactory, createNewDatabasePort,
applicationContextError, Optional.empty()); applicationContextError, Optional.empty(), Optional.empty());
}
/**
* Backward-compatible constructor that fills {@code configurationFileLockPort} with
* {@link Optional#empty()}.
* <p>
* 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<String> 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<String> applicationContextError,
Optional<SchedulerControlUseCase> 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());
} }
/** /**
@@ -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.HistoricalDocumentContext;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult; 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.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.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.RunId; import de.gecheckt.pdf.umbenenner.domain.model.RunId;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.scene.control.Alert;
/** /**
* Coordinates a single batch run (regular or targeted mini-run) triggered from the * Coordinates a single batch run (regular or targeted mini-run) triggered from the
@@ -115,6 +118,7 @@ public final class GuiBatchRunCoordinator {
private final Consumer<Runnable> fxDispatcher; private final Consumer<Runnable> fxDispatcher;
private final Listener listener; private final Listener listener;
private final GuiHistoricalDocumentContextPort historicalDocumentContextPort; private final GuiHistoricalDocumentContextPort historicalDocumentContextPort;
private final Optional<ConfigurationFileLockPort> configurationFileLockPort;
private final AtomicReference<Thread> activeWorker = new AtomicReference<>(); private final AtomicReference<Thread> activeWorker = new AtomicReference<>();
private final AtomicBoolean cancellationRequested = new AtomicBoolean(); private final AtomicBoolean cancellationRequested = new AtomicBoolean();
@@ -176,6 +180,33 @@ public final class GuiBatchRunCoordinator {
defaultThreadFactory(), defaultFxDispatcher(), listener, historicalDocumentContextPort); 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.
* <p>
* 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> configurationFileLockPort) {
this(launcher, miniRunLauncher, resetPort,
defaultThreadFactory(), defaultFxDispatcher(), listener,
historicalDocumentContextPort, configurationFileLockPort);
}
/** /**
* Creates the coordinator with custom hooks for the worker-thread factory and the * Creates the coordinator with custom hooks for the worker-thread factory and the
* UI-thread dispatcher. * UI-thread dispatcher.
@@ -205,22 +236,25 @@ public final class GuiBatchRunCoordinator {
} }
/** /**
* Creates the coordinator with all ports, custom thread factory, FX dispatcher and * Creates the coordinator with all ports, custom thread factory, FX dispatcher,
* historical file name port. * historical file name port, and an optional configuration file lock port.
* <p> * <p>
* This is the canonical constructor. All other constructors delegate here. * This is the canonical constructor. All other constructors delegate here.
* *
* @param launcher bridge to Bootstrap for regular batch runs; 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 miniRunLauncher bridge to Bootstrap for targeted mini-runs; must not be null
* @param resetPort bridge to Bootstrap for status-reset-only operations; must * @param resetPort bridge to Bootstrap for status-reset-only operations; must
* not be null * not be null
* @param threadFactory factory returning a ready-to-start worker thread; must not * @param threadFactory factory returning a ready-to-start worker thread; must not
* be null * be null
* @param fxDispatcher dispatcher that schedules a runnable on the JavaFX Application * @param fxDispatcher dispatcher that schedules a runnable on the JavaFX Application
* Thread; must not be null * Thread; must not be null
* @param listener GUI listener; must not be null * @param listener GUI listener; must not be null
* @param historicalDocumentContextPort port for resolving the historical AI-proposed filename for * @param historicalDocumentContextPort port for resolving the historical AI-proposed filename for
* skipped documents; must not be null * 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, public GuiBatchRunCoordinator(GuiBatchRunLauncher launcher,
GuiMiniRunLauncher miniRunLauncher, GuiMiniRunLauncher miniRunLauncher,
@@ -228,7 +262,8 @@ public final class GuiBatchRunCoordinator {
Function<Runnable, Thread> threadFactory, Function<Runnable, Thread> threadFactory,
Consumer<Runnable> fxDispatcher, Consumer<Runnable> fxDispatcher,
Listener listener, Listener listener,
GuiHistoricalDocumentContextPort historicalDocumentContextPort) { GuiHistoricalDocumentContextPort historicalDocumentContextPort,
Optional<ConfigurationFileLockPort> configurationFileLockPort) {
this.launcher = Objects.requireNonNull(launcher, "launcher must not be null"); this.launcher = Objects.requireNonNull(launcher, "launcher must not be null");
this.miniRunLauncher = Objects.requireNonNull(miniRunLauncher, "miniRunLauncher must not be null"); this.miniRunLauncher = Objects.requireNonNull(miniRunLauncher, "miniRunLauncher must not be null");
this.resetPort = Objects.requireNonNull(resetPort, "resetPort 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.listener = Objects.requireNonNull(listener, "listener must not be null");
this.historicalDocumentContextPort = Objects.requireNonNull( this.historicalDocumentContextPort = Objects.requireNonNull(
historicalDocumentContextPort, "historicalDocumentContextPort must not be null"); historicalDocumentContextPort, "historicalDocumentContextPort must not be null");
this.configurationFileLockPort =
configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort;
}
/**
* Backward-compatible constructor that omits the configuration file lock port.
* <p>
* 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<Runnable, Thread> threadFactory,
Consumer<Runnable> 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 {}.", LOG.info("GUI-Verarbeitungslauf: Worker-Thread gestartet für Konfiguration {}.",
configFilePath); configFilePath);
observerSummary.set(null); observerSummary.set(null);
BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
BatchRunCancellationToken token = cancellationRequested::get; if (configurationFileLockPort.isPresent()) {
GuiBatchRunLaunchOutcome outcome; try {
try { configurationFileLockPort.get().acquireLock();
outcome = launcher.launch(configFilePath, observer, token); } catch (ConfigurationFileLockException e) {
if (outcome == null) { LOG.warn("GUI-Verarbeitungslauf: Konfigurationsdatei gesperrt Lauf abgebrochen: {}",
outcome = GuiBatchRunLaunchOutcome.failedAfterStart( e.getMessage());
"Launcher hat kein Ergebnis geliefert."); 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<DocumentFingerprint> fingerprintFilter) { private void executeMiniRun(Path configFilePath, Set<DocumentFingerprint> fingerprintFilter) {
LOG.info("GUI-Mini-Verarbeitungslauf: Worker-Thread gestartet für {} Dokument(e), " LOG.info("GUI-Mini-Verarbeitungslauf: Worker-Thread gestartet für {} Dokument(e), "
+ "Konfiguration {}.", fingerprintFilter.size(), configFilePath); + "Konfiguration {}.", fingerprintFilter.size(), configFilePath);
observerSummary.set(null); observerSummary.set(null);
BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
BatchRunCancellationToken token = cancellationRequested::get; if (configurationFileLockPort.isPresent()) {
GuiBatchRunLaunchOutcome outcome; try {
try { configurationFileLockPort.get().acquireLock();
outcome = miniRunLauncher.launch(configFilePath, fingerprintFilter, observer, token); } catch (ConfigurationFileLockException e) {
if (outcome == null) { LOG.warn("GUI-Mini-Verarbeitungslauf: Konfigurationsdatei gesperrt Lauf abgebrochen: {}",
outcome = GuiBatchRunLaunchOutcome.failedAfterStart( e.getMessage());
"Mini-Run-Launcher hat kein Ergebnis geliefert."); 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<DocumentFingerprint> fingerprints) { private void executeReset(Path configFilePath, Set<DocumentFingerprint> fingerprints) {
@@ -611,6 +713,19 @@ public final class GuiBatchRunCoordinator {
historicalContext); 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() { private static GuiHistoricalDocumentContextPort noOpHistoricalDocumentContextPort() {
return (configPath, fingerprint) -> Optional.empty(); return (configPath, fingerprint) -> Optional.empty();
} }
@@ -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.ManualFileRenameSuccess;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult; 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.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.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.RunId; import de.gecheckt.pdf.umbenenner.domain.model.RunId;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiTooltipTexts; 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 * 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; * @param launcherSupplier Supplier für den aktiven Batch-Lauf-Launcher;
* darf nicht null sein * darf nicht null sein
@@ -256,6 +258,9 @@ public final class GuiBatchRunTab {
* darf leeres Optional zurückliefern * darf leeres Optional zurückliefern
* @param targetFolderSupplier Supplier für den konfigurierten Zielordner als * @param targetFolderSupplier Supplier für den konfigurierten Zielordner als
* Pfad-String; darf leeres Optional zurückliefern * 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<GuiBatchRunLauncher> launcherSupplier, public GuiBatchRunTab(Supplier<GuiBatchRunLauncher> launcherSupplier,
Supplier<GuiMiniRunLauncher> miniRunLauncherSupplier, Supplier<GuiMiniRunLauncher> miniRunLauncherSupplier,
@@ -267,7 +272,8 @@ public final class GuiBatchRunTab {
Supplier<GuiManualFileCopyPort> manualFileCopyPortSupplier, Supplier<GuiManualFileCopyPort> manualFileCopyPortSupplier,
Supplier<GuiHistoricalDocumentContextPort> historicalDocumentContextPortSupplier, Supplier<GuiHistoricalDocumentContextPort> historicalDocumentContextPortSupplier,
Supplier<Optional<Path>> sourceFolderSupplier, Supplier<Optional<Path>> sourceFolderSupplier,
Supplier<Optional<String>> targetFolderSupplier) { Supplier<Optional<String>> targetFolderSupplier,
Optional<ConfigurationFileLockPort> configurationFileLockPort) {
Objects.requireNonNull(launcherSupplier, "launcherSupplier must not be null"); Objects.requireNonNull(launcherSupplier, "launcherSupplier must not be null");
Objects.requireNonNull(miniRunLauncherSupplier, "miniRunLauncherSupplier must not be null"); Objects.requireNonNull(miniRunLauncherSupplier, "miniRunLauncherSupplier must not be null");
Objects.requireNonNull(resetPortSupplier, "resetPortSupplier must not be null"); Objects.requireNonNull(resetPortSupplier, "resetPortSupplier must not be null");
@@ -286,6 +292,8 @@ public final class GuiBatchRunTab {
this.targetFolderSupplier = Objects.requireNonNull( this.targetFolderSupplier = Objects.requireNonNull(
targetFolderSupplier, "targetFolderSupplier must not be null"); targetFolderSupplier, "targetFolderSupplier must not be null");
Optional<ConfigurationFileLockPort> effectiveLockPort =
configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort;
this.coordinator = new GuiBatchRunCoordinator( this.coordinator = new GuiBatchRunCoordinator(
(configPath, observer, token) -> (configPath, observer, token) ->
launcherSupplier.get().launch(configPath, observer, token), launcherSupplier.get().launch(configPath, observer, token),
@@ -294,7 +302,8 @@ public final class GuiBatchRunTab {
(configPath, fingerprints) -> (configPath, fingerprints) ->
resetPortSupplier.get().reset(configPath, fingerprints), resetPortSupplier.get().reset(configPath, fingerprints),
new CoordinatorListener(), new CoordinatorListener(),
historicalDocumentContextPortSupplier.get()); historicalDocumentContextPortSupplier.get(),
effectiveLockPort);
this.tab.setClosable(false); this.tab.setClosable(false);
this.tab.setContent(buildContent()); this.tab.setContent(buildContent());
@@ -313,6 +322,51 @@ public final class GuiBatchRunTab {
updateButtonStates(); updateButtonStates();
} }
/**
* Rückwärtskompatible Variante ohne OS-Lock auf die Konfigurationsdatei.
* <p>
* 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<GuiBatchRunLauncher> launcherSupplier,
Supplier<GuiMiniRunLauncher> miniRunLauncherSupplier,
Supplier<GuiResetDocumentStatusPort> resetPortSupplier,
Supplier<Path> configPathSupplier,
BooleanSupplier savedConfigurationReadyCheck,
Runnable onRunStateChanged,
Supplier<GuiManualFileRenamePort> manualFileRenamePortSupplier,
Supplier<GuiManualFileCopyPort> manualFileCopyPortSupplier,
Supplier<GuiHistoricalDocumentContextPort> historicalDocumentContextPortSupplier,
Supplier<Optional<Path>> sourceFolderSupplier,
Supplier<Optional<String>> 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. * Rückwärtskompatible Variante für Aufrufer ohne Mini-Lauf- oder Rücksetz-Fähigkeiten.
* *
@@ -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.ActiveDatabaseContextPort;
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity; 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.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.BatchRunTriggerResult;
import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary; import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary;
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort; import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
@@ -974,8 +975,10 @@ public class BootstrapRunner {
GuiPromptEditorPort promptEditorPort = buildGuiPromptEditorPort( GuiPromptEditorPort promptEditorPort = buildGuiPromptEditorPort(
loadedState.values().promptTemplateFile()); loadedState.values().promptTemplateFile());
Optional<String> contextError = initializeApplicationRunContext(configPath); Optional<String> contextError = initializeApplicationRunContext(configPath);
Optional<ConfigurationFileLockPort> guiRunLockPort = Optional.empty();
if (contextError.isEmpty()) { if (contextError.isEmpty()) {
tryInitializeScheduler(configPath); tryInitializeScheduler(configPath);
guiRunLockPort = Optional.of(new FileChannelConfigurationAccessAdapter(configPath));
} }
Optional<SchedulerControlUseCase> schedulerUseCase = Optional<SchedulerControlUseCase> schedulerUseCase =
guiSchedulerUseCase.map(s -> (SchedulerControlUseCase) s); guiSchedulerUseCase.map(s -> (SchedulerControlUseCase) s);
@@ -986,7 +989,7 @@ public class BootstrapRunner {
historicalDocumentContextPort, applicationVersion, promptEditorPort, historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort, historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort,
this::buildGuiPromptEditorPort, createNewDatabasePort, contextError, this::buildGuiPromptEditorPort, createNewDatabasePort, contextError,
schedulerUseCase); schedulerUseCase, guiRunLockPort);
} catch (GuiConfigurationLoadException e) { } catch (GuiConfigurationLoadException e) {
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}", LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
e.getMessage(), e); e.getMessage(), e);