registry.js
5.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
'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)
})
}