///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.*; import java.util.*; import javax.jmdns.*; import javax.jmdns.impl.tasks.*; // REMIND: multiple IP addresses /** * mDNS implementation in Java. * * @version %I%, %G% * @author Arthur van Hoff, Rick Blair, Jeff Sonstein, Werner Randelshofer, * Pierre Frisch, Scott Lewis */ public class JmDNSImpl extends JmDNS { // private static Logger logger = Logger.getLogger(JmDNSImpl.class.getName()); // hack for the iPhone (arm darwin) to not set the NetworkInterface. This fails on GNU classpath 0.96. private static final boolean isIphone = Boolean.getBoolean("net.mdns.isArmDarwin"); /** * This is the multicast group, we are listening to for multicast DNS * messages. */ private InetAddress group; /** * This is our multicast socket. */ private MulticastSocket socket; /** * Used to fix live lock problem on unregester. */ private boolean closed = false; /** * Holds instances of JmDNS.DNSListener. Must by a synchronized collection, * because it is updated from concurrent threads. */ private List listeners; /** * Holds instances of ServiceListener's. Keys are Strings holding a fully * qualified service type. Values are LinkedList's of ServiceListener's. */ private Map serviceListeners; /** * Holds instances of ServiceTypeListener's. */ private List typeListeners; /** * Cache for DNSEntry's. */ private 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. */ Map services; /** * This hashtable holds the service types that have been registered or that * have been received in an incoming datagram. Keys are instances of String * which hold an all lower-case version of the fully qualified service type. * Values hold the fully qualified service type. */ Map serviceTypes; /** * This is the shutdown hook, we registered with the java runtime. */ private Thread shutdown; /** * Handle on the local host */ private HostInfo localHost; private Thread incomingListener = null; /** * 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; /** * The timer is used to dispatch all outgoing messages of JmDNS. It is also * used to dispatch maintenance tasks for the DNS cache. */ Timer timer; /** * 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 Object ioLock = new Object(); /** * 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. Remind: This does not work well with multiple planned * answers for packages that came in from different clients. */ private DNSIncoming plannedAnswer; // State machine /** * The state of JmDNS. <p/> For proper handling of concurrency, this * variable must be changed only using methods advanceState(), revertState() * and cancel(). */ private DNSState state = DNSState.PROBING_1; /** * Timer task associated to the host name. This is used to prevent from * having multiple tasks associated to the host name at the same time. */ private TimerTask task; /** * 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 HashMap serviceCollectors = new HashMap(); /** * Create an instance of JmDNS. */ public JmDNSImpl() throws IOException { // logger.finer("JmDNS instance created"); try { // determine the interface on which to run on InetAddress addr = null; String ip = System.getProperty("net.mdns.interface"); if (ip != null) { addr = InetAddress.getByName(ip); init(addr, addr.getHostName()); } else { addr = InetAddress.getLocalHost(); init(addr.isLoopbackAddress() ? null : addr, addr.getHostName()); } // [ // PJYF // Oct // 14 // 2004 // ] // Why // do // we // disallow // the // loopback // address // ? } catch (final IOException e) { init(null, "computer"); } } /** * Create an instance of JmDNS and bind it to a specific network interface * given its IP-address. */ public JmDNSImpl(InetAddress addr) throws IOException { try { init(addr, addr.getHostName()); } catch (final IOException e) { init(null, "computer"); } } /** * Initialize everything. * * @param address * The interface to which JmDNS binds to. * @param name * The host name of the interface. */ private void init(InetAddress address, String name) throws IOException { // A host name with "." is illegal. so strip off everything and append . // local. final int idx = name.indexOf("."); if (idx > 0) { name = name.substring(0, idx); } name += ".local."; // localHost to IP address binding localHost = new HostInfo(address, name); cache = new DNSCache(100); listeners = Collections.synchronizedList(new ArrayList()); serviceListeners = new HashMap(); typeListeners = new ArrayList(); services = new Hashtable(20); serviceTypes = new Hashtable(20); // REMIND: If I could pass in a name for the Timer thread, // I would pass' JmDNS.Timer'. timer = new Timer(); new RecordReaper(this).start(timer); shutdown = new Thread(new Shutdown(), "JmDNS.Shutdown"); Runtime.getRuntime().addShutdownHook(shutdown); incomingListener = new Thread(new SocketListener(this), "JmDNS.SocketListener"); // Bind to multicast socket openMulticastSocket(getLocalHost()); start(getServices().values()); } private void start(Collection serviceInfos) { setState(DNSState.PROBING_1); incomingListener.start(); new Prober(this).start(timer); for (final Iterator iterator = serviceInfos.iterator(); iterator.hasNext();) { try { registerService(new ServiceInfoImpl((ServiceInfoImpl) iterator.next())); } catch (final Exception exception) { // logger.log(Level.WARNING, "start() Registration exception ", exception); } } } private void openMulticastSocket(HostInfo hostInfo) throws IOException { if (group == null) { group = InetAddress.getByName(DNSConstants.MDNS_GROUP); } if (socket != null) { this.closeMulticastSocket(); } socket = new MulticastSocket(DNSConstants.MDNS_PORT); if (!isIphone && (hostInfo != null) && (localHost.getInterface() != null)) { socket.setNetworkInterface(hostInfo.getInterface()); } socket.setTimeToLive(255); socket.joinGroup(group); } private void closeMulticastSocket() { // logger.finer("closeMulticastSocket()"); if (socket != null) { // close socket try { socket.leaveGroup(group); socket.close(); if (incomingListener != null) { incomingListener.join(); } } catch (final Exception exception) { // logger.log(Level.WARNING, "closeMulticastSocket() Close socket exception ", // exception); } socket = null; } } // State machine /** * Sets the state and notifies all objects that wait on JmDNS. */ public synchronized void advanceState() { setState(getState().advance()); notifyAll(); } /** * Sets the state and notifies all objects that wait on JmDNS. */ synchronized void revertState() { setState(getState().revert()); notifyAll(); } /** * Sets the state and notifies all objects that wait on JmDNS. */ synchronized void cancel() { setState(DNSState.CANCELED); notifyAll(); } /** * Returns the current state of this info. */ public DNSState getState() { return state; } /** * Return the DNSCache associated with the cache variable */ public DNSCache getCache() { return cache; } /** * @see javax.jmdns.JmDNS#getHostName() */ public String getHostName() { return localHost.getName(); } public HostInfo getLocalHost() { return localHost; } /** * @see javax.jmdns.JmDNS#getInterface() */ public InetAddress getInterface() throws IOException { return socket.getInterface(); } /** * @see javax.jmdns.JmDNS#getServiceInfo(java.lang.String, java.lang.String) */ public ServiceInfo getServiceInfo(String type, String name) { return getServiceInfo(type, name, 3 * 1000); } /** * @see javax.jmdns.JmDNS#getServiceInfo(java.lang.String, java.lang.String, * int) */ public ServiceInfo getServiceInfo(String type, String name, int timeout) { final ServiceInfoImpl info = new ServiceInfoImpl(type, name); new ServiceInfoResolver(this, info).start(timer); try { final long end = System.currentTimeMillis() + timeout; long delay; synchronized (info) { while (!info.hasData() && (delay = end - System.currentTimeMillis()) > 0) { info.wait(delay); } } } catch (final InterruptedException e) { // empty } return (info.hasData()) ? info : null; } /** * @see javax.jmdns.JmDNS#requestServiceInfo(java.lang.String, * java.lang.String) */ public void requestServiceInfo(String type, String name) { requestServiceInfo(type, name, 3 * 1000); } /** * @see javax.jmdns.JmDNS#requestServiceInfo(java.lang.String, * java.lang.String, int) */ public void requestServiceInfo(String type, String name, int timeout) { registerServiceType(type); final ServiceInfoImpl info = new ServiceInfoImpl(type, name); new ServiceInfoResolver(this, info).start(timer); try { final long end = System.currentTimeMillis() + timeout; long delay; synchronized (info) { while (!info.hasData() && (delay = end - System.currentTimeMillis()) > 0) { info.wait(delay); } } } catch (final InterruptedException e) { // empty } } void handleServiceResolved(ServiceInfoImpl info) { List list = null; ArrayList listCopy = null; synchronized (serviceListeners) { list = (List) serviceListeners.get(info.type.toLowerCase()); if (list != null) { listCopy = new ArrayList(list); } } if (listCopy != null) { final ServiceEvent event = new ServiceEventImpl(this, info.type, info.getName(), info); for (final Iterator iterator = listCopy.iterator(); iterator.hasNext();) { ((ServiceListener) iterator.next()).serviceResolved(event); } } } /** * @see * javax.jmdns.JmDNS#addServiceTypeListener(javax.jmdns.ServiceTypeListener * ) */ public void addServiceTypeListener(ServiceTypeListener listener) throws IOException { synchronized (this) { typeListeners.remove(listener); typeListeners.add(listener); } // report cached service types for (final Iterator iterator = serviceTypes.values().iterator(); iterator.hasNext();) { listener.serviceTypeAdded(new ServiceEventImpl(this, (String) iterator.next(), null, null)); } new TypeResolver(this).start(timer); } /** * @see javax.jmdns.JmDNS#removeServiceTypeListener(javax.jmdns. * ServiceTypeListener) */ public void removeServiceTypeListener(ServiceTypeListener listener) { synchronized (this) { typeListeners.remove(listener); } } /** * @see javax.jmdns.JmDNS#addServiceListener(java.lang.String, * javax.jmdns.ServiceListener) */ public void addServiceListener(String type, ServiceListener listener) { final String lotype = type.toLowerCase(); removeServiceListener(lotype, listener); List list = null; synchronized (serviceListeners) { list = (List) serviceListeners.get(lotype); if (list == null) { list = Collections.synchronizedList(new LinkedList()); serviceListeners.put(lotype, list); } list.add(listener); } // report cached service types final List serviceEvents = new ArrayList(); synchronized (cache) { for (final Iterator i = cache.iterator(); i.hasNext();) { for (DNSCache.CacheNode n = (DNSCache.CacheNode) i.next(); n != null; n = n.next()) { final DNSRecord rec = (DNSRecord) n.getValue(); if (rec.type == DNSConstants.TYPE_SRV) { if (rec.name.endsWith(type)) { serviceEvents.add(new ServiceEventImpl(this, type, toUnqualifiedName(type, rec.name), null)); } } } } } // Actually call listener with all service events added above for (final Iterator i = serviceEvents.iterator(); i.hasNext();) { listener.serviceAdded((ServiceEventImpl) i.next()); } // Create/start ServiceResolver new ServiceResolver(this, type).start(timer); } /** * @see javax.jmdns.JmDNS#removeServiceListener(java.lang.String, * javax.jmdns.ServiceListener) */ public void removeServiceListener(String type, ServiceListener listener) { type = type.toLowerCase(); List list = null; synchronized (serviceListeners) { list = (List) serviceListeners.get(type); if (list != null) { list.remove(listener); if (list.size() == 0) { serviceListeners.remove(type); } } } } /** * @see javax.jmdns.JmDNS#registerService(javax.jmdns.ServiceInfo) */ public void registerService(ServiceInfo infoAbstract) throws IOException { final ServiceInfoImpl info = (ServiceInfoImpl) infoAbstract; registerServiceType(info.type); // bind the service to this address info.server = localHost.getName(); info.addr = localHost.getAddress(); synchronized (this) { makeServiceNameUnique(info); services.put(info.getQualifiedName().toLowerCase(), info); } new /* Service */Prober(this).start(timer); try { synchronized (info) { while (info.getState().compareTo(DNSState.ANNOUNCED) < 0) { info.wait(); } } } catch (final InterruptedException e) { // empty } // logger.fine("registerService() JmDNS registered service as " + info); } /** * @see javax.jmdns.JmDNS#unregisterService(javax.jmdns.ServiceInfo) */ public void unregisterService(ServiceInfo infoAbstract) { final ServiceInfoImpl info = (ServiceInfoImpl) infoAbstract; synchronized (this) { services.remove(info.getQualifiedName().toLowerCase()); } info.cancel(); // Note: We use this lock object to synchronize on it. // Synchronizing on another object (e.g. the ServiceInfo) does // not make sense, because the sole purpose of the lock is to // wait until the canceler has finished. If we synchronized on // the ServiceInfo or on the Canceler, we would block all // accesses to synchronized methods on that object. This is not // what we want! final Object lock = new Object(); // Remind: We get a deadlock here, if the Canceler does not run! try { synchronized (lock) { new Canceler(this, info, lock).start(timer); lock.wait(); } } catch (final InterruptedException e) { // empty } } /** * @see javax.jmdns.JmDNS#unregisterAllServices() */ public void unregisterAllServices() { // logger.finer("unregisterAllServices()"); if (services.size() == 0) { return; } Collection list; synchronized (this) { list = new LinkedList(services.values()); services.clear(); } for (final Iterator iterator = list.iterator(); iterator.hasNext();) { ((ServiceInfoImpl) iterator.next()).cancel(); } final Object lock = new Object(); new Canceler(this, list, lock).start(timer); // Remind: We get a livelock here, if the Canceler does not run! try { synchronized (lock) { if (!closed) { lock.wait(1000); } } } catch (final InterruptedException e) { // empty } } /** * @see javax.jmdns.JmDNS#registerServiceType(java.lang.String) */ public void registerServiceType(String type) { final String name = type.toLowerCase(); if (serviceTypes.get(name) == null) { if ((type.indexOf(DNSConstants.DNS_META_QUERY) < 0) && !type.endsWith(".in-addr.arpa.")) { Collection list; synchronized (this) { serviceTypes.put(name, type); list = new LinkedList(typeListeners); } for (final Iterator iterator = list.iterator(); iterator.hasNext();) { ((ServiceTypeListener) iterator.next()).serviceTypeAdded(new ServiceEventImpl(this, type, null, null)); } } } } /** * Generate a possibly unique name for a host using the information we have * in the cache. * * @return returns true, if the name of the host had to be changed. */ // private boolean makeHostNameUnique(DNSRecord.Address host) // { // final String originalName = host.getName(); // System.currentTimeMillis(); // // boolean collision; // do // { // collision = false; // // // Check for collision in cache // for (DNSCache.CacheNode j = cache.find(host.getName().toLowerCase()); j != null; j = j // .next()) // { // if (false) // { // host.name = incrementName(host.getName()); // collision = true; // break; // } // } // } // while (collision); // // if (originalName.equals(host.getName())) // { // return false; // } // else // { // return true; // } // } /** * 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.getQualifiedName(); final long now = System.currentTimeMillis(); boolean collision; do { collision = false; // Check for collision in cache for (DNSCache.CacheNode j = cache.find(info.getQualifiedName().toLowerCase()); j != null; j = j.next()) { final DNSRecord a = (DNSRecord) j.getValue(); if ((a.type == DNSConstants.TYPE_SRV) && !a.isExpired(now)) { final DNSRecord.Service s = (DNSRecord.Service) a; if (s.port != info.port || !s.server.equals(localHost.getName())) { // logger // .finer("makeServiceNameUnique() JmDNS.makeServiceNameUnique srv collision:" // + a // + " s.server=" // + s.server // + " " // + localHost.getName() // + " equals:" + (s.server.equals(localHost.getName()))); info.setName(incrementName(info.getName())); collision = true; break; } } } // Check for collision with other service infos published by JmDNS final Object selfService = services.get(info.getQualifiedName().toLowerCase()); if (selfService != null && selfService != info) { info.setName(incrementName(info.getName())); collision = true; } } while (collision); return !(originalQualifiedName.equals(info.getQualifiedName())); } String incrementName(String name) { try { final int l = name.lastIndexOf('('); final int r = name.lastIndexOf(')'); if ((l >= 0) && (l < r)) { name = name.substring(0, l) + "(" + (Integer.parseInt(name.substring(l + 1, r)) + 1) + ")"; } else { name += " (2)"; } } catch (final NumberFormatException e) { name += " (2)"; } return name; } /** * 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. */ public void addListener(DNSListener listener, DNSQuestion question) { final long now = System.currentTimeMillis(); // add the new listener synchronized (this) { listeners.add(listener); } // report existing matched records if (question != null) { for (DNSCache.CacheNode i = cache.find(question.name); i != null; i = i.next()) { final DNSRecord c = (DNSRecord) i.getValue(); if (question.answeredBy(c) && !c.isExpired(now)) { listener.updateRecord(this, now, c); } } } } /** * Remove a listener from all outstanding questions. The listener will no * longer receive any updates. */ public void removeListener(DNSListener listener) { synchronized (this) { listeners.remove(listener); } } // Remind: Method updateRecord should receive a better name. /** * Notify all listeners that a record was updated. */ public void updateRecord(long now, DNSRecord rec) { // We do not want to block the entire DNS while we are updating the // record for each listener (service info) List listenerList = null; synchronized (this) { listenerList = new ArrayList(listeners); } for (final Iterator iterator = listenerList.iterator(); iterator.hasNext();) { final DNSListener listener = (DNSListener) iterator.next(); listener.updateRecord(this, now, rec); } if (rec.type == DNSConstants.TYPE_PTR || rec.type == DNSConstants.TYPE_SRV) { List serviceListenerList = null; synchronized (serviceListeners) { serviceListenerList = (List) serviceListeners.get(rec.name.toLowerCase()); // Iterate on a copy in case listeners will modify it if (serviceListenerList != null) { serviceListenerList = new ArrayList(serviceListenerList); } } if (serviceListenerList != null) { final boolean expired = rec.isExpired(now); final String type = rec.getName(); final String name = ((DNSRecord.Pointer) rec).getAlias(); // DNSRecord old = (DNSRecord)services.get(name.toLowerCase()); if (!expired) { // new record final ServiceEvent event = new ServiceEventImpl(this, type, toUnqualifiedName(type, name), null); for (final Iterator iterator = serviceListenerList.iterator(); iterator.hasNext();) { ((ServiceListener) iterator.next()).serviceAdded(event); } } else { // expire record final ServiceEvent event = new ServiceEventImpl(this, type, toUnqualifiedName(type, name), null); for (final Iterator iterator = serviceListenerList.iterator(); iterator.hasNext();) { ((ServiceListener) iterator.next()).serviceRemoved(event); } } } } } /** * Handle an incoming response. Cache answers, and pass them on to the * appropriate questions. */ void handleResponse(DNSIncoming msg) throws IOException { final long now = System.currentTimeMillis(); boolean hostConflictDetected = false; boolean serviceConflictDetected = false; for (final Iterator i = msg.answers.iterator(); i.hasNext();) { boolean isInformative = false; DNSRecord rec = (DNSRecord) i.next(); final boolean expired = rec.isExpired(now); // update the cache final DNSRecord c = (DNSRecord) cache.get(rec); if (c != null) { if (expired) { isInformative = true; cache.remove(c); } else { c.resetTTL(rec); rec = c; } } else { if (!expired) { isInformative = true; cache.add(rec); } } switch (rec.type) { case DNSConstants.TYPE_PTR : // handle _mdns._udp records if (rec.getName().indexOf(DNSConstants.DNS_META_QUERY) >= 0) { if (!expired && rec.name.startsWith("_services" + DNSConstants.DNS_META_QUERY)) { isInformative = true; registerServiceType(((DNSRecord.Pointer) rec).alias); } continue; } registerServiceType(rec.name); break; } if ((rec.getType() == DNSConstants.TYPE_A) || (rec.getType() == DNSConstants.TYPE_AAAA)) { hostConflictDetected |= rec.handleResponse(this); } else { serviceConflictDetected |= rec.handleResponse(this); } // notify the listeners if (isInformative) { updateRecord(now, rec); } } if (hostConflictDetected || serviceConflictDetected) { new Prober(this).start(timer); } } /** * Handle an incoming query. See if we can answer any part of it given our * service infos. */ void handleQuery(DNSIncoming in, InetAddress addr, int port) throws IOException { // Track known answers boolean hostConflictDetected = false; boolean serviceConflictDetected = false; final long expirationTime = System.currentTimeMillis() + DNSConstants.KNOWN_ANSWER_TTL; for (final Iterator i = in.answers.iterator(); i.hasNext();) { final DNSRecord answer = (DNSRecord) i.next(); if ((answer.getType() == DNSConstants.TYPE_A) || (answer.getType() == DNSConstants.TYPE_AAAA)) { hostConflictDetected |= answer.handleQuery(this, expirationTime); } else { serviceConflictDetected |= answer.handleQuery(this, expirationTime); } } if (plannedAnswer != null) { plannedAnswer.append(in); } else { if (in.isTruncated()) { plannedAnswer = in; } new Responder(this, in, addr, port).start(); } if (hostConflictDetected || serviceConflictDetected) { new Prober(this).start(timer); } } /** * Add an answer to a question. Deal with the case when the outgoing packet * overflows */ public DNSOutgoing addAnswer(DNSIncoming in, InetAddress addr, int port, DNSOutgoing out, DNSRecord rec) throws IOException { if (out == null) { out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA); } try { out.addAnswer(in, rec); } catch (final IOException e) { out.flags |= DNSConstants.FLAGS_TC; out.id = in.id; out.finish(); send(out); out = new DNSOutgoing(DNSConstants.FLAGS_QR_RESPONSE | DNSConstants.FLAGS_AA); out.addAnswer(in, rec); } return out; } /** * Send an outgoing multicast DNS message. */ public void send(DNSOutgoing out) throws IOException { out.finish(); if (!out.isEmpty()) { final DatagramPacket packet = new DatagramPacket(out.data, out.off, group, DNSConstants.MDNS_PORT); try { final DNSIncoming msg = new DNSIncoming(packet); // logger.finest("send() JmDNS out:" + msg.print(true)); } catch (final IOException e) { // logger.throwing(getClass().toString(), // "send(DNSOutgoing) - JmDNS can not parse what it sends!!!", e); } final MulticastSocket ms = socket; if (ms != null && !ms.isClosed()) ms.send(packet); } } public void startAnnouncer() { if (getState() != DNSState.CANCELED) new Announcer(this).start(timer); } public void startRenewer() { if (getState() != DNSState.CANCELED) new Renewer(this).start(timer); } public void schedule(TimerTask task, int delay) { if (getState() != DNSState.CANCELED) timer.schedule(task, delay); } // REMIND: Why is this not an anonymous inner class? /** * Shutdown operations. */ private class Shutdown implements Runnable { public void run() { shutdown = null; close(); } } /** * Recover jmdns when there is an error. */ public void recover() { // logger.finer("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 (DNSState.CANCELED != getState()) { synchronized (this) { // Synchronize only if we are not already in process to prevent // dead locks // // logger.finer("recover() Cleanning up"); // Stop JmDNS setState(DNSState.CANCELED); // This protects against recursive // calls // We need to keep a copy for reregistration final Collection oldServiceInfos = new ArrayList(getServices().values()); // Cancel all services unregisterAllServices(); disposeServiceCollectors(); // // close multicast socket closeMulticastSocket(); // cache.clear(); // logger.finer("recover() All is clean"); // // All is clear now start the services // try { openMulticastSocket(getLocalHost()); start(oldServiceInfos); } catch (final Exception exception) { // logger.log(Level.WARNING, "recover() Start services exception ", exception); } // logger.log(Level.WARNING, "recover() We are back!"); } } } /** * @see javax.jmdns.JmDNS#close() */ public void close() { if (getState() != DNSState.CANCELED) { synchronized (this) { // Synchronize only if we are not already in process to prevent // dead locks // Stop JmDNS setState(DNSState.CANCELED); // This protects against recursive // calls unregisterAllServices(); // Stop the timer timer.cancel(); disposeServiceCollectors(); // close socket closeMulticastSocket(); // remove the shutdown hook if (shutdown != null) { Runtime.getRuntime().removeShutdownHook(shutdown); } } } } /** * List cache entries, for debugging only. */ void print() { System.out.println("---- cache ----"); cache.print(); System.out.println(); } /** * @see javax.jmdns.JmDNS#printServices() */ public void printServices() { System.err.println(toString()); } public String toString() { final StringBuffer aLog = new StringBuffer(); aLog.append("\t---- Services -----"); if (services != null) { for (final Iterator k = services.keySet().iterator(); k.hasNext();) { final Object key = k.next(); aLog.append("\n\t\tService: " + key + ": " + services.get(key)); } } aLog.append("\n"); aLog.append("\t---- Types ----"); if (serviceTypes != null) { for (final Iterator k = serviceTypes.keySet().iterator(); k.hasNext();) { final Object key = k.next(); aLog.append("\n\t\tType: " + key + ": " + serviceTypes.get(key)); } } aLog.append("\n"); aLog.append(cache.toString()); aLog.append("\n"); aLog.append("\t---- Service Collectors ----"); if (serviceCollectors != null) { synchronized (serviceCollectors) { for (final Iterator k = serviceCollectors.keySet().iterator(); k.hasNext();) { final Object key = k.next(); aLog.append("\n\t\tService Collector: " + key + ": " + serviceCollectors.get(key)); } serviceCollectors.clear(); } } return aLog.toString(); } /** * @see javax.jmdns.JmDNS#list(java.lang.String) */ public ServiceInfo[] list(String type) { // 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. ServiceCollector collector; boolean newCollectorCreated; synchronized (serviceCollectors) { collector = (ServiceCollector) serviceCollectors.get(type); if (collector == null) { collector = new ServiceCollector(type); serviceCollectors.put(type, collector); addServiceListener(type, collector); newCollectorCreated = true; } else { newCollectorCreated = false; } } // After creating a new ServiceCollector, we collect service infos for // 200 milliseconds. This should be enough time, to get some service // infos from the network. if (newCollectorCreated) { try { Thread.sleep(200); } catch (final InterruptedException e) { } } return collector.list(); } /** * This method disposes all ServiceCollector instances which have been * created by calls to method <code>list(type)</code>. * * @see #list */ private void disposeServiceCollectors() { // logger.finer("disposeServiceCollectors()"); synchronized (serviceCollectors) { for (final Iterator i = serviceCollectors.values().iterator(); i.hasNext();) { final ServiceCollector collector = (ServiceCollector) i.next(); removeServiceListener(collector.type, collector); } serviceCollectors.clear(); } } /** * 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 Map infos = Collections.synchronizedMap(new HashMap()); public String type; public ServiceCollector(String type) { this.type = type; } /** * A service has been added. */ public void serviceAdded(ServiceEvent event) { synchronized (infos) { event.getDNS().requestServiceInfo(event.getType(), event.getName(), 0); } } /** * A service has been removed. */ public void serviceRemoved(ServiceEvent event) { synchronized (infos) { infos.remove(event.getName()); } } /** * A service hase been resolved. Its details are now available in the * ServiceInfo record. */ public void serviceResolved(ServiceEvent event) { synchronized (infos) { infos.put(event.getName(), event.getInfo()); } } /** * Returns an array of all service infos which have been collected by * this ServiceCollector. */ public ServiceInfoImpl[] list() { synchronized (infos) { return (ServiceInfoImpl[]) infos.values().toArray(new ServiceInfoImpl[infos.size()]); } } public String toString() { final StringBuffer aLog = new StringBuffer(); synchronized (infos) { for (final Iterator k = infos.keySet().iterator(); k.hasNext();) { final Object key = k.next(); aLog.append("\n\t\tService: " + key + ": " + infos.get(key)); } } return aLog.toString(); } }; private static String toUnqualifiedName(String type, String qualifiedName) { if (qualifiedName.endsWith(type)) { return qualifiedName.substring(0, qualifiedName.length() - type.length() - 1); } else { return qualifiedName; } } public void setState(DNSState state) { this.state = state; } public void setTask(TimerTask task) { this.task = task; } public TimerTask getTask() { return task; } public Map 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 setIoLock(Object ioLock) { this.ioLock = ioLock; } public Object getIoLock() { return ioLock; } public void setPlannedAnswer(DNSIncoming plannedAnswer) { this.plannedAnswer = plannedAnswer; } public DNSIncoming getPlannedAnswer() { return plannedAnswer; } void setLocalHost(HostInfo localHost) { this.localHost = localHost; } public Map getServiceTypes() { return serviceTypes; } public void setClosed(boolean closed) { this.closed = closed; } public boolean isClosed() { return closed; } public MulticastSocket getSocket() { return socket; } public InetAddress getGroup() { return group; } }