package jmri.util.zeroconf; import java.io.IOException; import java.net.Inet4Address; import java.net.Inet6Address; import java.util.HashMap; import java.util.concurrent.CountDownLatch; import javax.jmdns.JmDNS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A MockZeroConfServiceManager object manages zeroConf network service * advertisements for testing purposes without actually advertising a service on * the network. *
* ZeroConfService objects encapsulate zeroConf network services created using * JmDNS, providing methods to start and stop service advertisements and to * query service state. Typical usage would be: *
* ZeroConfService myService = ZeroConfService.create("_withrottle._tcp.local.", port);
* myService.publish();
* or, if you do not wish to retain the ZeroConfService object:
*
* ZeroConfService.create("_http._tcp.local.", port).publish();
* ZeroConfService objects can also be created with a HashMap of
* properties that are included in the TXT record for the service advertisement.
* This HashMap should remain small, but it could include information such as
* the default path (for a web server), a specific protocol version, or other
* information. Note that all service advertisements include the JMRI version,
* using the key "version", and the JMRI version numbers in a string
* "major.minor.test" with the key "jmri"
* * All ZeroConfServices are published with the computer's hostname as the mDNS * hostname (unless it cannot be determined by JMRI), as well as the JMRI node * name in the TXT record with the key "node". *
* All ZeroConfServices are automatically stopped when the JMRI application * shuts down. Use {@link #allServices() } to get a collection of all published * ZeroConfService objects. *
* JMRI is free software; you can redistribute it and/or modify it under the * terms of version 2 of the GNU General Public License as published by the Free * Software Foundation. See the "COPYING" file for a copy of this license. *
* JMRI is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU General Public License for more details. *
* * @author Randall Wood Copyright (C) 2011, 2013 * @see javax.jmdns.JmDNS * @see javax.jmdns.ServiceInfo */ public class MockZeroConfServiceManager extends ZeroConfServiceManager { private static final Logger log = LoggerFactory.getLogger(MockZeroConfServiceManager.class); /** * Start advertising the service. This goes through all actions of * advertising the service except sending the advertisement over the * network. This allows listeners to get notified of the advertisement and * {@link #isPublished(jmri.util.zeroconf.ZeroConfService) } to function * correctly. * * @param service The service to publish */ @Override public void publish(ZeroConfService service) { if (!isPublished(service)) { //get current preference values services.put(service.getKey(), service); service.getListeners().stream().forEach((listener) -> { listener.serviceQueued(new ZeroConfServiceEvent(service, null)); }); for (JmDNS netService : getDNSes().values()) { ZeroConfServiceEvent event; try { if (netService.getInetAddress() instanceof Inet6Address && !preferences.isUseIPv6()) { // Skip if address is IPv6 and should not be advertised on log.debug("Ignoring IPv6 address {}", netService.getInetAddress().getHostAddress()); continue; } if (netService.getInetAddress() instanceof Inet4Address && !preferences.isUseIPv4()) { // Skip if address is IPv4 and should not be advertised on log.debug("Ignoring IPv4 address {}", netService.getInetAddress().getHostAddress()); continue; } try { log.debug("Publishing ZeroConfService for '{}' on {}", service.getKey(), netService.getInetAddress().getHostAddress()); } catch (IOException ex) { log.debug("Publishing ZeroConfService for '{}' with IOException ", service.getKey(), ex); } // JmDNS requires a 1-to-1 mapping of getServiceInfo to InetAddress if (!service.containsServiceInfo(netService.getInetAddress())) { try { log.debug("Mock service '{}' registration on {} successful.", service.getKey(), netService.getInetAddress().getHostAddress()); } catch (IllegalStateException ex) { // thrown if the reference getServiceInfo object is in use try { log.debug("Initial attempt to register '{}' on {} failed.", service.getKey(), netService.getInetAddress().getHostAddress()); service.addServiceInfo(netService.getInetAddress()); log.debug("Retrying register '{}' on {}.", service.getKey(), netService.getInetAddress().getHostAddress()); } catch (IllegalStateException ex1) { // thrown if service gets registered on interface by // the networkListener before this loop on interfaces // completes, so we only ensure a later notification // is not posted continuing to next interface in list log.debug("'{}' is already registered on {}.", service.getKey(), netService.getInetAddress().getHostAddress()); continue; } } } else { log.debug("skipping '{}' on {}, already in serviceInfos.", service.getKey(), netService.getInetAddress().getHostAddress()); } event = new ZeroConfServiceEvent(service, netService); } catch (IOException ex) { log.error("Unable to publish service for '{}': {}", service.getKey(), ex.getMessage()); continue; } service.getListeners().stream().forEach((listener) -> { listener.servicePublished(event); }); } } } /** * Stop advertising the service. * * @param service The service to stop advertising */ @Override public void stop(ZeroConfService service) { log.debug("Stopping ZeroConfService {}", service.getKey()); if (services.containsKey(service.getKey())) { getDNSes().values().stream().forEach((netService) -> { unregister(service, netService); }); services.remove(service.getKey()); } } @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = "DCN_NULLPOINTER_EXCEPTION", justification = "some objects may already be unregistered") private void unregister(ZeroConfService service, javax.jmdns.JmDNS netService) { try { try { log.debug("Unregistering {} from {}", service.getKey(), netService.getInetAddress()); netService.unregisterService(service.getServiceInfo(netService.getInetAddress())); service.removeServiceInfo(netService.getInetAddress()); service.getListeners().stream().forEach((listener) -> { listener.serviceUnpublished(new ZeroConfServiceEvent(service, netService)); }); } catch (NullPointerException ex) { log.debug("{} already unregistered from {}", service.getKey(), netService.getInetAddress()); } } catch (IOException ex) { log.error("Unable to stop ZeroConfService {}. {}", service.getKey(), ex.getLocalizedMessage()); } } /** * Stop advertising all services. */ @Override public void stopAll() { stopAll(false); } private void stopAll(final boolean close) { log.debug("Stopping all ZeroConfServices"); CountDownLatch zcLatch = new CountDownLatch(services.size()); new HashMap<>(services).values().parallelStream().forEach(service -> { stop(service); zcLatch.countDown(); }); try { zcLatch.await(); } catch (InterruptedException ex) { log.warn("ZeroConfService stop threads interrupted.", ex); } CountDownLatch nsLatch = new CountDownLatch(getDNSes().size()); new HashMap<>(getDNSes()).values().parallelStream().forEach((netService) -> { new Thread(() -> { netService.unregisterAllServices(); if (close) { try { netService.close(); } catch (IOException ex) { log.debug("jmdns.close() returned IOException: {}", ex.getMessage()); } } nsLatch.countDown(); }).start(); }); try { zcLatch.await(); } catch (InterruptedException ex) { log.warn("JmDNS unregister threads interrupted.", ex); } services.clear(); } }