123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- '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)
- })
- }
|