Fix #35: Einzelinstanz-Schutz ueber Loopback-ServerSocket
Eine zweite parallele Instanz wird beim Start abgewiesen. Der Schutz greift fuer GUI- und Headless-Pfad gleichermassen vor der Modusweiche in BootstrapRunner. Umsetzung als ServerSocket-Bind auf 127.0.0.1:47832: stale-lock-frei, da das Betriebssystem den Port beim Prozessende automatisch freigibt, robust unter Windows mit gemappten Laufwerken und UNC-Pfaden, und ohne Konflikt mit dem bestehenden RunLockPort, der nur den Batch-Lauf schuetzt. Bei kollidierender Bindung erscheint im GUI-Modus ein Swing-Dialog (JavaFX ist hier noch nicht initialisiert) und im Headless-Modus eine Logmeldung; beide Pfade enden mit Exit-Code 1. Ein ShutdownHook und try-with-resources geben den Port deterministisch wieder frei. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+78
@@ -0,0 +1,78 @@
|
||||
package de.gecheckt.pdf.umbenenner.bootstrap.singleinstance;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.net.ServerSocket;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit-Tests für {@link SingleInstanceGuard}.
|
||||
* <p>
|
||||
* Alle Tests verwenden einen dynamisch gebundenen freien Port, damit parallele
|
||||
* Testläufe keine Konflikte auf dem Produktions-Standardport erzeugen.
|
||||
*/
|
||||
class SingleInstanceGuardTest {
|
||||
|
||||
/**
|
||||
* Ermittelt einen freien lokalen Port.
|
||||
*
|
||||
* @return ein freier TCP-Port
|
||||
* @throws Exception wenn kein freier Port ermittelt werden kann
|
||||
*/
|
||||
private static int freienPortErmitteln() throws Exception {
|
||||
try (ServerSocket s = new ServerSocket(0)) {
|
||||
return s.getLocalPort();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void ersterAcquireErfolgtOhneFehler() throws Exception {
|
||||
int port = freienPortErmitteln();
|
||||
try (SingleInstanceGuard guard = new SingleInstanceGuard(port)) {
|
||||
assertDoesNotThrow(guard::acquire,
|
||||
"Der erste acquire() soll den Port erfolgreich belegen.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void zweiterAcquireWirftAnotherInstanceRunningException() throws Exception {
|
||||
int port = freienPortErmitteln();
|
||||
try (SingleInstanceGuard ersterGuard = new SingleInstanceGuard(port)) {
|
||||
ersterGuard.acquire();
|
||||
|
||||
SingleInstanceGuard zweiterGuard = new SingleInstanceGuard(port);
|
||||
assertThrows(AnotherInstanceRunningException.class, zweiterGuard::acquire,
|
||||
"Während der erste Guard aktiv ist, soll der zweite acquire() eine "
|
||||
+ "AnotherInstanceRunningException werfen.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeErlaubtNeuesBinden() throws Exception {
|
||||
int port = freienPortErmitteln();
|
||||
SingleInstanceGuard ersterGuard = new SingleInstanceGuard(port);
|
||||
ersterGuard.acquire();
|
||||
ersterGuard.close();
|
||||
|
||||
// Nach close() soll ein neuer Guard den Port wieder belegen können
|
||||
try (SingleInstanceGuard neuerGuard = new SingleInstanceGuard(port)) {
|
||||
assertDoesNotThrow(neuerGuard::acquire,
|
||||
"Nach close() des ersten Guards soll ein neuer Guard erfolgreich binden können.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeIstIdempotent() throws Exception {
|
||||
int port = freienPortErmitteln();
|
||||
SingleInstanceGuard guard = new SingleInstanceGuard(port);
|
||||
guard.acquire();
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
guard.close();
|
||||
guard.close();
|
||||
guard.close();
|
||||
}, "Mehrfaches close() soll keine Ausnahme werfen.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user