@quadratclown/dbus-next
Version:
The next great DBus library for Node
228 lines (195 loc) • 6.69 kB
JavaScript
const EventEmitter = require('events');
const {
isInterfaceNameValid,
isMemberNameValid
} = require('../validators');
class ProxyListener {
constructor (signal, iface) {
this.refcount = 0;
this.fn = (msg) => {
const { body, signature, sender } = msg;
if (iface.$object.bus._nameOwners[iface.$object.name] !== sender) {
return;
}
if (signature !== signal.signature) {
console.error(`warning: got signature ${signature} for signal ${msg.interface}.${signal.name} (expected ${signal.signature})`);
return;
}
iface.emit.apply(iface, [signal.name].concat(body));
};
}
}
/**
* A class to represent a proxy to an interface exported on the bus to be used
* by a client. A `ProxyInterface` is gotten by interface name from the {@link
* ProxyObject} from the {@link MessageBus}. This class is constructed
* dynamically based on the introspection data on the bus. The advertised
* methods of the interface are exposed as class methods that take arguments
* and return a Promsie that resolves to types specified by the type signature
* of the DBus method. The `ProxyInterface` is an `EventEmitter` that emits
* events with types that are specified by the type signature of the DBus
* signal advertised on the bus when that signal is received.
*
* If an interface method call returns an error, `ProxyInterface` method call
* will throw a {@link DBusError}.
*
* @example
* // this demonstrates the use of the standard
* // `org.freedesktop.DBus.Properties` interface for an interface that exports
* // some properties.
* let bus = dbus.sessionBus();
* let obj = await bus.getProxyObject('org.test.bus_name', '/org/test/path');
* let properties = obj.getInterface('org.freedesktop.DBus.Properties');
* // the `Get` method provided by this interface takes two strings and returns
* // a Variant
* let someProperty = await properties.Get('org.test.interface_name', 'SomeProperty');
* // the `PropertiesChanged` signal provided by this interface will emit an
* // event on the interface with its specified signal arguments.
* properties.on('PropertiesChanged', (props, invalidated) => {});
*/
class ProxyInterface extends EventEmitter {
/**
* Create a new `ProxyInterface`. This constructor should not be called
* directly. Use {@link ProxyObject#getInterface} to get a proxy interface.
*/
constructor (name, object) {
super();
this.$name = name;
this.$object = object;
this.$properties = [];
this.$methods = [];
this.$signals = [];
this.$listeners = {};
const getEventDetails = (eventName) => {
const signal = this.$signals.find((s) => s.name === eventName);
if (!signal) {
return [null, null];
}
const detailedEvent = JSON.stringify({
path: this.$object.path,
interface: this.$name,
member: eventName
});
return [signal, detailedEvent];
};
this.on('removeListener', (eventName, listener) => {
const [signal, detailedEvent] = getEventDetails(eventName);
if (!signal) {
return;
}
const proxyListener = this._getEventListener(signal);
if (proxyListener.refcount <= 0) {
return;
}
proxyListener.refcount -= 1;
if (proxyListener.refcount > 0) {
return;
}
this.$object.bus._removeMatch(this._signalMatchRuleString(eventName))
.catch(error => {
this.$object.bus.emit('error', error);
});
this.$object.bus._signals.removeListener(detailedEvent, proxyListener.fn);
});
this.on('newListener', (eventName, listener) => {
const [signal, detailedEvent] = getEventDetails(eventName);
if (!signal) {
return;
}
const proxyListener = this._getEventListener(signal);
if (proxyListener.refcount > 0) {
proxyListener.refcount += 1;
return;
}
proxyListener.refcount = 1;
this.$object.bus._addMatch(this._signalMatchRuleString(eventName))
.catch(error => {
this.$object.bus.emit('error', error);
});
this.$object.bus._signals.on(detailedEvent, proxyListener.fn);
});
}
_signalMatchRuleString (eventName) {
return `type='signal',sender='${this.$object.name}',interface='${this.$name}',path='${this.$object.path}',member='${eventName}'`;
}
_getEventListener (signal) {
if (this.$listeners[signal.name]) {
return this.$listeners[signal.name];
}
this.$listeners[signal.name] = new ProxyListener(signal, this);
return this.$listeners[signal.name];
}
static _fromXml (object, xml) {
if (!('$' in xml) || !isInterfaceNameValid(xml.$.name)) {
return null;
}
const name = xml.$.name;
const iface = new ProxyInterface(name, object);
if (Array.isArray(xml.property)) {
for (const p of xml.property) {
// TODO validation
if ('$' in p) {
iface.$properties.push(p.$);
}
}
}
if (Array.isArray(xml.signal)) {
for (const s of xml.signal) {
if (!('$' in s) || !isMemberNameValid(s.$.name)) {
continue;
}
const signal = {
name: s.$.name,
signature: ''
};
if (Array.isArray(s.arg)) {
for (const a of s.arg) {
if ('$' in a && 'type' in a.$) {
// TODO signature validation
signal.signature += a.$.type;
}
}
}
iface.$signals.push(signal);
}
}
if (Array.isArray(xml.method)) {
for (const m of xml.method) {
if (!('$' in m) || !isMemberNameValid(m.$.name)) {
continue;
}
const method = {
name: m.$.name,
inSignature: '',
outSignature: ''
};
if (Array.isArray(m.arg)) {
for (const a of m.arg) {
if (!('$' in a) || typeof a.$.type !== 'string') {
continue;
}
const arg = a.$;
if (arg.direction === 'in') {
method.inSignature += arg.type;
} else if (arg.direction === 'out') {
method.outSignature += arg.type;
}
}
}
// TODO signature validation
iface.$methods.push(method);
iface[method.name] = function (...args) {
const objArgs = [
name,
method.name,
method.inSignature,
method.outSignature
].concat(args);
return object._callMethod.apply(object, objArgs);
};
}
}
return iface;
}
}
module.exports = ProxyInterface;