dnssd
Version:
Bonjour/Avahi-like service discovery in pure JavaScript
261 lines (207 loc) • 7.82 kB
JavaScript
'use strict';
var misc = require('./misc');
var ServiceType = require('./ServiceType');
var EventEmitter = require('./EventEmitter');
var ServiceResolver = require('./ServiceResolver');
var NetworkInterface = require('./NetworkInterface');
var Query = require('./Query');
var filename = require('path').basename(__filename);
var debug = require('./debug')('dnssd:' + filename);
var RType = require('./constants').RType;
var STATE = { STOPPED: 'stopped', STARTED: 'started' };
/**
* Creates a new Browser
*
* @emits 'serviceUp'
* @emits 'serviceChanged'
* @emits 'serviceDown'
* @emits 'error'
*
* @param {ServiceType|Object|String|Array} type - the service to browse
* @param {Object} [options]
*/
function Browser(type) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (!(this instanceof Browser)) return new Browser(type, options);
EventEmitter.call(this);
// convert argument ServiceType to validate it (might throw)
var serviceType = type instanceof ServiceType ? type : new ServiceType(type);
// can't search for multiple subtypes at the same time
if (serviceType.subtypes.length > 1) {
throw new Error('Too many subtypes. Can only browse one at a time.');
}
this._id = serviceType.toString();
debug('Creating new browser for "' + this._id + '"');
this._resolvers = {}; // active service resolvers (when browsing services)
this._serviceTypes = {}; // active service types (when browsing service types)
this._protocol = serviceType.protocol;
this._serviceName = serviceType.name;
this._subtype = serviceType.subtypes[0];
this._isWildcard = serviceType.isEnumerator;
this._domain = options.domain || 'local.';
this._maintain = 'maintain' in options ? options.maintain : true;
this._resolve = 'resolve' in options ? options.resolve : true;
this._interface = NetworkInterface.get(options.interface);
this._state = STATE.STOPPED;
// emitter used to stop child queries instead of holding onto a reference
// for each one
this._offswitch = new EventEmitter();
}
Browser.prototype = Object.create(EventEmitter.prototype);
Browser.prototype.constructor = Browser;
/**
* Starts browser
* @return {this}
*/
Browser.prototype.start = function () {
var _this = this;
if (this._state === STATE.STARTED) {
debug('Browser already started!');
return this;
}
debug('Starting browser for "' + this._id + '"');
this._state = STATE.STARTED;
// listen for fatal errors on interface
this._interface.using(this).once('error', this._onError);
this._interface.bind().then(function () {
return _this._startQuery();
}).catch(function (err) {
return _this._onError(err);
});
return this;
};
/**
* Stops browser.
*
* Browser shutdown has to:
* - shut down all child service resolvers (they're no longer needed)
* - stop the ongoing browsing queries on all interfaces
* - remove all listeners since the browser is down
* - deregister from the interfaces so they can shut down if needed
*/
Browser.prototype.stop = function () {
debug('Stopping browser for "' + this._id + '"');
this._interface.removeListenersCreatedBy(this);
this._interface.stopUsing();
debug('Sending stop signal to active queries');
this._offswitch.emit('stop');
// because resolver.stop()'s will trigger serviceDown:
this.removeAllListeners('serviceDown');
Object.values(this._resolvers).forEach(function (resolver) {
return resolver.stop();
});
this._state = STATE.STOPPED;
this._resolvers = {};
this._serviceTypes = {};
};
/**
* Get a list of currently available services
* @return {Objects[]}
*/
Browser.prototype.list = function () {
// if browsing service types
if (this._isWildcard) {
return Object.values(this._serviceTypes);
}
return Object.values(this._resolvers).filter(function (resolver) {
return resolver.isResolved();
}).map(function (resolver) {
return resolver.service();
});
};
/**
* Error handler
* @emits 'error'
*/
Browser.prototype._onError = function (err) {
debug('Error on "' + this._id + '", shutting down. Got: \n' + err);
this.stop();
this.emit('error', err);
};
/**
* Starts the query for either services (like each available printer)
* or service types using enumerator (listing all mDNS service on a network).
* Queries are sent out on each network interface the browser uses.
*/
Browser.prototype._startQuery = function () {
var name = misc.fqdn(this._serviceName, this._protocol, this._domain);
if (this._subtype) name = misc.fqdn(this._subtype, '_sub', name);
var question = { name: name, qtype: RType.PTR };
var answerHandler = this._isWildcard ? this._addServiceType.bind(this) : this._addService.bind(this);
// start sending continuous, ongoing queries for services
new Query(this._interface, this._offswitch).add(question).on('answer', answerHandler).start();
};
/**
* Answer handler for service types. Adds type and alerts user.
*
* @emits 'serviceUp' with new service types
* @param {ResourceRecord} answer
*/
Browser.prototype._addServiceType = function (answer) {
var name = answer.PTRDName;
if (this._state === STATE.STOPPED) return debug.v('Already stopped, ignoring');
if (answer.ttl === 0) return debug.v('TTL=0, ignoring');
if (this._serviceTypes[name]) return debug.v('Already found, ignoring');
debug('Found new service type: "' + name + '"');
var _misc$parse = misc.parse(name),
service = _misc$parse.service,
protocol = _misc$parse.protocol;
// remove any leading underscores for users
service = service.replace(/^_/, '');
protocol = protocol.replace(/^_/, '');
var serviceType = { name: service, protocol: protocol };
this._serviceTypes[name] = serviceType;
this.emit('serviceUp', serviceType);
};
/**
* Answer handler for services.
*
* New found services cause a ServiceResolve to be created. The resolver
* parse the additionals and query out for an records needed to fully
* describe the service (hostname, IP, port, TXT).
*
* @emits 'serviceUp' when a new service is found
* @emits 'serviceChanged' when a resolved service changes data (IP, etc.)
* @emits 'serviceDown' when a resolved service goes down
*
* @param {ResourceRecord} answer - the record that has service data
* @param {ResourceRecord[]} [additionals] - other records that might be related
*/
Browser.prototype._addService = function (answer, additionals) {
var _this2 = this;
var name = answer.PTRDName;
if (this._state === STATE.STOPPED) return debug.v('Already stopped, ignoring');
if (answer.ttl === 0) return debug.v('TTL=0, ignoring');
if (this._resolvers[name]) return debug.v('Already found, ignoring');
debug('Found new service: "' + name + '"');
if (!this._resolve) {
this.emit('serviceUp', misc.parse(name).instance);
return;
}
var resolver = new ServiceResolver(name, this._interface);
this._resolvers[name] = resolver;
resolver.once('resolved', function () {
debug('Service up');
// - stop resolvers that dont need to be maintained
// - only emit 'serviceDown' events once services that have been resolved
if (!_this2._maintain) {
resolver.stop();
_this2._resolvers[name] = null;
} else {
resolver.once('down', function () {
return _this2.emit('serviceDown', resolver.service());
});
}
_this2.emit('serviceUp', resolver.service());
});
resolver.on('updated', function () {
debug('Service updated');
_this2.emit('serviceChanged', resolver.service());
});
resolver.once('down', function () {
debug('Service down');
delete _this2._resolvers[name];
});
resolver.start(additionals);
};
module.exports = Browser;