Experiments toward using the `bonjour` npm package instead of avahi

This commit is contained in:
Tony Garnock-Jones 2018-12-12 20:27:10 +00:00
parent 4fb9a066b9
commit 21a1773cf3
3 changed files with 87 additions and 132 deletions

View File

@ -20,6 +20,8 @@
},
"dependencies": {
"@syndicate-lang/core": "^0.0.19",
"@syndicate-lang/driver-streams-node": "^0.0.1"
"@syndicate-lang/driver-streams-node": "^0.0.1",
"@syndicate-lang/driver-timer": "^0.0.20",
"bonjour": "^3.5.0"
}
}

View File

@ -18,16 +18,17 @@
// TODO: support other than the default (usually ".local") domain.
const { Observe, currentFacet, genUuid } = require("@syndicate-lang/core");
const { Observe, currentFacet, genUuid, Dataspace } = require("@syndicate-lang/core");
const S = activate require("@syndicate-lang/driver-streams-node");
const { TimeLaterThan } = activate require("@syndicate-lang/driver-timer");
const bonjour = require('bonjour')();
assertion type Service(name, serviceType) = Symbol.for("mdns-service");
assertion type Publish(svc, hostName, port, txtDataRecords) = Symbol.for("mdns-publish");
assertion type Published(svc, hostName, port, txtDataRecords) = Symbol.for("mdns-published");
assertion type Discovered(svc, hostName, port, txtDataRecords, address, family, interfaceName) = Symbol.for("mdns-discovered");
assertion type Publish(svc, hostName, port, txtData) = Symbol.for("mdns-publish");
assertion type Published(svc, hostName, port, txtData) = Symbol.for("mdns-published");
assertion type Discovered(svc, hostName, port, txtData, address) = Symbol.for("mdns-discovered");
// ^ Avahi gave us "family" and "interfaceName", too, but `bonjour` doesn't.
// TODO: nested dataspace to scope these??
message type BrowserInput(subprocessId, fields) = Symbol.for("-mdns-browser-input");
assertion type WildcardBrowserActive() = Symbol.for("-mdns-wildcard-browser-active");
export {
@ -37,137 +38,84 @@ export {
Discovered,
};
function unescapeLabel(str) {
// Per avahi's avahi-common/domain.c's avahi_escape_label:
return str.replace(
/\\(\d\d\d|\.|\\)/g, // that is, \NNN in decimal or \. or \\
function (x) {
if (x.length === 4) return String.fromCharCode(Number(x.slice(1)));
// else x.length === 2
return x[1];
});
function parseServiceType(serviceType) {
const splitType = /_([^_]+)\._([^_]+)/.exec(serviceType);
if (!splitType) {
throw new Error('Cannot parse serviceType + ' + JSON.stringify(serviceType))
}
return [splitType[1], splitType[2]];
}
spawn named 'driver/avahi-publish' {
during Observe(Published($svc, $hostName, $port, $txtDataRecords)) {
assert Publish(svc, hostName, port, txtDataRecords);
spawn named 'driver/mdns-publish' {
during Observe(Published($svc, $hostName, $port, $txtData)) {
assert Publish(svc, hostName, port, txtData);
}
during Publish($svc(Service($name, $serviceType)), $hostName, $port, $txtDataRecords) {
const topFacet = currentFacet();
// TODO: The `bonjour` module is limited to publishing a single TXT
// record with key-value strings. It'd be nice to support the full
// range of TXT content allowed by the mDNS and DNS RFCs, including
// non-key-value-formatted data and multiple TXT records.
during Publish($svc(Service($name, $serviceType)), $hostName, $port, $txtData)
spawn named ['bonjour', svc] {
const options = {};
options.name = name;
if (hostName !== null) options.host = hostName;
options.port = port;
[options.type, options.protocol] = parseServiceType(serviceType);
options.txt = txtData.toJS();
console.log('publish', options);
const service = bonjour.publish(options);
on stop service.stop();
const args = ['-f', '-s'];
if (hostName !== null) args.push('-H', hostName);
args.push(name, serviceType, port.toString());
txtDataRecords.forEach((txt) => args.push(txt));
field this.established = false;
assert Published(svc, hostName, port, txtData) when (this.established);
const id = genUuid('avahi-publish');
assert S.Subprocess(id, 'avahi-publish', args, {stdio: ['ignore', 'ignore', 'pipe']});
stop on message S.SubprocessError(id, $err) {
console.error("Couldn't start avahi-publish", err);
}
stop on asserted S.SubprocessExit(id, $code, _) {
console.error("Subprocess avahi-publish terminated with code", code);
}
on asserted S.SubprocessRunning(id, _, [_, _, $stderr]) {
react {
field this.established = false;
assert Published(svc, hostName, port, txtDataRecords) when (this.established);
on retracted S.Readable(stderr) topFacet.stop();
on message S.Line(stderr, $line) {
line = line.toString('utf-8');
if (line.startsWith('Established')) {
this.established = true;
} else if (line.startsWith('Disconnected')) {
this.established = false;
} else if (line.startsWith('Got SIG')) {
// e.g. "Got SIGTERM, quitting."; ignore.
} else {
console.log('avahi-publish', name+':', line);
}
}
}
}
service.on('up', Dataspace.wrapExternal(() => { this.established = true; }));
service.on('error', Dataspace.wrapExternal((err) => { throw err; }));
}
}
spawn named 'driver/avahi-browse' {
during Observe(Discovered(Service(_, $serviceType), _, _, _, _, _, _)) {
const topFacet = currentFacet();
const args = ['-f', '-r', '-k', '-p'];
spawn named 'driver/mdns-browse' {
during Observe(Discovered(Service(_, $serviceType), _, _, _, _)) {
const options = {};
if (typeof serviceType === 'string') {
args.push(serviceType);
} else {
args.push('-a');
[options.type, options.protocol] = parseServiceType(serviceType);
}
const browser = bonjour.find(options);
on stop browser.stop();
// field this.nextUpdate = +(new Date());
// on asserted TimeLaterThan(this.nextUpdate) {
// console.log('updating');
// browser.update();
// }
const eachRecord = (service, cb) => {
const svc = Service(service.name, '_' + service.type + '._' + service.protocol);
for (const addr of service.addresses) {
cb(Discovered(svc, service.host, service.port, service.txt, addr));
}
// const now = +(new Date());
// if (this.nextUpdate < now) {
// this.nextUpdate = now + 200;
// }
};
browser.on('up', Dataspace.wrapExternal((service) => {
eachRecord(service, (a) => {
currentFacet().actor.adhocAssert(a)
});
}));
browser.on('down', Dataspace.wrapExternal((service) => {
eachRecord(service, (a) => {
currentFacet().actor.adhocRetract(a)
});
}));
if (typeof serviceType !== 'string') {
assert WildcardBrowserActive();
} else {
stop on asserted WildcardBrowserActive();
}
const id = genUuid('avahi-browse');
assert S.Subprocess(id, 'avahi-browse', args, {stdio: ['ignore', 'pipe', 'ignore']});
stop on message S.SubprocessError(id, $err) {
console.error("Couldn't start avahi-browse", err);
}
stop on asserted S.SubprocessExit(id, $code, _) {
if (code !== 0) {
console.error("Subprocess avahi-browse terminated with code", code);
}
}
on asserted S.SubprocessRunning(id, _, [_, $stdout, _]) {
react {
on retracted S.Readable(stdout) topFacet.stop();
on message S.Line(stdout, $line) {
// Parsing of TXT record data (appearing after the port
// number in an '=' record) is unreliable given the way
// avahi-browse formats it.
//
// See https://github.com/lathiat/avahi/pull/206.
//
// However, it's still useful to have, so we do our best!
//
const pieces = line.toString('utf-8').split(/;/);
if (pieces[0] === '=') {
// A resolved address record, which has TXT data.
const normalFields = pieces.slice(0, 9);
const txtFields = pieces.slice(9).join(';'); // it's these that are dodgy
if (txtFields === '') {
normalFields.push([]);
} else {
normalFields.push(txtFields.slice(1,-1).split(/" "/)); // OMG this is vile
}
send BrowserInput(id, normalFields);
} else {
// Something else.
send BrowserInput(id, pieces);
}
}
on message BrowserInput(id, ["+", $interfaceName, $family, $name, $serviceType, $domain]) {
react {
const svc = Service(unescapeLabel(name), serviceType);
stop on message BrowserInput(
id, ["-", interfaceName, family, name, serviceType, domain]);
on message BrowserInput(
id, ['=', interfaceName, family, name, serviceType, domain,
$hostName, $address, $portStr, $txtDataRecords])
{
const port0 = Number(portStr);
const port = Number.isNaN(port0) ? null : port0;
react assert Discovered(
svc, hostName, port, txtDataRecords, address, family, interfaceName);
}
}
}
}
}
}
}

View File

@ -3,25 +3,30 @@ const S = activate require("@syndicate-lang/driver-streams-node");
const M = activate require("@syndicate-lang/driver-mdns");
spawn named 'test' {
const svc = M.Service((new Date()).toJSON(), '_syndicate._tcp');
assert M.Publish(svc, null, 8001, []);
const label = (new Date()).toJSON().replace(/\./g, '-');
// ^ I declare defeat. None of the underlying DNS and mDNS support
// libraries support fully-general DNS labels. In particular, labels
// with dots in them cause trouble. I tried to fix the underlying
// problem, but the changes required are just too much. So instead,
// I'm avoiding a perfectly reasonable use of DNS. This is why we
// can't have nice things.
const svc = M.Service(label, '_syndicate._tcp');
assert M.Publish(svc, null, 8001, {});
during M.Discovered(M.Service($name, '_syndicate._tcp'),
$hostName,
$port,
$txtDataRecords,
$address,
"IPv4",
$interfaceName)
$address)
{
on start console.log('+', name, hostName, port, txtDataRecords, address, interfaceName);
on stop console.log('-', name, hostName, port, txtDataRecords, address, interfaceName);
on start console.log('+', name, hostName, port, txtDataRecords, address);
on stop console.log('-', name, hostName, port, txtDataRecords, address);
}
during M.Discovered(M.Service($n, $t), $h, $p, $d, $a, "IPv4", $i) {
during M.Discovered(M.Service($n, $t), $h, $p, $d, $a) {
if (t !== '_syndicate._tcp') {
on start console.log('**', t, n, h, p, d, a, i);
on stop console.log('==', t, n, h, p, d, a, i);
on start console.log('**', t, n, h, p, d, a);
on stop console.log('==', t, n, h, p, d, a);
}
}
}