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 b7274fa..4e7a51f 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 @@ -97,6 +97,8 @@ import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.Technical import de.gecheckt.pdf.umbenenner.bootstrap.adapter.AiModelCatalogDispatcher; import de.gecheckt.pdf.umbenenner.bootstrap.adapter.GuiConfigurationPropertiesWriter; import de.gecheckt.pdf.umbenenner.bootstrap.adapter.Log4jProcessingLogger; +import de.gecheckt.pdf.umbenenner.bootstrap.singleinstance.AnotherInstanceRunningException; +import de.gecheckt.pdf.umbenenner.bootstrap.singleinstance.SingleInstanceGuard; import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArguments; import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupMode; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; @@ -189,6 +191,7 @@ public class BootstrapRunner { private final UseCaseFactory useCaseFactory; private final CommandFactory commandFactory; private final GuiAdapterFactory guiAdapterFactory; + private final SingleInstanceGuardFactory singleInstanceGuardFactory; /** * Functional interface encapsulating the legacy configuration migration step. @@ -319,6 +322,23 @@ public class BootstrapRunner { GuiAdapter create(); } + /** + * Factory für den prozessweiten Einzelinstanz-Schutz. + *

+ * Die Produktionsimplementierung liefert {@code new SingleInstanceGuard()}. + * In Tests kann eine No-Op-Lambda injiziert werden, damit kein Test den + * Produktionsport belegt und parallele Testläufe einander nicht behindern. + */ + @FunctionalInterface + public interface SingleInstanceGuardFactory { + /** + * Erstellt eine neue {@link SingleInstanceGuard}-Instanz. + * + * @return eine neue {@link SingleInstanceGuard}-Instanz; niemals {@code null} + */ + SingleInstanceGuard create(); + } + /** * Creates the BootstrapRunner with default factories for production use. *

@@ -352,6 +372,7 @@ public class BootstrapRunner { de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken.neverCancelled()); this.commandFactory = SchedulerBatchCommand::new; this.guiAdapterFactory = GuiAdapter::new; + this.singleInstanceGuardFactory = SingleInstanceGuard::new; } /** @@ -450,7 +471,8 @@ public class BootstrapRunner { this(path -> { /* no-op: tests inject mock ConfigurationPort directly */ }, configPortFactory, runLockPortFactory, validatorFactory, schemaInitPortFactory, useCaseFactory, commandFactory, - GuiAdapter::new); + GuiAdapter::new, + noOpSingleInstanceGuardFactory()); } /** @@ -475,7 +497,8 @@ public class BootstrapRunner { UseCaseFactory useCaseFactory, CommandFactory commandFactory) { this(migrationStep, configPortFactory, runLockPortFactory, validatorFactory, - schemaInitPortFactory, useCaseFactory, commandFactory, GuiAdapter::new); + schemaInitPortFactory, useCaseFactory, commandFactory, GuiAdapter::new, + noOpSingleInstanceGuardFactory()); } /** @@ -502,6 +525,37 @@ public class BootstrapRunner { UseCaseFactory useCaseFactory, CommandFactory commandFactory, GuiAdapterFactory guiAdapterFactory) { + this(migrationStep, configPortFactory, runLockPortFactory, validatorFactory, + schemaInitPortFactory, useCaseFactory, commandFactory, guiAdapterFactory, + noOpSingleInstanceGuardFactory()); + } + + /** + * Erstellt den BootstrapRunner mit vollständiger Kontrolle über alle Factories, + * einschließlich der Einzelinstanz-Guard-Factory. + *

+ * Dieser Konstruktor ist für Tests gedacht, die den Guard-Pfad oder das + * Zusammenspiel aller Factories prüfen müssen. + * + * @param migrationStep der Migrationsschritt vor dem Laden der Konfiguration + * @param configPortFactory Factory für den ConfigurationPort + * @param runLockPortFactory Factory für den RunLockPort + * @param validatorFactory Factory für den StartConfigurationValidator + * @param schemaInitPortFactory Factory für den PersistenceSchemaInitializationPort + * @param useCaseFactory Factory für den BatchRunProcessingUseCase + * @param commandFactory Factory für den SchedulerBatchCommand + * @param guiAdapterFactory Factory für den GuiAdapter + * @param singleInstanceGuardFactory Factory für den SingleInstanceGuard + */ + public BootstrapRunner(MigrationStep migrationStep, + ConfigurationPortFactory configPortFactory, + RunLockPortFactory runLockPortFactory, + ValidatorFactory validatorFactory, + SchemaInitializationPortFactory schemaInitPortFactory, + UseCaseFactory useCaseFactory, + CommandFactory commandFactory, + GuiAdapterFactory guiAdapterFactory, + SingleInstanceGuardFactory singleInstanceGuardFactory) { this.migrationStep = migrationStep; this.configPortFactory = configPortFactory; this.runLockPortFactory = runLockPortFactory; @@ -510,11 +564,39 @@ public class BootstrapRunner { this.useCaseFactory = useCaseFactory; this.commandFactory = commandFactory; this.guiAdapterFactory = guiAdapterFactory; + this.singleInstanceGuardFactory = singleInstanceGuardFactory; + } + + /** + * Liefert eine No-Op-{@link SingleInstanceGuardFactory}, die einen Guard erzeugt, + * der beim Aufruf von {@code acquire()} nichts tut. + *

+ * Wird in Test-Konstruktoren verwendet, damit parallele Testläufe keine Portkonflikte + * auf dem Produktionsport erzeugen. + * + * @return eine No-Op-Guard-Factory; niemals {@code null} + */ + private static SingleInstanceGuardFactory noOpSingleInstanceGuardFactory() { + return () -> new SingleInstanceGuard() { + @Override + public void acquire() { /* kein Port belegen in Tests */ } + @Override + public void close() { /* keine Aktion */ } + }; } /** * Runs the complete application startup sequence for the given startup arguments. *

+ * Vor dem Start beider Modus-Pfade wird die prozessweite Einzelinstanz-Sicherung + * aktiviert. Ist bereits eine andere Instanz der Anwendung aktiv, wird der Start + * sofort abgebrochen: + *

+ *

* Dispatches to the GUI or headless batch adapter based on the startup mode carried * in {@code startupArguments}: *