diff options
Diffstat (limited to 'src/javax/jmdns/impl/JmDNSImpl.java')
-rw-r--r-- | src/javax/jmdns/impl/JmDNSImpl.java | 2223 |
1 files changed, 2223 insertions, 0 deletions
diff --git a/src/javax/jmdns/impl/JmDNSImpl.java b/src/javax/jmdns/impl/JmDNSImpl.java new file mode 100644 index 0000000..3e64130 --- /dev/null +++ b/src/javax/jmdns/impl/JmDNSImpl.java @@ -0,0 +1,2223 @@ +// /Copyright 2003-2005 Arthur van Hoff, Rick Blair +// Licensed under Apache License version 2.0 +// Original license LGPL + +package javax.jmdns.impl; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.net.SocketException; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.jmdns.JmDNS; +import javax.jmdns.ServiceEvent; +import javax.jmdns.ServiceInfo; +import javax.jmdns.ServiceInfo.Fields; +import javax.jmdns.ServiceListener; +import javax.jmdns.ServiceTypeListener; +import javax.jmdns.impl.ListenerStatus.ServiceListenerStatus; +import javax.jmdns.impl.ListenerStatus.ServiceTypeListenerStatus; +import javax.jmdns.impl.constants.DNSConstants; +import javax.jmdns.impl.constants.DNSRecordClass; +import javax.jmdns.impl.constants.DNSRecordType; +import javax.jmdns.impl.constants.DNSState; +import javax.jmdns.impl.tasks.DNSTask; + +// REMIND: multiple IP addresses + +/** + * mDNS implementation in Java. + * + * @author Arthur van Hoff, Rick Blair, Jeff Sonstein, Werner Randelshofer, Pierre Frisch, Scott Lewis + */ +public class JmDNSImpl extends JmDNS implements DNSStatefulObject, DNSTaskStarter { + private static Logger logger = Logger.getLogger(JmDNSImpl.class.getName()); + + public enum Operation { + Remove, Update, Add, RegisterServiceType, Noop + } + + /** + * This is the multicast group, we are listening to for multicast DNS messages. + */ + private volatile InetAddress _group; + /** + * This is our multicast socket. + */ + private volatile MulticastSocket _socket; + + /** + * Holds instances of JmDNS.DNSListener. Must by a synchronized collection, because it is updated from concurrent threads. + */ + private final List<DNSListener> _listeners; + + /** + * Holds instances of ServiceListener's. Keys are Strings holding a fully qualified service type. Values are LinkedList's of ServiceListener's. + */ + private final ConcurrentMap<String, List<ServiceListenerStatus>> _serviceListeners; + + /** + * Holds instances of ServiceTypeListener's. + */ + private final Set<ServiceTypeListenerStatus> _typeListeners; + + /** + * Cache for DNSEntry's. + */ + private final DNSCache _cache; + + /** + * This hashtable holds the services that have been registered. Keys are instances of String which hold an all lower-case version of the fully qualified service name. Values are instances of ServiceInfo. + */ + private final ConcurrentMap<String, ServiceInfo> _services; + + /** + * This hashtable holds the service types that have been registered or that have been received in an incoming datagram.<br/> + * Keys are instances of String which hold an all lower-case version of the fully qualified service type.<br/> + * Values hold the fully qualified service type. + */ + private final ConcurrentMap<String, ServiceTypeEntry> _serviceTypes; + + private volatile Delegate _delegate; + + /** + * This is used to store type entries. The type is stored as a call variable and the map support the subtypes. + * <p> + * The key is the lowercase version as the value is the case preserved version. + * </p> + */ + public static class ServiceTypeEntry extends AbstractMap<String, String> implements Cloneable { + + private final Set<Map.Entry<String, String>> _entrySet; + + private final String _type; + + private static class SubTypeEntry implements Entry<String, String>, java.io.Serializable, Cloneable { + + private static final long serialVersionUID = 9188503522395855322L; + + private final String _key; + private final String _value; + + public SubTypeEntry(String subtype) { + super(); + _value = (subtype != null ? subtype : ""); + _key = _value.toLowerCase(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getKey() { + return _key; + } + + /** + * {@inheritDoc} + */ + @Override + public String getValue() { + return _value; + } + + /** + * Replaces the value corresponding to this entry with the specified value (optional operation). This implementation simply throws <tt>UnsupportedOperationException</tt>, as this class implements an <i>immutable</i> map entry. + * + * @param value + * new value to be stored in this entry + * @return (Does not return) + * @exception UnsupportedOperationException + * always + */ + @Override + public String setValue(String value) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object entry) { + if (!(entry instanceof Map.Entry)) { + return false; + } + return this.getKey().equals(((Map.Entry<?, ?>) entry).getKey()) && this.getValue().equals(((Map.Entry<?, ?>) entry).getValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return (_key == null ? 0 : _key.hashCode()) ^ (_value == null ? 0 : _value.hashCode()); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#clone() + */ + @Override + public SubTypeEntry clone() { + // Immutable object + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return _key + "=" + _value; + } + + } + + public ServiceTypeEntry(String type) { + super(); + this._type = type; + this._entrySet = new HashSet<Map.Entry<String, String>>(); + } + + /** + * The type associated with this entry. + * + * @return the type + */ + public String getType() { + return _type; + } + + /* + * (non-Javadoc) + * @see java.util.AbstractMap#entrySet() + */ + @Override + public Set<Map.Entry<String, String>> entrySet() { + return _entrySet; + } + + /** + * Returns <code>true</code> if this set contains the specified element. More formally, returns <code>true</code> if and only if this set contains an element <code>e</code> such that + * <code>(o==null ? e==null : o.equals(e))</code>. + * + * @param subtype + * element whose presence in this set is to be tested + * @return <code>true</code> if this set contains the specified element + */ + public boolean contains(String subtype) { + return subtype != null && this.containsKey(subtype.toLowerCase()); + } + + /** + * Adds the specified element to this set if it is not already present. More formally, adds the specified element <code>e</code> to this set if this set contains no element <code>e2</code> such that + * <code>(e==null ? e2==null : e.equals(e2))</code>. If this set already contains the element, the call leaves the set unchanged and returns <code>false</code>. + * + * @param subtype + * element to be added to this set + * @return <code>true</code> if this set did not already contain the specified element + */ + public boolean add(String subtype) { + if (subtype == null || this.contains(subtype)) { + return false; + } + _entrySet.add(new SubTypeEntry(subtype)); + return true; + } + + /** + * Returns an iterator over the elements in this set. The elements are returned in no particular order (unless this set is an instance of some class that provides a guarantee). + * + * @return an iterator over the elements in this set + */ + public Iterator<String> iterator() { + return this.keySet().iterator(); + } + + /* + * (non-Javadoc) + * @see java.util.AbstractMap#clone() + */ + @Override + public ServiceTypeEntry clone() { + ServiceTypeEntry entry = new ServiceTypeEntry(this.getType()); + for (Map.Entry<String, String> subTypeEntry : this.entrySet()) { + entry.add(subTypeEntry.getValue()); + } + return entry; + } + + /* + * (non-Javadoc) + * @see java.util.AbstractMap#toString() + */ + @Override + public String toString() { + final StringBuilder aLog = new StringBuilder(200); + if (this.isEmpty()) { + aLog.append("empty"); + } else { + for (String value : this.values()) { + aLog.append(value); + aLog.append(", "); + } + aLog.setLength(aLog.length() - 2); + } + return aLog.toString(); + } + + } + + /** + * This is the shutdown hook, we registered with the java runtime. + */ + protected Thread _shutdown; + + /** + * Handle on the local host + */ + private HostInfo _localHost; + + private Thread _incomingListener; + + /** + * Throttle count. This is used to count the overall number of probes sent by JmDNS. When the last throttle increment happened . + */ + private int _throttle; + + /** + * Last throttle increment. + */ + private long _lastThrottleIncrement; + + private final ExecutorService _executor = Executors.newSingleThreadExecutor(); + + // + // 2009-09-16 ldeck: adding docbug patch with slight ammendments + // 'Fixes two deadlock conditions involving JmDNS.close() - ID: 1473279' + // + // --------------------------------------------------- + /** + * The timer that triggers our announcements. We can't use the main timer object, because that could cause a deadlock where Prober waits on JmDNS.this lock held by close(), close() waits for us to finish, and we wait for Prober to give us back + * the timer thread so we can announce. (Patch from docbug in 2006-04-19 still wasn't patched .. so I'm doing it!) + */ + // private final Timer _cancelerTimer; + // --------------------------------------------------- + + /** + * The source for random values. This is used to introduce random delays in responses. This reduces the potential for collisions on the network. + */ + private final static Random _random = new Random(); + + /** + * This lock is used to coordinate processing of incoming and outgoing messages. This is needed, because the Rendezvous Conformance Test does not forgive race conditions. + */ + private final ReentrantLock _ioLock = new ReentrantLock(); + + /** + * If an incoming package which needs an answer is truncated, we store it here. We add more incoming DNSRecords to it, until the JmDNS.Responder timer picks it up.<br/> + * FIXME [PJYF June 8 2010]: This does not work well with multiple planned answers for packages that came in from different clients. + */ + private DNSIncoming _plannedAnswer; + + // State machine + + /** + * This hashtable is used to maintain a list of service types being collected by this JmDNS instance. The key of the hashtable is a service type name, the value is an instance of JmDNS.ServiceCollector. + * + * @see #list + */ + private final ConcurrentMap<String, ServiceCollector> _serviceCollectors; + + private final String _name; + + /** + * Main method to display API information if run from java -jar + * + * @param argv + * the command line arguments + */ + public static void main(String[] argv) { + String version = null; + try { + final Properties pomProperties = new Properties(); + pomProperties.load(JmDNSImpl.class.getResourceAsStream("/META-INF/maven/javax.jmdns/jmdns/pom.properties")); + version = pomProperties.getProperty("version"); + } catch (Exception e) { + version = "RUNNING.IN.IDE.FULL"; + } + System.out.println("JmDNS version \"" + version + "\""); + System.out.println(" "); + + System.out.println("Running on java version \"" + System.getProperty("java.version") + "\"" + " (build " + System.getProperty("java.runtime.version") + ")" + " from " + System.getProperty("java.vendor")); + + System.out.println("Operating environment \"" + System.getProperty("os.name") + "\"" + " version " + System.getProperty("os.version") + " on " + System.getProperty("os.arch")); + + System.out.println("For more information on JmDNS please visit https://sourceforge.net/projects/jmdns/"); + } + + /** + * Create an instance of JmDNS and bind it to a specific network interface given its IP-address. + * + * @param address + * IP address to bind to. + * @param name + * name of the newly created JmDNS + * @exception IOException + */ + public JmDNSImpl(InetAddress address, String name) throws IOException { + super(); + if (logger.isLoggable(Level.FINER)) { + logger.finer("JmDNS instance created"); + } + _cache = new DNSCache(100); + + _listeners = Collections.synchronizedList(new ArrayList<DNSListener>()); + _serviceListeners = new ConcurrentHashMap<String, List<ServiceListenerStatus>>(); + _typeListeners = Collections.synchronizedSet(new HashSet<ServiceTypeListenerStatus>()); + _serviceCollectors = new ConcurrentHashMap<String, ServiceCollector>(); + + _services = new ConcurrentHashMap<String, ServiceInfo>(20); + _serviceTypes = new ConcurrentHashMap<String, ServiceTypeEntry>(20); + + _localHost = HostInfo.newHostInfo(address, this, name); + _name = (name != null ? name : _localHost.getName()); + + // _cancelerTimer = new Timer("JmDNS.cancelerTimer"); + + // (ldeck 2.1.1) preventing shutdown blocking thread + // ------------------------------------------------- + // _shutdown = new Thread(new Shutdown(), "JmDNS.Shutdown"); + // Runtime.getRuntime().addShutdownHook(_shutdown); + + // ------------------------------------------------- + + // Bind to multicast socket + this.openMulticastSocket(this.getLocalHost()); + this.start(this.getServices().values()); + + this.startReaper(); + } + + private void start(Collection<? extends ServiceInfo> serviceInfos) { + if (_incomingListener == null) { + _incomingListener = new SocketListener(this); + _incomingListener.start(); + } + this.startProber(); + for (ServiceInfo info : serviceInfos) { + try { + this.registerService(new ServiceInfoImpl(info)); + } catch (final Exception exception) { + logger.log(Level.WARNING, "start() Registration exception ", exception); + } + } + } + + private void openMulticastSocket(HostInfo hostInfo) throws IOException { + if (_group == null) { + if (hostInfo.getInetAddress() instanceof Inet6Address) { + _group = InetAddress.getByName(DNSConstants.MDNS_GROUP_IPV6); + } else { + _group = InetAddress.getByName(DNSConstants.MDNS_GROUP); + } + } + if (_socket != null) { + this.closeMulticastSocket(); + } + _socket = new MulticastSocket(DNSConstants.MDNS_PORT); + if ((hostInfo != null) && (hostInfo.getInterface() != null)) { + try { + _socket.setNetworkInterface(hostInfo.getInterface()); + } catch (SocketException e) { + if (logger.isLoggable(Level.FINE)) { + logger.fine("openMulticastSocket() Set network interface exception: " + e.getMessage()); + } + } + } + _socket.setTimeToLive(255); + _socket.joinGroup(_group); + } + + private void closeMulticastSocket() { + // jP: 20010-01-18. See below. We'll need this monitor... + // assert (Thread.holdsLock(this)); + if (logger.isLoggable(Level.FINER)) { + logger.finer("closeMulticastSocket()"); + } + if (_socket != null) { + // close socket + try { + try { + _socket.leaveGroup(_group); + } catch (SocketException exception) { + // + } + _socket.close(); + // jP: 20010-01-18. It isn't safe to join() on the listener + // thread - it attempts to lock the IoLock object, and deadlock + // ensues. Per issue #2933183, changed this to wait on the JmDNS + // monitor, checking on each notify (or timeout) that the + // listener thread has stopped. + // + while (_incomingListener != null && _incomingListener.isAlive()) { + synchronized (this) { + try { + if (_incomingListener != null && _incomingListener.isAlive()) { + // wait time is arbitrary, we're really expecting notification. + if (logger.isLoggable(Level.FINER)) { + logger.finer("closeMulticastSocket(): waiting for jmDNS monitor"); + } + this.wait(1000); + } + } catch (InterruptedException ignored) { + // Ignored + } + } + } + _incomingListener = null; + } catch (final Exception exception) { + logger.log(Level.WARNING, "closeMulticastSocket() Close socket exception ", exception); + } + _socket = null; + } + } + + // State machine + /** + * {@inheritDoc} + */ + @Override + public boolean advanceState(DNSTask task) { + return this._localHost.advanceState(task); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean revertState() { + return this._localHost.revertState(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean cancelState() { + return this._localHost.cancelState(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean closeState() { + return this._localHost.closeState(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean recoverState() { + return this._localHost.recoverState(); + } + + /** + * {@inheritDoc} + */ + @Override + public JmDNSImpl getDns() { + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public void associateWithTask(DNSTask task, DNSState state) { + this._localHost.associateWithTask(task, state); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeAssociationWithTask(DNSTask task) { + this._localHost.removeAssociationWithTask(task); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isAssociatedWithTask(DNSTask task, DNSState state) { + return this._localHost.isAssociatedWithTask(task, state); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isProbing() { + return this._localHost.isProbing(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isAnnouncing() { + return this._localHost.isAnnouncing(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isAnnounced() { + return this._localHost.isAnnounced(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isCanceling() { + return this._localHost.isCanceling(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isCanceled() { + return this._localHost.isCanceled(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isClosing() { + return this._localHost.isClosing(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isClosed() { + return this._localHost.isClosed(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean waitForAnnounced(long timeout) { + return this._localHost.waitForAnnounced(timeout); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean waitForCanceled(long timeout) { + return this._localHost.waitForCanceled(timeout); + } + + /** + * Return the DNSCache associated with the cache variable + * + * @return DNS cache + */ + public DNSCache getCache() { + return _cache; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return _name; + } + + /** + * {@inheritDoc} + */ + @Override + public String getHostName() { + return _localHost.getName(); + } + + /** + * Returns the local host info + * + * @return local host info + */ + public HostInfo getLocalHost() { + return _localHost; + } + + /** + * {@inheritDoc} + */ + @Override + public InetAddress getInetAddress() throws IOException { + return _localHost.getInetAddress(); + } + + /** + * {@inheritDoc} + */ + @Override + @Deprecated + public InetAddress getInterface() throws IOException { + return _socket.getInterface(); + } + + /** + * {@inheritDoc} + */ + @Override + public ServiceInfo getServiceInfo(String type, String name) { + return this.getServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT); + } + + /** + * {@inheritDoc} + */ + @Override + public ServiceInfo getServiceInfo(String type, String name, long timeout) { + return this.getServiceInfo(type, name, false, timeout); + } + + /** + * {@inheritDoc} + */ + @Override + public ServiceInfo getServiceInfo(String type, String name, boolean persistent) { + return this.getServiceInfo(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT); + } + + /** + * {@inheritDoc} + */ + @Override + public ServiceInfo getServiceInfo(String type, String name, boolean persistent, long timeout) { + final ServiceInfoImpl info = this.resolveServiceInfo(type, name, "", persistent); + this.waitForInfoData(info, timeout); + return (info.hasData() ? info : null); + } + + ServiceInfoImpl resolveServiceInfo(String type, String name, String subtype, boolean persistent) { + this.cleanCache(); + String loType = type.toLowerCase(); + this.registerServiceType(type); + if (_serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null) { + this.addServiceListener(loType, _serviceCollectors.get(loType), ListenerStatus.SYNCHONEOUS); + } + + // Check if the answer is in the cache. + final ServiceInfoImpl info = this.getServiceInfoFromCache(type, name, subtype, persistent); + // We still run the resolver to do the dispatch but if the info is already there it will quit immediately + this.startServiceInfoResolver(info); + + return info; + } + + ServiceInfoImpl getServiceInfoFromCache(String type, String name, String subtype, boolean persistent) { + // Check if the answer is in the cache. + ServiceInfoImpl info = new ServiceInfoImpl(type, name, subtype, 0, 0, 0, persistent, (byte[]) null); + DNSEntry pointerEntry = this.getCache().getDNSEntry(new DNSRecord.Pointer(type, DNSRecordClass.CLASS_ANY, false, 0, info.getQualifiedName())); + if (pointerEntry instanceof DNSRecord) { + ServiceInfoImpl cachedInfo = (ServiceInfoImpl) ((DNSRecord) pointerEntry).getServiceInfo(persistent); + if (cachedInfo != null) { + // To get a complete info record we need to retrieve the service, address and the text bytes. + + Map<Fields, String> map = cachedInfo.getQualifiedNameMap(); + byte[] srvBytes = null; + String server = ""; + DNSEntry serviceEntry = this.getCache().getDNSEntry(info.getQualifiedName(), DNSRecordType.TYPE_SRV, DNSRecordClass.CLASS_ANY); + if (serviceEntry instanceof DNSRecord) { + ServiceInfo cachedServiceEntryInfo = ((DNSRecord) serviceEntry).getServiceInfo(persistent); + if (cachedServiceEntryInfo != null) { + cachedInfo = new ServiceInfoImpl(map, cachedServiceEntryInfo.getPort(), cachedServiceEntryInfo.getWeight(), cachedServiceEntryInfo.getPriority(), persistent, (byte[]) null); + srvBytes = cachedServiceEntryInfo.getTextBytes(); + server = cachedServiceEntryInfo.getServer(); + } + } + DNSEntry addressEntry = this.getCache().getDNSEntry(server, DNSRecordType.TYPE_A, DNSRecordClass.CLASS_ANY); + if (addressEntry instanceof DNSRecord) { + ServiceInfo cachedAddressInfo = ((DNSRecord) addressEntry).getServiceInfo(persistent); + if (cachedAddressInfo != null) { + for (Inet4Address address : cachedAddressInfo.getInet4Addresses()) { + cachedInfo.addAddress(address); + } + cachedInfo._setText(cachedAddressInfo.getTextBytes()); + } + } + addressEntry = this.getCache().getDNSEntry(server, DNSRecordType.TYPE_AAAA, DNSRecordClass.CLASS_ANY); + if (addressEntry instanceof DNSRecord) { + ServiceInfo cachedAddressInfo = ((DNSRecord) addressEntry).getServiceInfo(persistent); + if (cachedAddressInfo != null) { + for (Inet6Address address : cachedAddressInfo.getInet6Addresses()) { + cachedInfo.addAddress(address); + } + cachedInfo._setText(cachedAddressInfo.getTextBytes()); + } + } + DNSEntry textEntry = this.getCache().getDNSEntry(cachedInfo.getQualifiedName(), DNSRecordType.TYPE_TXT, DNSRecordClass.CLASS_ANY); + if (textEntry instanceof DNSRecord) { + ServiceInfo cachedTextInfo = ((DNSRecord) textEntry).getServiceInfo(persistent); + if (cachedTextInfo != null) { + cachedInfo._setText(cachedTextInfo.getTextBytes()); + } + } + if (cachedInfo.getTextBytes().length == 0) { + cachedInfo._setText(srvBytes); + } + if (cachedInfo.hasData()) { + info = cachedInfo; + } + } + } + return info; + } + + private void waitForInfoData(ServiceInfo info, long timeout) { + synchronized (info) { + long loops = (timeout / 200L); + if (loops < 1) { + loops = 1; + } + for (int i = 0; i < loops; i++) { + if (info.hasData()) { + break; + } + try { + info.wait(200); + } catch (final InterruptedException e) { + /* Stub */ + } + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void requestServiceInfo(String type, String name) { + this.requestServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT); + } + + /** + * {@inheritDoc} + */ + @Override + public void requestServiceInfo(String type, String name, boolean persistent) { + this.requestServiceInfo(type, name, persistent, DNSConstants.SERVICE_INFO_TIMEOUT); + } + + /** + * {@inheritDoc} + */ + @Override + public void requestServiceInfo(String type, String name, long timeout) { + this.requestServiceInfo(type, name, false, DNSConstants.SERVICE_INFO_TIMEOUT); + } + + /** + * {@inheritDoc} + */ + @Override + public void requestServiceInfo(String type, String name, boolean persistent, long timeout) { + final ServiceInfoImpl info = this.resolveServiceInfo(type, name, "", persistent); + this.waitForInfoData(info, timeout); + } + + void handleServiceResolved(ServiceEvent event) { + List<ServiceListenerStatus> list = _serviceListeners.get(event.getType().toLowerCase()); + final List<ServiceListenerStatus> listCopy; + if ((list != null) && (!list.isEmpty())) { + if ((event.getInfo() != null) && event.getInfo().hasData()) { + final ServiceEvent localEvent = event; + synchronized (list) { + listCopy = new ArrayList<ServiceListenerStatus>(list); + } + for (final ServiceListenerStatus listener : listCopy) { + _executor.submit(new Runnable() { + /** {@inheritDoc} */ + @Override + public void run() { + listener.serviceResolved(localEvent); + } + }); + } + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void addServiceTypeListener(ServiceTypeListener listener) throws IOException { + ServiceTypeListenerStatus status = new ServiceTypeListenerStatus(listener, ListenerStatus.ASYNCHONEOUS); + _typeListeners.add(status); + + // report cached service types + for (String type : _serviceTypes.keySet()) { + status.serviceTypeAdded(new ServiceEventImpl(this, type, "", null)); + } + + this.startTypeResolver(); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeServiceTypeListener(ServiceTypeListener listener) { + ServiceTypeListenerStatus status = new ServiceTypeListenerStatus(listener, ListenerStatus.ASYNCHONEOUS); + _typeListeners.remove(status); + } + + /** + * {@inheritDoc} + */ + @Override + public void addServiceListener(String type, ServiceListener listener) { + this.addServiceListener(type, listener, ListenerStatus.ASYNCHONEOUS); + } + + private void addServiceListener(String type, ServiceListener listener, boolean synch) { + ServiceListenerStatus status = new ServiceListenerStatus(listener, synch); + final String loType = type.toLowerCase(); + List<ServiceListenerStatus> list = _serviceListeners.get(loType); + if (list == null) { + if (_serviceListeners.putIfAbsent(loType, new LinkedList<ServiceListenerStatus>()) == null) { + if (_serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null) { + // We have a problem here. The service collectors must be called synchronously so that their cache get cleaned up immediately or we will report . + this.addServiceListener(loType, _serviceCollectors.get(loType), ListenerStatus.SYNCHONEOUS); + } + } + list = _serviceListeners.get(loType); + } + if (list != null) { + synchronized (list) { + if (!list.contains(listener)) { + list.add(status); + } + } + } + // report cached service types + final List<ServiceEvent> serviceEvents = new ArrayList<ServiceEvent>(); + Collection<DNSEntry> dnsEntryLits = this.getCache().allValues(); + for (DNSEntry entry : dnsEntryLits) { + final DNSRecord record = (DNSRecord) entry; + if (record.getRecordType() == DNSRecordType.TYPE_SRV) { + if (record.getKey().endsWith(loType)) { + // Do not used the record embedded method for generating event this will not work. + // serviceEvents.add(record.getServiceEvent(this)); + serviceEvents.add(new ServiceEventImpl(this, record.getType(), toUnqualifiedName(record.getType(), record.getName()), record.getServiceInfo())); + } + } + } + // Actually call listener with all service events added above + for (ServiceEvent serviceEvent : serviceEvents) { + status.serviceAdded(serviceEvent); + } + // Create/start ServiceResolver + this.startServiceResolver(type); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeServiceListener(String type, ServiceListener listener) { + String loType = type.toLowerCase(); + List<ServiceListenerStatus> list = _serviceListeners.get(loType); + if (list != null) { + synchronized (list) { + ServiceListenerStatus status = new ServiceListenerStatus(listener, ListenerStatus.ASYNCHONEOUS); + list.remove(status); + if (list.isEmpty()) { + _serviceListeners.remove(loType, list); + } + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void registerService(ServiceInfo infoAbstract) throws IOException { + if (this.isClosing() || this.isClosed()) { + throw new IllegalStateException("This DNS is closed."); + } + final ServiceInfoImpl info = (ServiceInfoImpl) infoAbstract; + + if (info.getDns() != null) { + if (info.getDns() != this) { + throw new IllegalStateException("A service information can only be registered with a single instamce of JmDNS."); + } else if (_services.get(info.getKey()) != null) { + throw new IllegalStateException("A service information can only be registered once."); + } + } + info.setDns(this); + + this.registerServiceType(info.getTypeWithSubtype()); + + // bind the service to this address + info.recoverState(); + info.setServer(_localHost.getName()); + info.addAddress(_localHost.getInet4Address()); + info.addAddress(_localHost.getInet6Address()); + + this.waitForAnnounced(DNSConstants.SERVICE_INFO_TIMEOUT); + + this.makeServiceNameUnique(info); + while (_services.putIfAbsent(info.getKey(), info) != null) { + this.makeServiceNameUnique(info); + } + + this.startProber(); + info.waitForAnnounced(DNSConstants.SERVICE_INFO_TIMEOUT); + + if (logger.isLoggable(Level.FINE)) { + logger.fine("registerService() JmDNS registered service as " + info); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unregisterService(ServiceInfo infoAbstract) { + final ServiceInfoImpl info = (ServiceInfoImpl) _services.get(infoAbstract.getKey()); + + if (info != null) { + info.cancelState(); + this.startCanceler(); + info.waitForCanceled(DNSConstants.CLOSE_TIMEOUT); + + _services.remove(info.getKey(), info); + if (logger.isLoggable(Level.FINE)) { + logger.fine("unregisterService() JmDNS unregistered service as " + info); + } + } else { + logger.warning("Removing unregistered service info: " + infoAbstract.getKey()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void unregisterAllServices() { + if (logger.isLoggable(Level.FINER)) { + logger.finer("unregisterAllServices()"); + } + + for (String name : _services.keySet()) { + ServiceInfoImpl info = (ServiceInfoImpl) _services.get(name); + if (info != null) { + if (logger.isLoggable(Level.FINER)) { + logger.finer("Cancelling service info: " + info); + } + info.cancelState(); + } + } + this.startCanceler(); + + for (String name : _services.keySet()) { + ServiceInfoImpl info = (ServiceInfoImpl) _services.get(name); + if (info != null) { + if (logger.isLoggable(Level.FINER)) { + logger.finer("Wait for service info cancel: " + info); + } + info.waitForCanceled(DNSConstants.CLOSE_TIMEOUT); + _services.remove(name, info); + } + } + + } + + /** + * {@inheritDoc} + */ + @Override + public boolean registerServiceType(String type) { + boolean typeAdded = false; + Map<Fields, String> map = ServiceInfoImpl.decodeQualifiedNameMapForType(type); + String domain = map.get(Fields.Domain); + String protocol = map.get(Fields.Protocol); + String application = map.get(Fields.Application); + String subtype = map.get(Fields.Subtype); + + final String name = (application.length() > 0 ? "_" + application + "." : "") + (protocol.length() > 0 ? "_" + protocol + "." : "") + domain + "."; + final String loname = name.toLowerCase(); + if (logger.isLoggable(Level.FINE)) { + logger.fine(this.getName() + ".registering service type: " + type + " as: " + name + (subtype.length() > 0 ? " subtype: " + subtype : "")); + } + if (!_serviceTypes.containsKey(loname) && !application.toLowerCase().equals("dns-sd") && !domain.toLowerCase().endsWith("in-addr.arpa") && !domain.toLowerCase().endsWith("ip6.arpa")) { + typeAdded = _serviceTypes.putIfAbsent(loname, new ServiceTypeEntry(name)) == null; + if (typeAdded) { + final ServiceTypeListenerStatus[] list = _typeListeners.toArray(new ServiceTypeListenerStatus[_typeListeners.size()]); + final ServiceEvent event = new ServiceEventImpl(this, name, "", null); + for (final ServiceTypeListenerStatus status : list) { + _executor.submit(new Runnable() { + /** {@inheritDoc} */ + @Override + public void run() { + status.serviceTypeAdded(event); + } + }); + } + } + } + if (subtype.length() > 0) { + ServiceTypeEntry subtypes = _serviceTypes.get(loname); + if ((subtypes != null) && (!subtypes.contains(subtype))) { + synchronized (subtypes) { + if (!subtypes.contains(subtype)) { + typeAdded = true; + subtypes.add(subtype); + final ServiceTypeListenerStatus[] list = _typeListeners.toArray(new ServiceTypeListenerStatus[_typeListeners.size()]); + final ServiceEvent event = new ServiceEventImpl(this, "_" + subtype + "._sub." + name, "", null); + for (final ServiceTypeListenerStatus status : list) { + _executor.submit(new Runnable() { + /** {@inheritDoc} */ + @Override + public void run() { + status.subTypeForServiceTypeAdded(event); + } + }); + } + } + } + } + } + return typeAdded; + } + + /** + * Generate a possibly unique name for a service using the information we have in the cache. + * + * @return returns true, if the name of the service info had to be changed. + */ + private boolean makeServiceNameUnique(ServiceInfoImpl info) { + final String originalQualifiedName = info.getKey(); + final long now = System.currentTimeMillis(); + + boolean collision; + do { + collision = false; + + // Check for collision in cache + for (DNSEntry dnsEntry : this.getCache().getDNSEntryList(info.getKey())) { + if (DNSRecordType.TYPE_SRV.equals(dnsEntry.getRecordType()) && !dnsEntry.isExpired(now)) { + final DNSRecord.Service s = (DNSRecord.Service) dnsEntry; + if (s.getPort() != info.getPort() || !s.getServer().equals(_localHost.getName())) { + if (logger.isLoggable(Level.FINER)) { + logger.finer("makeServiceNameUnique() JmDNS.makeServiceNameUnique srv collision:" + dnsEntry + " s.server=" + s.getServer() + " " + _localHost.getName() + " equals:" + (s.getServer().equals(_localHost.getName()))); + } + info.setName(incrementName(info.getName())); + collision = true; + break; + } + } + } + + // Check for collision with other service infos published by JmDNS + final ServiceInfo selfService = _services.get(info.getKey()); + if (selfService != null && selfService != info) { + info.setName(incrementName(info.getName())); + collision = true; + } + } + while (collision); + + return !(originalQualifiedName.equals(info.getKey())); + } + + String incrementName(String name) { + String aName = name; + try { + final int l = aName.lastIndexOf('('); + final int r = aName.lastIndexOf(')'); + if ((l >= 0) && (l < r)) { + aName = aName.substring(0, l) + "(" + (Integer.parseInt(aName.substring(l + 1, r)) + 1) + ")"; + } else { + aName += " (2)"; + } + } catch (final NumberFormatException e) { + aName += " (2)"; + } + return aName; + } + + /** + * Add a listener for a question. The listener will receive updates of answers to the question as they arrive, or from the cache if they are already available. + * + * @param listener + * DSN listener + * @param question + * DNS query + */ + public void addListener(DNSListener listener, DNSQuestion question) { + final long now = System.currentTimeMillis(); + + // add the new listener + _listeners.add(listener); + + // report existing matched records + + if (question != null) { + for (DNSEntry dnsEntry : this.getCache().getDNSEntryList(question.getName().toLowerCase())) { + if (question.answeredBy(dnsEntry) && !dnsEntry.isExpired(now)) { + listener.updateRecord(this.getCache(), now, dnsEntry); + } + } + } + } + + /** + * Remove a listener from all outstanding questions. The listener will no longer receive any updates. + * + * @param listener + * DSN listener + */ + public void removeListener(DNSListener listener) { + _listeners.remove(listener); + } + + /** + * Renew a service when the record become stale. If there is no service collector for the type this method does nothing. + * + * @param record + * DNS record + */ + public void renewServiceCollector(DNSRecord record) { + ServiceInfo info = record.getServiceInfo(); + if (_serviceCollectors.containsKey(info.getType().toLowerCase())) { + // Create/start ServiceResolver + this.startServiceResolver(info.getType()); + } + } + + // Remind: Method updateRecord should receive a better name. + /** + * Notify all listeners that a record was updated. + * + * @param now + * update date + * @param rec + * DNS record + * @param operation + * DNS cache operation + */ + public void updateRecord(long now, DNSRecord rec, Operation operation) { + // We do not want to block the entire DNS while we are updating the record for each listener (service info) + { + List<DNSListener> listenerList = null; + synchronized (_listeners) { + listenerList = new ArrayList<DNSListener>(_listeners); + } + for (DNSListener listener : listenerList) { + listener.updateRecord(this.getCache(), now, rec); + } + } + if (DNSRecordType.TYPE_PTR.equals(rec.getRecordType())) + // if (DNSRecordType.TYPE_PTR.equals(rec.getRecordType()) || DNSRecordType.TYPE_SRV.equals(rec.getRecordType())) + { + ServiceEvent event = rec.getServiceEvent(this); + if ((event.getInfo() == null) || !event.getInfo().hasData()) { + // We do not care about the subtype because the info is only used if complete and the subtype will then be included. + ServiceInfo info = this.getServiceInfoFromCache(event.getType(), event.getName(), "", false); + if (info.hasData()) { + event = new ServiceEventImpl(this, event.getType(), event.getName(), info); + } + } + + List<ServiceListenerStatus> list = _serviceListeners.get(event.getType().toLowerCase()); + final List<ServiceListenerStatus> serviceListenerList; + if (list != null) { + synchronized (list) { + serviceListenerList = new ArrayList<ServiceListenerStatus>(list); + } + } else { + serviceListenerList = Collections.emptyList(); + } + if (logger.isLoggable(Level.FINEST)) { + logger.finest(this.getName() + ".updating record for event: " + event + " list " + serviceListenerList + " operation: " + operation); + } + if (!serviceListenerList.isEmpty()) { + final ServiceEvent localEvent = event; + + switch (operation) { + case Add: + for (final ServiceListenerStatus listener : serviceListenerList) { + if (listener.isSynchronous()) { + listener.serviceAdded(localEvent); + } else { + _executor.submit(new Runnable() { + /** {@inheritDoc} */ + @Override + public void run() { + listener.serviceAdded(localEvent); + } + }); + } + } + break; + case Remove: + for (final ServiceListenerStatus listener : serviceListenerList) { + if (listener.isSynchronous()) { + listener.serviceRemoved(localEvent); + } else { + _executor.submit(new Runnable() { + /** {@inheritDoc} */ + @Override + public void run() { + listener.serviceRemoved(localEvent); + } + }); + } + } + break; + default: + break; + } + } + } + } + + void handleRecord(DNSRecord record, long now) { + DNSRecord newRecord = record; + + Operation cacheOperation = Operation.Noop; + final boolean expired = newRecord.isExpired(now); + if (logger.isLoggable(Level.FINE)) { + logger.fine(this.getName() + " handle response: " + newRecord); + } + + // update the cache + if (!newRecord.isServicesDiscoveryMetaQuery() && !newRecord.isDomainDiscoveryQuery()) { + final boolean unique = newRecord.isUnique(); + final DNSRecord cachedRecord = (DNSRecord) this.getCache().getDNSEntry(newRecord); + if (logger.isLoggable(Level.FINE)) { + logger.fine(this.getName() + " handle response cached record: " + cachedRecord); + } + if (unique) { + for (DNSEntry entry : this.getCache().getDNSEntryList(newRecord.getKey())) { + if (newRecord.getRecordType().equals(entry.getRecordType()) && newRecord.getRecordClass().equals(entry.getRecordClass()) && (entry != cachedRecord)) { + ((DNSRecord) entry).setWillExpireSoon(now); + } + } + } + if (cachedRecord != null) { + if (expired) { + // if the record has a 0 ttl that means we have a cancel record we need to delay the removal by 1s + if (newRecord.getTTL() == 0) { + cacheOperation = Operation.Noop; + cachedRecord.setWillExpireSoon(now); + // the actual record will be disposed of by the record reaper. + } else { + cacheOperation = Operation.Remove; + this.getCache().removeDNSEntry(cachedRecord); + } + } else { + // If the record content has changed we need to inform our listeners. + if (!newRecord.sameValue(cachedRecord) || (!newRecord.sameSubtype(cachedRecord) && (newRecord.getSubtype().length() > 0))) { + if (newRecord.isSingleValued()) { + cacheOperation = Operation.Update; + this.getCache().replaceDNSEntry(newRecord, cachedRecord); + } else { + // Address record can have more than one value on multi-homed machines + cacheOperation = Operation.Add; + this.getCache().addDNSEntry(newRecord); + } + } else { + cachedRecord.resetTTL(newRecord); + newRecord = cachedRecord; + } + } + } else { + if (!expired) { + cacheOperation = Operation.Add; + this.getCache().addDNSEntry(newRecord); + } + } + } + + // Register new service types + if (newRecord.getRecordType() == DNSRecordType.TYPE_PTR) { + // handle DNSConstants.DNS_META_QUERY records + boolean typeAdded = false; + if (newRecord.isServicesDiscoveryMetaQuery()) { + // The service names are in the alias. + if (!expired) { + typeAdded = this.registerServiceType(((DNSRecord.Pointer) newRecord).getAlias()); + } + return; + } + typeAdded |= this.registerServiceType(newRecord.getName()); + if (typeAdded && (cacheOperation == Operation.Noop)) { + cacheOperation = Operation.RegisterServiceType; + } + } + + // notify the listeners + if (cacheOperation != Operation.Noop) { + this.updateRecord(now, newRecord, cacheOperation); + } + + } + + /** + * Handle an incoming response. Cache answers, and pass them on to the appropriate questions. + * + * @exception IOException + */ + void handleResponse(DNSIncoming msg) throws IOException { + final long now = System.currentTimeMillis(); + + boolean hostConflictDetected = false; + boolean serviceConflictDetected = false; + + for (DNSRecord newRecord : msg.getAllAnswers()) { + this.handleRecord(newRecord, now); + + if (DNSRecordType.TYPE_A.equals(newRecord.getRecordType()) || DNSRecordType.TYPE_AAAA.equals(newRecord.getRecordType())) { + hostConflictDetected |= newRecord.handleResponse(this); + } else { + serviceConflictDetected |= newRecord.handleResponse(this); + } + + } + + if (hostConflictDetected || serviceConflictDetected) { + this.startProber(); + } + } + + /** + * Handle an incoming query. See if we can answer any part of it given our service infos. + * + * @param in + * @param addr + * @param port + * @exception IOException + */ + void handleQuery(DNSIncoming in, InetAddress addr, int port) throws IOException { + if (logger.isLoggable(Level.FINE)) { + logger.fine(this.getName() + ".handle query: " + in); + } + // Track known answers + boolean conflictDetected = false; + final long expirationTime = System.currentTimeMillis() + DNSConstants.KNOWN_ANSWER_TTL; + for (DNSRecord answer : in.getAllAnswers()) { + conflictDetected |= answer.handleQuery(this, expirationTime); + } + + this.ioLock(); + try { + + if (_plannedAnswer != null) { + _plannedAnswer.append(in); + } else { + DNSIncoming plannedAnswer = in.clone(); + if (in.isTruncated()) { + _plannedAnswer = plannedAnswer; + } + this.startResponder(plannedAnswer, port); + } + + } finally { + this.ioUnlock(); + } + + final long now = System.currentTimeMillis(); + for (DNSRecord answer : in.getAnswers()) { + this.handleRecord(answer, now); + } + + if (conflictDetected) { + this.startProber(); + } + } + + public void respondToQuery(DNSIncoming in) { + this.ioLock(); + try { + if (_plannedAnswer == in) { + _plannedAnswer = null; + } + } finally { + this.ioUnlock(); + } + } + + /** + * Add an answer to a question. Deal with the case when the outgoing packet overflows + * + * @param in + * @param addr + * @param port + * @param out + * @param rec + * @return outgoing answer + * @exception IOException + */ + public DNSOutgoing addAnswer(DNSIncoming in, InetAddress addr, int port, DNSOutgoing out, DNSRecord rec) throws IOException { + DNSOutgoing newOut = out; + if (newOut == null) { + newOut = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false, in.getSenderUDPPayload()); + } + try { + newOut.addAnswer(in, rec); + } catch (final IOException e) { + newOut.setFlags(newOut.getFlags() | DNSConstants.FLAGS_TC); + newOut.setId(in.getId()); + send(newOut); + + newOut = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA, false, in.getSenderUDPPayload()); + newOut.addAnswer(in, rec); + } + return newOut; + } + + /** + * Send an outgoing multicast DNS message. + * + * @param out + * @exception IOException + */ + public void send(DNSOutgoing out) throws IOException { + if (!out.isEmpty()) { + byte[] message = out.data(); + final DatagramPacket packet = new DatagramPacket(message, message.length, _group, DNSConstants.MDNS_PORT); + + if (logger.isLoggable(Level.FINEST)) { + try { + final DNSIncoming msg = new DNSIncoming(packet); + if (logger.isLoggable(Level.FINEST)) { + logger.finest("send(" + this.getName() + ") JmDNS out:" + msg.print(true)); + } + } catch (final IOException e) { + logger.throwing(getClass().toString(), "send(" + this.getName() + ") - JmDNS can not parse what it sends!!!", e); + } + } + final MulticastSocket ms = _socket; + if (ms != null && !ms.isClosed()) { + ms.send(packet); + } + } + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#purgeTimer() + */ + @Override + public void purgeTimer() { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).purgeTimer(); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#purgeStateTimer() + */ + @Override + public void purgeStateTimer() { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).purgeStateTimer(); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#cancelTimer() + */ + @Override + public void cancelTimer() { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).cancelTimer(); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#cancelStateTimer() + */ + @Override + public void cancelStateTimer() { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).cancelStateTimer(); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#startProber() + */ + @Override + public void startProber() { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startProber(); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#startAnnouncer() + */ + @Override + public void startAnnouncer() { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startAnnouncer(); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#startRenewer() + */ + @Override + public void startRenewer() { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startRenewer(); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#startCanceler() + */ + @Override + public void startCanceler() { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startCanceler(); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#startReaper() + */ + @Override + public void startReaper() { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startReaper(); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#startServiceInfoResolver(javax.jmdns.impl.ServiceInfoImpl) + */ + @Override + public void startServiceInfoResolver(ServiceInfoImpl info) { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startServiceInfoResolver(info); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#startTypeResolver() + */ + @Override + public void startTypeResolver() { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startTypeResolver(); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#startServiceResolver(java.lang.String) + */ + @Override + public void startServiceResolver(String type) { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startServiceResolver(type); + } + + /* + * (non-Javadoc) + * @see javax.jmdns.impl.DNSTaskStarter#startResponder(javax.jmdns.impl.DNSIncoming, int) + */ + @Override + public void startResponder(DNSIncoming in, int port) { + DNSTaskStarter.Factory.getInstance().getStarter(this.getDns()).startResponder(in, port); + } + + // REMIND: Why is this not an anonymous inner class? + /** + * Shutdown operations. + */ + protected class Shutdown implements Runnable { + /** {@inheritDoc} */ + @Override + public void run() { + try { + _shutdown = null; + close(); + } catch (Throwable exception) { + System.err.println("Error while shuting down. " + exception); + } + } + } + + private final Object _recoverLock = new Object(); + + /** + * Recover jmdns when there is an error. + */ + public void recover() { + logger.finer(this.getName() + "recover()"); + // We have an IO error so lets try to recover if anything happens lets close it. + // This should cover the case of the IP address changing under our feet + if (this.isClosing() || this.isClosed() || this.isCanceling() || this.isCanceled()) { + return; + } + + // We need some definite lock here as we may have multiple timer running in the same thread that will not be stopped by the reentrant lock + // in the state object. This is only a problem in this case as we are going to execute in seperate thread so that the timer can clear. + synchronized (_recoverLock) { + // Stop JmDNS + // This protects against recursive calls + if (this.cancelState()) { + logger.finer(this.getName() + "recover() thread " + Thread.currentThread().getName()); + Thread recover = new Thread(this.getName() + ".recover()") { + /** + * {@inheritDoc} + */ + @Override + public void run() { + __recover(); + } + }; + recover.start(); + } + } + } + + void __recover() { + // Synchronize only if we are not already in process to prevent dead locks + // + if (logger.isLoggable(Level.FINER)) { + logger.finer(this.getName() + "recover() Cleanning up"); + } + + logger.warning("RECOVERING"); + // Purge the timer + this.purgeTimer(); + + // We need to keep a copy for reregistration + final Collection<ServiceInfo> oldServiceInfos = new ArrayList<ServiceInfo>(getServices().values()); + + // Cancel all services + this.unregisterAllServices(); + this.disposeServiceCollectors(); + + this.waitForCanceled(DNSConstants.CLOSE_TIMEOUT); + + // Purge the canceler timer + this.purgeStateTimer(); + + // + // close multicast socket + this.closeMulticastSocket(); + + // + this.getCache().clear(); + if (logger.isLoggable(Level.FINER)) { + logger.finer(this.getName() + "recover() All is clean"); + } + + if (this.isCanceled()) { + // + // All is clear now start the services + // + for (ServiceInfo info : oldServiceInfos) { + ((ServiceInfoImpl) info).recoverState(); + } + this.recoverState(); + + try { + this.openMulticastSocket(this.getLocalHost()); + this.start(oldServiceInfos); + } catch (final Exception exception) { + logger.log(Level.WARNING, this.getName() + "recover() Start services exception ", exception); + } + logger.log(Level.WARNING, this.getName() + "recover() We are back!"); + } else { + // We have a problem. We could not clear the state. + logger.log(Level.WARNING, this.getName() + "recover() Could not recover we are Down!"); + if (this.getDelegate() != null) { + this.getDelegate().cannotRecoverFromIOError(this.getDns(), oldServiceInfos); + } + } + + } + + public void cleanCache() { + long now = System.currentTimeMillis(); + for (DNSEntry entry : this.getCache().allValues()) { + try { + DNSRecord record = (DNSRecord) entry; + if (record.isExpired(now)) { + this.updateRecord(now, record, Operation.Remove); + this.getCache().removeDNSEntry(record); + } else if (record.isStale(now)) { + // we should query for the record we care about i.e. those in the service collectors + this.renewServiceCollector(record); + } + } catch (Exception exception) { + logger.log(Level.SEVERE, this.getName() + ".Error while reaping records: " + entry, exception); + logger.severe(this.toString()); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void close() { + if (this.isClosing()) { + return; + } + + if (logger.isLoggable(Level.FINER)) { + logger.finer("Cancelling JmDNS: " + this); + } + // Stop JmDNS + // This protects against recursive calls + if (this.closeState()) { + // We got the tie break now clean up + + // Stop the timer + logger.finer("Canceling the timer"); + this.cancelTimer(); + + // Cancel all services + this.unregisterAllServices(); + this.disposeServiceCollectors(); + + if (logger.isLoggable(Level.FINER)) { + logger.finer("Wait for JmDNS cancel: " + this); + } + this.waitForCanceled(DNSConstants.CLOSE_TIMEOUT); + + // Stop the canceler timer + logger.finer("Canceling the state timer"); + this.cancelStateTimer(); + + // Stop the executor + _executor.shutdown(); + + // close socket + this.closeMulticastSocket(); + + // remove the shutdown hook + if (_shutdown != null) { + Runtime.getRuntime().removeShutdownHook(_shutdown); + } + + if (logger.isLoggable(Level.FINER)) { + logger.finer("JmDNS closed."); + } + } + advanceState(null); + } + + /** + * {@inheritDoc} + */ + @Override + @Deprecated + public void printServices() { + System.err.println(toString()); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder aLog = new StringBuilder(2048); + aLog.append("\t---- Local Host -----"); + aLog.append("\n\t"); + aLog.append(_localHost); + aLog.append("\n\t---- Services -----"); + for (String key : _services.keySet()) { + aLog.append("\n\t\tService: "); + aLog.append(key); + aLog.append(": "); + aLog.append(_services.get(key)); + } + aLog.append("\n"); + aLog.append("\t---- Types ----"); + for (String key : _serviceTypes.keySet()) { + ServiceTypeEntry subtypes = _serviceTypes.get(key); + aLog.append("\n\t\tType: "); + aLog.append(subtypes.getType()); + aLog.append(": "); + aLog.append(subtypes.isEmpty() ? "no subtypes" : subtypes); + } + aLog.append("\n"); + aLog.append(_cache.toString()); + aLog.append("\n"); + aLog.append("\t---- Service Collectors ----"); + for (String key : _serviceCollectors.keySet()) { + aLog.append("\n\t\tService Collector: "); + aLog.append(key); + aLog.append(": "); + aLog.append(_serviceCollectors.get(key)); + } + aLog.append("\n"); + aLog.append("\t---- Service Listeners ----"); + for (String key : _serviceListeners.keySet()) { + aLog.append("\n\t\tService Listener: "); + aLog.append(key); + aLog.append(": "); + aLog.append(_serviceListeners.get(key)); + } + return aLog.toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public ServiceInfo[] list(String type) { + return this.list(type, DNSConstants.SERVICE_INFO_TIMEOUT); + } + + /** + * {@inheritDoc} + */ + @Override + public ServiceInfo[] list(String type, long timeout) { + this.cleanCache(); + // Implementation note: The first time a list for a given type is + // requested, a ServiceCollector is created which collects service + // infos. This greatly speeds up the performance of subsequent calls + // to this method. The caveats are, that 1) the first call to this + // method for a given type is slow, and 2) we spawn a ServiceCollector + // instance for each service type which increases network traffic a + // little. + + String loType = type.toLowerCase(); + + boolean newCollectorCreated = false; + if (this.isCanceling() || this.isCanceled()) { + return new ServiceInfo[0]; + } + + ServiceCollector collector = _serviceCollectors.get(loType); + if (collector == null) { + newCollectorCreated = _serviceCollectors.putIfAbsent(loType, new ServiceCollector(type)) == null; + collector = _serviceCollectors.get(loType); + if (newCollectorCreated) { + this.addServiceListener(type, collector, ListenerStatus.SYNCHONEOUS); + } + } + if (logger.isLoggable(Level.FINER)) { + logger.finer(this.getName() + ".collector: " + collector); + } + // At this stage the collector should never be null but it keeps findbugs happy. + return (collector != null ? collector.list(timeout) : new ServiceInfo[0]); + } + + /** + * {@inheritDoc} + */ + @Override + public Map<String, ServiceInfo[]> listBySubtype(String type) { + return this.listBySubtype(type, DNSConstants.SERVICE_INFO_TIMEOUT); + } + + /** + * {@inheritDoc} + */ + @Override + public Map<String, ServiceInfo[]> listBySubtype(String type, long timeout) { + Map<String, List<ServiceInfo>> map = new HashMap<String, List<ServiceInfo>>(5); + for (ServiceInfo info : this.list(type, timeout)) { + String subtype = info.getSubtype().toLowerCase(); + if (!map.containsKey(subtype)) { + map.put(subtype, new ArrayList<ServiceInfo>(10)); + } + map.get(subtype).add(info); + } + + Map<String, ServiceInfo[]> result = new HashMap<String, ServiceInfo[]>(map.size()); + for (String subtype : map.keySet()) { + List<ServiceInfo> infoForSubType = map.get(subtype); + result.put(subtype, infoForSubType.toArray(new ServiceInfo[infoForSubType.size()])); + } + + return result; + } + + /** + * This method disposes all ServiceCollector instances which have been created by calls to method <code>list(type)</code>. + * + * @see #list + */ + private void disposeServiceCollectors() { + if (logger.isLoggable(Level.FINER)) { + logger.finer("disposeServiceCollectors()"); + } + for (String type : _serviceCollectors.keySet()) { + ServiceCollector collector = _serviceCollectors.get(type); + if (collector != null) { + this.removeServiceListener(type, collector); + _serviceCollectors.remove(type, collector); + } + } + } + + /** + * Instances of ServiceCollector are used internally to speed up the performance of method <code>list(type)</code>. + * + * @see #list + */ + private static class ServiceCollector implements ServiceListener { + // private static Logger logger = Logger.getLogger(ServiceCollector.class.getName()); + + /** + * A set of collected service instance names. + */ + private final ConcurrentMap<String, ServiceInfo> _infos; + + /** + * A set of collected service event waiting to be resolved. + */ + private final ConcurrentMap<String, ServiceEvent> _events; + + /** + * This is the type we are listening for (only used for debugging). + */ + private final String _type; + + /** + * This is used to force a wait on the first invocation of list. + */ + private volatile boolean _needToWaitForInfos; + + public ServiceCollector(String type) { + super(); + _infos = new ConcurrentHashMap<String, ServiceInfo>(); + _events = new ConcurrentHashMap<String, ServiceEvent>(); + _type = type; + _needToWaitForInfos = true; + } + + /** + * A service has been added. + * + * @param event + * service event + */ + @Override + public void serviceAdded(ServiceEvent event) { + synchronized (this) { + ServiceInfo info = event.getInfo(); + if ((info != null) && (info.hasData())) { + _infos.put(event.getName(), info); + } else { + String subtype = (info != null ? info.getSubtype() : ""); + info = ((JmDNSImpl) event.getDNS()).resolveServiceInfo(event.getType(), event.getName(), subtype, true); + if (info != null) { + _infos.put(event.getName(), info); + } else { + _events.put(event.getName(), event); + } + } + } + } + + /** + * A service has been removed. + * + * @param event + * service event + */ + @Override + public void serviceRemoved(ServiceEvent event) { + synchronized (this) { + _infos.remove(event.getName()); + _events.remove(event.getName()); + } + } + + /** + * A service has been resolved. Its details are now available in the ServiceInfo record. + * + * @param event + * service event + */ + @Override + public void serviceResolved(ServiceEvent event) { + synchronized (this) { + _infos.put(event.getName(), event.getInfo()); + _events.remove(event.getName()); + } + } + + /** + * Returns an array of all service infos which have been collected by this ServiceCollector. + * + * @param timeout + * timeout if the info list is empty. + * @return Service Info array + */ + public ServiceInfo[] list(long timeout) { + if (_infos.isEmpty() || !_events.isEmpty() || _needToWaitForInfos) { + long loops = (timeout / 200L); + if (loops < 1) { + loops = 1; + } + for (int i = 0; i < loops; i++) { + try { + Thread.sleep(200); + } catch (final InterruptedException e) { + /* Stub */ + } + if (_events.isEmpty() && !_infos.isEmpty() && !_needToWaitForInfos) { + break; + } + } + } + _needToWaitForInfos = false; + return _infos.values().toArray(new ServiceInfo[_infos.size()]); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuffer aLog = new StringBuffer(); + aLog.append("\n\tType: "); + aLog.append(_type); + if (_infos.isEmpty()) { + aLog.append("\n\tNo services collected."); + } else { + aLog.append("\n\tServices"); + for (String key : _infos.keySet()) { + aLog.append("\n\t\tService: "); + aLog.append(key); + aLog.append(": "); + aLog.append(_infos.get(key)); + } + } + if (_events.isEmpty()) { + aLog.append("\n\tNo event queued."); + } else { + aLog.append("\n\tEvents"); + for (String key : _events.keySet()) { + aLog.append("\n\t\tEvent: "); + aLog.append(key); + aLog.append(": "); + aLog.append(_events.get(key)); + } + } + return aLog.toString(); + } + } + + static String toUnqualifiedName(String type, String qualifiedName) { + String loType = type.toLowerCase(); + String loQualifiedName = qualifiedName.toLowerCase(); + if (loQualifiedName.endsWith(loType) && !(loQualifiedName.equals(loType))) { + return qualifiedName.substring(0, qualifiedName.length() - type.length() - 1); + } + return qualifiedName; + } + + public Map<String, ServiceInfo> getServices() { + return _services; + } + + public void setLastThrottleIncrement(long lastThrottleIncrement) { + this._lastThrottleIncrement = lastThrottleIncrement; + } + + public long getLastThrottleIncrement() { + return _lastThrottleIncrement; + } + + public void setThrottle(int throttle) { + this._throttle = throttle; + } + + public int getThrottle() { + return _throttle; + } + + public static Random getRandom() { + return _random; + } + + public void ioLock() { + _ioLock.lock(); + } + + public void ioUnlock() { + _ioLock.unlock(); + } + + public void setPlannedAnswer(DNSIncoming plannedAnswer) { + this._plannedAnswer = plannedAnswer; + } + + public DNSIncoming getPlannedAnswer() { + return _plannedAnswer; + } + + void setLocalHost(HostInfo localHost) { + this._localHost = localHost; + } + + public Map<String, ServiceTypeEntry> getServiceTypes() { + return _serviceTypes; + } + + public MulticastSocket getSocket() { + return _socket; + } + + public InetAddress getGroup() { + return _group; + } + + @Override + public Delegate getDelegate() { + return this._delegate; + } + + @Override + public Delegate setDelegate(Delegate delegate) { + Delegate previous = this._delegate; + this._delegate = delegate; + return previous; + } + +} |