All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.drasyl.localhost.LocalHostDiscovery Maven / Gradle / Ivy

Go to download

This packages contains the building blocks required to create the drasyl overlay network.

There is a newer version: 0.10.0
Show newest version
/*
 * Copyright (c) 2020-2021 Heiko Bornholdt and Kevin Röbert
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
 * OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.drasyl.localhost;

import com.fasterxml.jackson.core.type.TypeReference;
import io.reactivex.rxjava3.disposables.Disposable;
import org.drasyl.event.Event;
import org.drasyl.event.NodeDownEvent;
import org.drasyl.event.NodeUnrecoverableErrorEvent;
import org.drasyl.event.NodeUpEvent;
import org.drasyl.identity.IdentityPublicKey;
import org.drasyl.pipeline.HandlerContext;
import org.drasyl.pipeline.address.InetSocketAddressWrapper;
import org.drasyl.pipeline.skeleton.SimpleOutboundHandler;
import org.drasyl.remote.protocol.Protocol.Application;
import org.drasyl.remote.protocol.RemoteEnvelope;
import org.drasyl.util.FutureCombiner;
import org.drasyl.util.SetUtil;
import org.drasyl.util.ThrowingBiConsumer;
import org.drasyl.util.logging.Logger;
import org.drasyl.util.logging.LoggerFactory;
import org.drasyl.util.network.NetworkUtil;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.WatchService;
import java.time.Duration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.time.Duration.ofSeconds;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.drasyl.util.JSONUtil.JACKSON_READER;
import static org.drasyl.util.JSONUtil.JACKSON_WRITER;
import static org.drasyl.util.RandomUtil.randomLong;

/**
 * Uses the file system to discover other drasyl nodes running on the local computer.
 * 

* To do this, all nodes regularly write their {@link org.drasyl.remote.handler.UdpServer} * address(es) to the file system. At the same time the file system is monitored to detect other * nodes. If the file system does not support monitoring ({@link WatchService}), a fallback to * polling is used. *

* Inspired by: Jadex */ @SuppressWarnings("java:S1192") public class LocalHostDiscovery extends SimpleOutboundHandler, IdentityPublicKey> { private static final Logger LOG = LoggerFactory.getLogger(LocalHostDiscovery.class); private static final Object path = LocalHostDiscovery.class; public static final Duration REFRESH_INTERVAL_SAFETY_MARGIN = ofSeconds(5); public static final Duration WATCH_SERVICE_POLL_INTERVAL = ofSeconds(5); public static final String FILE_SUFFIX = ".json"; private final ThrowingBiConsumer jacksonWriter; private final Map routes; private Disposable watchDisposable; private Disposable postDisposable; private WatchService watchService; // NOSONAR public LocalHostDiscovery() { this( JACKSON_WRITER::writeValue, new HashMap<>(), null, null ); } @SuppressWarnings({ "java:S107" }) LocalHostDiscovery(final ThrowingBiConsumer jacksonWriter, final Map routes, final Disposable watchDisposable, final Disposable postDisposable) { this.jacksonWriter = requireNonNull(jacksonWriter); this.routes = requireNonNull(routes); this.watchDisposable = watchDisposable; this.postDisposable = postDisposable; } @Override public void onEvent(final HandlerContext ctx, final Event event, final CompletableFuture future) { final FutureCombiner combiner = FutureCombiner.getInstance(); if (event instanceof NodeUpEvent) { combiner.add(startDiscovery(ctx, ((NodeUpEvent) event).getNode().getPort())); } else if (event instanceof NodeUnrecoverableErrorEvent || event instanceof NodeDownEvent) { combiner.add(stopDiscovery(ctx)); } // passthrough event combiner.add(ctx.passEvent(event, new CompletableFuture<>())) .combine(future); } @Override protected void matchedOutbound(final HandlerContext ctx, final IdentityPublicKey recipient, final RemoteEnvelope envelope, final CompletableFuture future) { final InetSocketAddressWrapper localAddress = routes.get(recipient); if (localAddress != null) { LOG.trace("Send message `{}` via local route {}.", () -> envelope, () -> localAddress); ctx.passOutbound(localAddress, envelope, future); } else { // passthrough message ctx.passOutbound(recipient, envelope, future); } } private synchronized CompletableFuture startDiscovery(final HandlerContext ctx, final int port) { LOG.debug("Start Local Host Discovery..."); final Path discoveryPath = discoveryPath(ctx); final File directory = discoveryPath.toFile(); if (!directory.mkdirs() && !directory.exists()) { LOG.warn("Discovery directory `{}` could not be created.", discoveryPath::toAbsolutePath); } else if (!(directory.isDirectory() && directory.canRead() && directory.canWrite())) { LOG.warn("Discovery directory `{}` not accessible.", discoveryPath::toAbsolutePath); } else { if (ctx.config().isRemoteLocalHostDiscoveryWatchEnabled()) { tryWatchDirectory(ctx, discoveryPath); } ctx.dependentScheduler().scheduleDirect(() -> scan(ctx)); keepOwnInformationUpToDate(ctx, discoveryPath.resolve(ctx.identity().getIdentityPublicKey().toString() + ".json"), port); } LOG.debug("Local Host Discovery started."); return completedFuture(null); } private synchronized CompletableFuture stopDiscovery(final HandlerContext ctx) { LOG.debug("Stop Local Host Discovery..."); if (watchDisposable != null) { watchDisposable.dispose(); } if (postDisposable != null) { postDisposable.dispose(); } final Path filePath = discoveryPath(ctx).resolve(ctx.identity().getIdentityPublicKey().toString() + ".json"); if (filePath.toFile().exists()) { try { Files.delete(filePath); } catch (final IOException e) { LOG.debug("Unable to delete `{}`", filePath, e); } } routes.keySet().forEach(publicKey -> ctx.peersManager().removePath(publicKey, LocalHostDiscovery.path)); routes.clear(); LOG.debug("Local Host Discovery stopped."); return completedFuture(null); } /** * Tries to monitor {@code discoveryPath} so that any changes are automatically reported. If * this is not possible, we have to fall back to periodical polling. */ private void tryWatchDirectory(final HandlerContext ctx, final Path discoveryPath) { try { final File directory = discoveryPath.toFile(); final FileSystem fileSystem = discoveryPath.getFileSystem(); watchService = fileSystem.newWatchService(); discoveryPath.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); LOG.debug("Watch service for directory `{}` registered", directory); final long pollInterval = WATCH_SERVICE_POLL_INTERVAL.toMillis(); watchDisposable = ctx.dependentScheduler().schedulePeriodicallyDirect(() -> { if (watchService.poll() != null) { // directory has been changed scan(ctx); } }, randomLong(pollInterval), pollInterval, MILLISECONDS); } catch (final IOException e) { LOG.debug("Unable to register watch service. Use polling as fallback: ", e); // use polling as fallback watchService = null; } } /** * Writes periodically the actual own information to {@link #discoveryPath}. */ private void keepOwnInformationUpToDate(final HandlerContext ctx, final Path filePath, final int port) { // get own address(es) final Set addresses; if (ctx.config().getRemoteBindHost().isAnyLocalAddress()) { // use all available addresses addresses = NetworkUtil.getAddresses(); } else { // use given host addresses = Set.of(ctx.config().getRemoteBindHost()); } final Set socketAddresses = addresses.stream().map(a -> new InetSocketAddress(a, port)).collect(Collectors.toSet()); final Duration refreshInterval; if (ctx.config().getRemoteLocalHostDiscoveryLeaseTime().compareTo(REFRESH_INTERVAL_SAFETY_MARGIN) > 0) { refreshInterval = ctx.config().getRemoteLocalHostDiscoveryLeaseTime().minus(REFRESH_INTERVAL_SAFETY_MARGIN); } else { refreshInterval = ofSeconds(1); } postDisposable = ctx.dependentScheduler().schedulePeriodicallyDirect(() -> { // only scan in polling mode when watchService does not work if (watchService == null) { scan(ctx); } postInformation(filePath, socketAddresses); }, randomLong(refreshInterval.toMillis()), refreshInterval.toMillis(), MILLISECONDS); } /** * Scans in {@link #discoveryPath} for ports of other drasyl nodes. * * @param ctx handler's context */ @SuppressWarnings("java:S134") synchronized void scan(final HandlerContext ctx) { final Path discoveryPath = discoveryPath(ctx); LOG.debug("Scan directory {} for new peers.", discoveryPath); final String ownPublicKeyString = ctx.identity().getIdentityPublicKey().toString(); final long maxAge = System.currentTimeMillis() - ctx.config().getRemoteLocalHostDiscoveryLeaseTime().toMillis(); final File[] files = discoveryPath.toFile().listFiles(); if (files != null) { final Map newRoutes = new HashMap<>(); for (final File file : files) { try { final String fileName = file.getName(); if (file.lastModified() >= maxAge && fileName.length() == IdentityPublicKey.KEY_LENGTH_AS_STRING + FILE_SUFFIX.length() && fileName.endsWith(FILE_SUFFIX) && !fileName.startsWith(ownPublicKeyString)) { final IdentityPublicKey publicKey = IdentityPublicKey.of(fileName.replace(".json", "")); final TypeReference> typeRef = new TypeReference<>() { }; final Set addresses = JACKSON_READER.forType(typeRef).readValue(file); if (!addresses.isEmpty()) { LOG.trace("Addresses `{}` for peer `{}` discovered by file `{}`", addresses, publicKey, fileName); final InetSocketAddress firstAddress = SetUtil.firstElement(addresses); newRoutes.put(publicKey, firstAddress); } } } catch (final IllegalArgumentException | IOException e) { LOG.warn("Unable to read peer information from `{}`: ", file.getAbsolutePath(), e); } } updateRoutes(ctx, newRoutes); } } private void updateRoutes(final HandlerContext ctx, final Map newRoutes) { // remove outdated routes for (final Iterator i = routes.keySet().iterator(); i.hasNext(); ) { final IdentityPublicKey publicKey = i.next(); if (!newRoutes.containsKey(publicKey)) { LOG.trace("Addresses for peer `{}` are outdated. Remove peer from routing table.", publicKey); ctx.peersManager().removePath(publicKey, path); i.remove(); } } // add new routes newRoutes.forEach(((publicKey, address) -> { if (!routes.containsKey(publicKey)) { routes.put(publicKey, new InetSocketAddressWrapper(address)); ctx.peersManager().addPath(publicKey, path); } })); } /** * Posts own port to {@code filePath}. */ @SuppressWarnings("java:S2308") private void postInformation(final Path filePath, final Set addresses) { LOG.trace("Post own addresses `{}` to file `{}`", addresses, filePath); final File file = filePath.toFile(); try { if (!file.setLastModified(System.currentTimeMillis())) { jacksonWriter.accept(file, addresses); file.deleteOnExit(); } } catch (final IOException e) { LOG.warn("Unable to write peer information to `{}`: {}", filePath::toAbsolutePath, e::getMessage); } } private static Path discoveryPath(final HandlerContext ctx) { return ctx.config().getRemoteLocalHostDiscoveryPath().resolve(String.valueOf(ctx.config().getNetworkId())); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy