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:
+2
-1
@@ -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(),
|
||||||
|
|||||||
+83
-2
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+125
-10
@@ -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,8 +236,8 @@ 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.
|
||||||
*
|
*
|
||||||
@@ -221,6 +252,47 @@ public final class GuiBatchRunCoordinator {
|
|||||||
* @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,
|
||||||
|
GuiMiniRunLauncher miniRunLauncher,
|
||||||
|
GuiResetDocumentStatusPort resetPort,
|
||||||
|
Function<Runnable, Thread> threadFactory,
|
||||||
|
Consumer<Runnable> fxDispatcher,
|
||||||
|
Listener listener,
|
||||||
|
GuiHistoricalDocumentContextPort historicalDocumentContextPort,
|
||||||
|
Optional<ConfigurationFileLockPort> 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");
|
||||||
|
this.threadFactory = Objects.requireNonNull(threadFactory, "threadFactory must not be null");
|
||||||
|
this.fxDispatcher = Objects.requireNonNull(fxDispatcher, "fxDispatcher must not be null");
|
||||||
|
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.
|
||||||
|
* <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,
|
public GuiBatchRunCoordinator(GuiBatchRunLauncher launcher,
|
||||||
GuiMiniRunLauncher miniRunLauncher,
|
GuiMiniRunLauncher miniRunLauncher,
|
||||||
@@ -229,14 +301,8 @@ public final class GuiBatchRunCoordinator {
|
|||||||
Consumer<Runnable> fxDispatcher,
|
Consumer<Runnable> fxDispatcher,
|
||||||
Listener listener,
|
Listener listener,
|
||||||
GuiHistoricalDocumentContextPort historicalDocumentContextPort) {
|
GuiHistoricalDocumentContextPort historicalDocumentContextPort) {
|
||||||
this.launcher = Objects.requireNonNull(launcher, "launcher must not be null");
|
this(launcher, miniRunLauncher, resetPort, threadFactory, fxDispatcher, listener,
|
||||||
this.miniRunLauncher = Objects.requireNonNull(miniRunLauncher, "miniRunLauncher must not be null");
|
historicalDocumentContextPort, Optional.empty());
|
||||||
this.resetPort = Objects.requireNonNull(resetPort, "resetPort must not be null");
|
|
||||||
this.threadFactory = Objects.requireNonNull(threadFactory, "threadFactory must not be null");
|
|
||||||
this.fxDispatcher = Objects.requireNonNull(fxDispatcher, "fxDispatcher must not be null");
|
|
||||||
this.listener = Objects.requireNonNull(listener, "listener must not be null");
|
|
||||||
this.historicalDocumentContextPort = Objects.requireNonNull(
|
|
||||||
historicalDocumentContextPort, "historicalDocumentContextPort must not be null");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -437,6 +503,21 @@ 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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
|
BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
|
||||||
BatchRunCancellationToken token = cancellationRequested::get;
|
BatchRunCancellationToken token = cancellationRequested::get;
|
||||||
GuiBatchRunLaunchOutcome outcome;
|
GuiBatchRunLaunchOutcome outcome;
|
||||||
@@ -454,12 +535,30 @@ public final class GuiBatchRunCoordinator {
|
|||||||
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
|
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
|
||||||
}
|
}
|
||||||
finishRun(outcome);
|
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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
|
BatchRunProgressObserver observer = buildDispatchingObserver(configFilePath);
|
||||||
BatchRunCancellationToken token = cancellationRequested::get;
|
BatchRunCancellationToken token = cancellationRequested::get;
|
||||||
GuiBatchRunLaunchOutcome outcome;
|
GuiBatchRunLaunchOutcome outcome;
|
||||||
@@ -477,6 +576,9 @@ public final class GuiBatchRunCoordinator {
|
|||||||
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
|
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
|
||||||
}
|
}
|
||||||
finishRun(outcome);
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-3
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
+4
-1
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user