190 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			190 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict'
 | |
| 
 | |
| var dnsEqual = require('dns-equal')
 | |
| var flatten = require('array-flatten')
 | |
| var Service = require('./service')
 | |
| 
 | |
| var REANNOUNCE_MAX_MS = 60 * 60 * 1000
 | |
| var REANNOUNCE_FACTOR = 3
 | |
| 
 | |
| module.exports = Registry
 | |
| 
 | |
| function Registry (server) {
 | |
|   this._server = server
 | |
|   this._services = []
 | |
| }
 | |
| 
 | |
| Registry.prototype.publish = function (opts) {
 | |
|   var service = new Service(opts)
 | |
|   service.start = start.bind(service, this)
 | |
|   service.stop = stop.bind(service, this)
 | |
|   service.start({ probe: opts.probe !== false })
 | |
|   return service
 | |
| }
 | |
| 
 | |
| Registry.prototype.unpublishAll = function (cb) {
 | |
|   teardown(this._server, this._services, cb)
 | |
|   this._services = []
 | |
| }
 | |
| 
 | |
| Registry.prototype.destroy = function () {
 | |
|   this._services.forEach(function (service) {
 | |
|     service._destroyed = true
 | |
|   })
 | |
| }
 | |
| 
 | |
| function start (registry, opts) {
 | |
|   if (this._activated) return
 | |
|   this._activated = true
 | |
| 
 | |
|   registry._services.push(this)
 | |
| 
 | |
|   if (opts.probe) {
 | |
|     var service = this
 | |
|     probe(registry._server.mdns, this, function (exists) {
 | |
|       if (exists) {
 | |
|         service.stop()
 | |
|         service.emit('error', new Error('Service name is already in use on the network'))
 | |
|         return
 | |
|       }
 | |
|       announce(registry._server, service)
 | |
|     })
 | |
|   } else {
 | |
|     announce(registry._server, this)
 | |
|   }
 | |
| }
 | |
| 
 | |
| function stop (registry, cb) {
 | |
|   if (!this._activated) return // TODO: What about the callback?
 | |
| 
 | |
|   teardown(registry._server, this, cb)
 | |
| 
 | |
|   var index = registry._services.indexOf(this)
 | |
|   if (index !== -1) registry._services.splice(index, 1)
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Check if a service name is already in use on the network.
 | |
|  *
 | |
|  * Used before announcing the new service.
 | |
|  *
 | |
|  * To guard against race conditions where multiple services are started
 | |
|  * simultaneously on the network, wait a random amount of time (between
 | |
|  * 0 and 250 ms) before probing.
 | |
|  *
 | |
|  * TODO: Add support for Simultaneous Probe Tiebreaking:
 | |
|  * https://tools.ietf.org/html/rfc6762#section-8.2
 | |
|  */
 | |
| function probe (mdns, service, cb) {
 | |
|   var sent = false
 | |
|   var retries = 0
 | |
|   var timer
 | |
| 
 | |
|   mdns.on('response', onresponse)
 | |
|   setTimeout(send, Math.random() * 250)
 | |
| 
 | |
|   function send () {
 | |
|     // abort if the service have or is being stopped in the meantime
 | |
|     if (!service._activated || service._destroyed) return
 | |
| 
 | |
|     mdns.query(service.fqdn, 'ANY', function () {
 | |
|       // This function will optionally be called with an error object. We'll
 | |
|       // just silently ignore it and retry as we normally would
 | |
|       sent = true
 | |
|       timer = setTimeout(++retries < 3 ? send : done, 250)
 | |
|       timer.unref()
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   function onresponse (packet) {
 | |
|     // Apparently conflicting Multicast DNS responses received *before*
 | |
|     // the first probe packet is sent MUST be silently ignored (see
 | |
|     // discussion of stale probe packets in RFC 6762 Section 8.2,
 | |
|     // "Simultaneous Probe Tiebreaking" at
 | |
|     // https://tools.ietf.org/html/rfc6762#section-8.2
 | |
|     if (!sent) return
 | |
| 
 | |
|     if (packet.answers.some(matchRR) || packet.additionals.some(matchRR)) done(true)
 | |
|   }
 | |
| 
 | |
|   function matchRR (rr) {
 | |
|     return dnsEqual(rr.name, service.fqdn)
 | |
|   }
 | |
| 
 | |
|   function done (exists) {
 | |
|     mdns.removeListener('response', onresponse)
 | |
|     clearTimeout(timer)
 | |
|     cb(!!exists)
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Initial service announcement
 | |
|  *
 | |
|  * Used to announce new services when they are first registered.
 | |
|  *
 | |
|  * Broadcasts right away, then after 3 seconds, 9 seconds, 27 seconds,
 | |
|  * and so on, up to a maximum interval of one hour.
 | |
|  */
 | |
| function announce (server, service) {
 | |
|   var delay = 1000
 | |
|   var packet = service._records()
 | |
| 
 | |
|   server.register(packet)
 | |
| 
 | |
|   ;(function broadcast () {
 | |
|     // abort if the service have or is being stopped in the meantime
 | |
|     if (!service._activated || service._destroyed) return
 | |
| 
 | |
|     server.mdns.respond(packet, function () {
 | |
|       // This function will optionally be called with an error object. We'll
 | |
|       // just silently ignore it and retry as we normally would
 | |
|       if (!service.published) {
 | |
|         service._activated = true
 | |
|         service.published = true
 | |
|         service.emit('up')
 | |
|       }
 | |
|       delay = delay * REANNOUNCE_FACTOR
 | |
|       if (delay < REANNOUNCE_MAX_MS && !service._destroyed) {
 | |
|         setTimeout(broadcast, delay).unref()
 | |
|       }
 | |
|     })
 | |
|   })()
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Stop the given services
 | |
|  *
 | |
|  * Besides removing a service from the mDNS registry, a "goodbye"
 | |
|  * message is sent for each service to let the network know about the
 | |
|  * shutdown.
 | |
|  */
 | |
| function teardown (server, services, cb) {
 | |
|   if (!Array.isArray(services)) services = [services]
 | |
| 
 | |
|   services = services.filter(function (service) {
 | |
|     return service._activated // ignore services not currently starting or started
 | |
|   })
 | |
| 
 | |
|   var records = flatten.depth(services.map(function (service) {
 | |
|     service._activated = false
 | |
|     var records = service._records()
 | |
|     records.forEach(function (record) {
 | |
|       record.ttl = 0 // prepare goodbye message
 | |
|     })
 | |
|     return records
 | |
|   }), 1)
 | |
| 
 | |
|   if (records.length === 0) return cb && cb()
 | |
| 
 | |
|   server.unregister(records)
 | |
| 
 | |
|   // send goodbye message
 | |
|   server.mdns.respond(records, function () {
 | |
|     services.forEach(function (service) {
 | |
|       service.published = false
 | |
|     })
 | |
|     if (cb) cb.apply(null, arguments)
 | |
|   })
 | |
| }
 |