@homebridge/dbus-native
Version:
D-bus protocol implementation in native javascript
216 lines (208 loc) • 7.4 kB
JavaScript
const xml2js = require('xml2js');
module.exports.introspectBus = function (obj, callback) {
var bus = obj.service.bus;
bus.invoke(
{
destination: obj.service.name,
path: obj.name,
interface: 'org.freedesktop.DBus.Introspectable',
member: 'Introspect'
},
function (err, xml) {
module.exports.processXML(err, xml, obj, callback);
}
);
};
module.exports.processXML = function (err, xml, obj, callback) {
if (err) return callback(err);
var parser = new xml2js.Parser();
parser.parseString(xml, function (err, result) {
if (err) return callback(err);
if (!result.node) throw new Error('No root XML node');
result = result.node; // unwrap the root node
// If no interface, try first sub node?
if (!result.interface) {
if (result.node && result.node.length > 0 && result.node[0]['$']) {
var subObj = Object.assign(obj, {});
if (subObj.name.slice(-1) !== '/') subObj.name += '/';
subObj.name += result.node[0]['$'].name;
return module.exports.introspectBus(subObj, callback);
}
return callback(new Error('No such interface found'));
}
var proxy = {};
var nodes = [];
var ifaceName, method, property, iface, arg, signature, currentIface;
var ifaces = result['interface'];
var xmlnodes = result['node'] || [];
for (var n = 1; n < xmlnodes.length; ++n) {
// Start at 1 because we want to skip the root node
nodes.push(xmlnodes[n]['$']['name']);
}
for (var i = 0; i < ifaces.length; ++i) {
iface = ifaces[i];
ifaceName = iface['$'].name;
currentIface = proxy[ifaceName] = new DBusInterface(obj, ifaceName);
for (var m = 0; iface.method && m < iface.method.length; ++m) {
method = iface.method[m];
signature = '';
var methodName = method['$'].name;
for (var a = 0; method.arg && a < method.arg.length; ++a) {
arg = method.arg[a]['$'];
if (arg.direction === 'in') signature += arg.type;
}
// add method
currentIface.$createMethod(methodName, signature);
}
for (var p = 0; iface.property && p < iface.property.length; ++p) {
property = iface.property[p];
currentIface.$createProp(
property['$'].name,
property['$'].type,
property['$'].access
);
}
// TODO: introspect signals
}
callback(null, proxy, nodes);
});
};
function DBusInterface(parent_obj, ifname) {
// Since methods and props presently get added directly to the object, to avoid collision with existing names we must use $ naming convention as $ is invalid for dbus member names
// https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names
this.$parent = parent_obj; // parent DbusObject
this.$name = ifname; // string interface name
this.$methods = {}; // dictionary of methods (exposed for test), should we just store signature or use object to store more info?
//this.$signals = {};
this.$properties = {};
this.$callbacks = [];
this.$sigHandlers = [];
}
DBusInterface.prototype.$getSigHandler = function (callback) {
var index;
if ((index = this.$callbacks.indexOf(callback)) === -1) {
index = this.$callbacks.push(callback) - 1;
this.$sigHandlers[index] = function (messageBody) {
callback.apply(null, messageBody);
};
}
return this.$sigHandlers[index];
};
DBusInterface.prototype.addListener = DBusInterface.prototype.on = function (
signame,
callback
) {
// http://dbus.freedesktop.org/doc/api/html/group__DBusBus.html#ga4eb6401ba014da3dbe3dc4e2a8e5b3ef
// An example is "type='signal',sender='org.freedesktop.DBus', interface='org.freedesktop.DBus',member='Foo', path='/bar/foo',destination=':452345.34'" ...
var bus = this.$parent.service.bus;
var signalFullName = bus.mangle(this.$parent.name, this.$name, signame);
if (!bus.signals.listeners(signalFullName).length) {
// This is the first time, so call addMatch
var match = getMatchRule(this.$parent.name, this.$name, signame);
bus.addMatch(
match,
function (err) {
if (err) throw new Error(err);
bus.signals.on(signalFullName, this.$getSigHandler(callback));
}.bind(this)
);
} else {
// The match is already there, just add event listener
bus.signals.on(signalFullName, this.$getSigHandler(callback));
}
};
DBusInterface.prototype.removeListener = DBusInterface.prototype.off =
function (signame, callback) {
var bus = this.$parent.service.bus;
var signalFullName = bus.mangle(this.$parent.name, this.$name, signame);
bus.signals.removeListener(signalFullName, this.$getSigHandler(callback));
if (!bus.signals.listeners(signalFullName).length) {
// There is no event handlers for this match
var match = getMatchRule(this.$parent.name, this.$name, signame);
bus.removeMatch(
match,
function (err) {
if (err) throw new Error(err);
// Now it is safe to empty these arrays
this.$callbacks.length = 0;
this.$sigHandlers.length = 0;
}.bind(this)
);
}
};
DBusInterface.prototype.$createMethod = function (mName, signature) {
this.$methods[mName] = signature;
this[mName] = function () {
this.$callMethod(mName, arguments);
};
};
DBusInterface.prototype.$callMethod = function (mName, args) {
var bus = this.$parent.service.bus;
if (!Array.isArray(args)) args = Array.from(args); // Array.prototype.slice.apply(args)
var callback =
typeof args[args.length - 1] === 'function' ? args.pop() : function () {};
var msg = {
destination: this.$parent.service.name,
path: this.$parent.name,
interface: this.$name,
member: mName
};
if (this.$methods[mName] !== '') {
msg.signature = this.$methods[mName];
msg.body = args;
}
bus.invoke(msg, callback);
};
DBusInterface.prototype.$createProp = function (
propName,
propType,
propAccess
) {
this.$properties[propName] = { type: propType, access: propAccess };
Object.defineProperty(this, propName, {
enumerable: true,
get: () => (callback) => this.$readProp(propName, callback),
set: function (val) {
this.$writeProp(propName, val);
}
});
};
DBusInterface.prototype.$readProp = function (propName, callback) {
var bus = this.$parent.service.bus;
bus.invoke(
{
destination: this.$parent.service.name,
path: this.$parent.name,
interface: 'org.freedesktop.DBus.Properties',
member: 'Get',
signature: 'ss',
body: [this.$name, propName]
},
function (err, val) {
if (err) {
callback(err);
} else {
var signature = val[0];
if (signature.length === 1) {
callback(err, val[1][0]);
} else {
callback(err, val[1]);
}
}
}
);
};
DBusInterface.prototype.$writeProp = function (propName, val) {
var bus = this.$parent.service.bus;
bus.invoke({
destination: this.$parent.service.name,
path: this.$parent.name,
interface: 'org.freedesktop.DBus.Properties',
member: 'Set',
signature: 'ssv',
body: [this.$name, propName, [this.$properties[propName].type, val]]
});
};
function getMatchRule(objName, ifName, signame) {
return `type='signal',path='${objName}',interface='${ifName}',member='${signame}'`;
}