diff --git a/src/main/java/emissary/admin/Startup.java b/src/main/java/emissary/admin/Startup.java index 391e161aa2..246406ae19 100755 --- a/src/main/java/emissary/admin/Startup.java +++ b/src/main/java/emissary/admin/Startup.java @@ -5,7 +5,10 @@ import emissary.config.ServiceConfigGuide; import emissary.core.EmissaryException; import emissary.core.Namespace; +import emissary.directory.DirectoryEntry; +import emissary.directory.DirectoryPlace; import emissary.directory.EmissaryNode; +import emissary.directory.IDirectoryPlace; import emissary.directory.KeyManipulator; import emissary.pickup.PickUpPlace; import emissary.place.IServiceProviderPlace; @@ -18,6 +21,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -65,6 +69,10 @@ public class Startup { protected final Map> placeLists = new ConcurrentHashMap<>(); protected final Map> pickupLists = new ConcurrentHashMap<>(); + // sets to keep track of possible invisible place startup + protected Set activeDirPlaces = new LinkedHashSet<>(); + protected Set placeAlreadyStarted = new LinkedHashSet<>(); + /** * n return the full DNS name and port without the protocol part */ @@ -160,6 +168,10 @@ public void start() throws EmissaryException { // they've completed their own set-up). So we have to startup // the pickup places here. startPickUpPlaces(); + + if (!verifyNoInvisiblePlacesStarted()) { + // TODO: If invisible places are started, shutdown the EmissaryServer + } } @@ -367,6 +379,8 @@ protected boolean placeSetup(final int directoryActionArg, final Map> pla List l = placeList.computeIfAbsent(host, k -> new ArrayList<>()); l.add(theLocation); } + + /** + * Verifies the active directory places vs places started up. Log if any places are started without being announced in + * start-up. + * + * @return true if no invisible places started, false if yes + */ + public boolean verifyNoInvisiblePlacesStarted() { + try { + IDirectoryPlace dirPlace = DirectoryPlace.lookup(); + List dirEntries = dirPlace.getEntries(); + for (DirectoryEntry currentDir : dirEntries) { + // add place names of active directories + activeDirPlaces.add(currentDir.getLocalPlace().getPlaceName()); + } + + // remove DirectoryPlace from activeDirPlaces. DirectoryPlace is started up automatically in order to + // start all other places, so it isn't per se "announced", but it is known and logged + activeDirPlaces.removeIf(dir -> dir.equalsIgnoreCase("DirectoryPlace")); + } catch (EmissaryException e) { + throw new RuntimeException(e); + } + + // compares place names in active dirs and active places, removes them from set if found + for (String thePlaceLocation : places.values()) { + activeDirPlaces.removeIf(dir -> dir.equalsIgnoreCase(thePlaceLocation.substring(thePlaceLocation.lastIndexOf("/") + 1))); + } + + // places that are attempted to startup but are already up are added to separate list + // this will only check if places are added to that list + if (!placeAlreadyStarted.isEmpty()) { + for (String thePlaceLocation : placeAlreadyStarted) { + activeDirPlaces.removeIf(dir -> dir.equalsIgnoreCase(thePlaceLocation)); + } + } + + // if any places are left in active dir keys, they are places not announced on startup + if (!activeDirPlaces.isEmpty()) { + logger.warn("{} place(s) started up without being announced! {}", activeDirPlaces.size(), activeDirPlaces); + return false; + } + + return true; + } } diff --git a/src/test/java/emissary/admin/StartupTest.java b/src/test/java/emissary/admin/StartupTest.java index ab84f52512..b8ae96efa4 100644 --- a/src/test/java/emissary/admin/StartupTest.java +++ b/src/test/java/emissary/admin/StartupTest.java @@ -1,5 +1,7 @@ package emissary.admin; +import emissary.core.Namespace; +import emissary.directory.DirectoryPlace; import emissary.directory.EmissaryNode; import emissary.pickup.file.FilePickUpClient; import emissary.pickup.file.FilePickUpPlace; @@ -7,18 +9,22 @@ import emissary.place.sample.DelayPlace; import emissary.place.sample.DevNullPlace; import emissary.test.core.junit5.UnitTest; +import emissary.util.io.ResourceReader; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.spy; class StartupTest extends UnitTest { - @Test void testSortPlaces() throws IOException { List somePlaces = new ArrayList<>(); @@ -43,4 +49,64 @@ void testSortPlaces() throws IOException { places); } + @Test + void testInvisPlaceStart() throws IOException { + // setup node, startup, and DirectoryPlace + EmissaryNode node = new EmissaryNode(); + Startup startup = new Startup(node.getNodeConfigurator(), node); + String location = "http://" + node.getNodeName() + ":" + node.getNodePort(); + dirStartUp(); + + // test if place is already started before startup + startup.activeDirPlaces.add(location + "/PlaceAlreadyStartedTest"); + startup.placeAlreadyStarted.add(location + "/PlaceAlreadyStartedTest"); + assertTrue(startup.verifyNoInvisiblePlacesStarted()); + startup.activeDirPlaces.clear(); + startup.placeAlreadyStarted.clear(); + + // test if place is started up normally and is active dir + startup.places.put(location, DevNullPlace.class.getSimpleName()); + startup.activeDirPlaces.add(DevNullPlace.class.getSimpleName()); + assertTrue(startup.verifyNoInvisiblePlacesStarted()); + startup.activeDirPlaces.clear(); + startup.places.remove(location, DevNullPlace.class.getSimpleName()); + + // test unannounced place with active dir + startup.activeDirPlaces.add(location + "/PlaceStartUnannouncedTest"); + assertFalse(startup.verifyNoInvisiblePlacesStarted()); + startup.activeDirPlaces.clear(); + + // teardown + dirTeardown(); + } + + private DirectoryPlace master = null; + private DirectoryPlace client = null; + + public void dirStartUp() throws IOException { + // master directory + InputStream configStream = new ResourceReader().getConfigDataAsStream(this); + + String masterloc = "http://localhost:8001/TestMasterDirectoryPlace"; + this.master = spy(new DirectoryPlace(configStream, masterloc, new EmissaryNode())); + configStream.close(); + Namespace.bind(masterloc, this.master); + + // non-master directory + configStream = new ResourceReader().getConfigDataAsStream(this); + String clientloc = "http://localhost:8001/DirectoryPlace"; + this.client = spy(new DirectoryPlace(configStream, this.master.getDirectoryEntry().getKey(), clientloc, new EmissaryNode())); + configStream.close(); + Namespace.bind(clientloc, this.client); + } + + public void dirTeardown() { + this.master.shutDown(); + this.master = null; + this.client.shutDown(); + this.client = null; + for (String s : Namespace.keySet()) { + Namespace.unbind(s); + } + } } diff --git a/src/test/resources/emissary/admin/StartupTest.cfg b/src/test/resources/emissary/admin/StartupTest.cfg new file mode 100755 index 0000000000..fd8dbf7708 --- /dev/null +++ b/src/test/resources/emissary/admin/StartupTest.cfg @@ -0,0 +1,7 @@ +PLACE_NAME = DirectoryPlace +SERVICE_NAME = DIRECTORY +SERVICE_TYPE = STUDY +SERVICE_DESCRIPTION = "This place will provide location information to agents" +SERVICE_COST = 50 +SERVICE_QUALITY = 50 +SERVICE_PROXY = "EMISSARY_DIRECTORY_SERVICES" diff --git a/src/test/resources/emissary/admin/emissary/directory/StartupTest.cfg b/src/test/resources/emissary/admin/emissary/directory/StartupTest.cfg new file mode 100644 index 0000000000..2d0602e361 --- /dev/null +++ b/src/test/resources/emissary/admin/emissary/directory/StartupTest.cfg @@ -0,0 +1,10 @@ +PLACE_NAME = DirectoryPlace +SERVICE_NAME = DIRECTORY +SERVICE_TYPE = STUDY +SERVICE_DESCRIPTION = "This place will provide location information to agents" +SERVICE_COST = 50 +SERVICE_QUALITY = 50 +SERVICE_PROXY = "EMISSARY_DIRECTORY_SERVICES" +# This is too fast for normal operations, just for the test! +HEARTBEAT_DELAY_SECONDS = 2 +HEARTBEAT_INTERVAL_SECONDS = 5