@homebridge/dbus-native
Version:
D-bus protocol implementation in native javascript
452 lines (414 loc) • 14.1 kB
JavaScript
const EventEmitter = require('events').EventEmitter;
const constants = require('./constants');
const parseSignature = require('./signature');
const stdDbusIfaces = require('./stdifaces');
const introspect = require('./introspect').introspectBus;
module.exports = function bus(conn, opts) {
if (!(this instanceof bus)) {
return new bus(conn, opts);
}
if (!opts) opts = {};
var self = this;
this.connection = conn;
this.serial = 1;
this.cookies = {}; // TODO: rename to methodReturnHandlers
this.methodCallHandlers = {};
this.signals = new EventEmitter();
this.exportedObjects = Object.create(null);
this.invoke = function (msg, callback) {
if (!msg.type) msg.type = constants.messageType.methodCall;
msg.serial = self.serial++;
this.cookies[msg.serial] = callback;
self.connection.message(msg);
};
this.invokeDbus = function (msg, callback) {
if (!msg.path) msg.path = '/org/freedesktop/DBus';
if (!msg.destination) msg.destination = 'org.freedesktop.DBus';
if (!msg['interface']) msg['interface'] = 'org.freedesktop.DBus';
self.invoke(msg, callback);
};
this.mangle = function (path, iface, member) {
var obj = {};
if (typeof path === 'object') {
// handle one argumant case mangle(msg)
obj.path = path.path;
obj['interface'] = path['interface'];
obj.member = path.member;
} else {
obj.path = path;
obj['interface'] = iface;
obj.member = member;
}
return JSON.stringify(obj);
};
this.sendSignal = function (path, iface, name, signature, args) {
var signalMsg = {
type: constants.messageType.signal,
serial: self.serial++,
interface: iface,
path: path,
member: name
};
if (signature) {
signalMsg.signature = signature;
signalMsg.body = args;
}
self.connection.message(signalMsg);
};
// Warning: errorName must respect the same rules as interface names (must contain a dot)
this.sendError = function (msg, errorName, errorText) {
var reply = {
type: constants.messageType.error,
serial: self.serial++,
replySerial: msg.serial,
destination: msg.sender,
errorName: errorName,
signature: 's',
body: [errorText]
};
this.connection.message(reply);
};
this.sendReply = function (msg, signature, body) {
var reply = {
type: constants.messageType.methodReturn,
serial: self.serial++,
replySerial: msg.serial,
destination: msg.sender,
signature: signature,
body: body
};
this.connection.message(reply);
};
// route reply/error
this.connection.on('message', function (msg) {
function invoke(impl, func, resultSignature) {
Promise.resolve()
.then(function () {
return func.apply(impl, (msg.body || []).concat(msg));
})
.then(
function (methodReturnResult) {
// a returned Error instance is treated as a DBus error reply
// (matches examples/return-types.js documented usage)
if (methodReturnResult instanceof Error) {
return self.sendError(
msg,
methodReturnResult.dbusName ||
'org.freedesktop.DBus.Error.Failed',
methodReturnResult.message || ''
);
}
var methodReturnReply = {
type: constants.messageType.methodReturn,
serial: self.serial++,
destination: msg.sender,
replySerial: msg.serial
};
try {
if (
resultSignature &&
methodReturnResult !== undefined &&
methodReturnResult !== null
) {
methodReturnReply.signature = resultSignature;
// If signature has multiple top-level types and the result
// is an array with the correct length, use it directly;
// otherwise wrap the result in an array so it is treated as
// a single return value.
var isMultipleReturns = false;
if (Array.isArray(methodReturnResult)) {
var typeCount = parseSignature(resultSignature).length;
isMultipleReturns =
typeCount > 1 && methodReturnResult.length === typeCount;
}
methodReturnReply.body = isMultipleReturns
? methodReturnResult
: [methodReturnResult];
}
self.connection.message(methodReturnReply);
} catch (e) {
self.sendError(
msg,
e.dbusName || 'org.freedesktop.DBus.Error.Failed',
e.message || ''
);
}
},
function (e) {
self.sendError(
msg,
e.dbusName || 'org.freedesktop.DBus.Error.Failed',
e.message || ''
);
}
);
}
var handler;
if (
msg.type === constants.messageType.methodReturn ||
msg.type === constants.messageType.error
) {
handler = self.cookies[msg.replySerial];
if (handler) {
delete self.cookies[msg.replySerial];
var props = {
connection: self.connection,
bus: self,
message: msg,
signature: msg.signature
};
var args = msg.body || [];
if (msg.type === constants.messageType.methodReturn) {
args = [null].concat(args); // first argument - no errors, null
handler.apply(props, args); // body as array of arguments
} else {
// err.message is conventionally a string; full body is exposed via err.body
handler.call(props, {
name: msg.errorName,
message: typeof args[0] === 'string' ? args[0] : '',
body: args
});
}
}
} else if (msg.type === constants.messageType.signal) {
self.signals.emit(self.mangle(msg), msg.body, msg.signature);
} else {
// methodCall
if (stdDbusIfaces(msg, self)) return;
// exported interfaces handlers
var obj, iface, impl;
if ((obj = self.exportedObjects[msg.path])) {
if ((iface = obj[msg['interface']])) {
// now we are ready to serve msg.member
impl = iface[1];
var func = impl[msg.member];
if (!func) {
self.sendError(
msg,
'org.freedesktop.DBus.Error.UnknownMethod',
`Method "${msg.member}" on interface "${msg.interface}" doesn't exist`
);
return;
}
// TODO safety check here
var resultSignature = iface[0].methods[msg.member][1];
invoke(impl, func, resultSignature);
return;
} else {
self.sendError(
msg,
'org.freedesktop.DBus.Error.UnknownInterface',
`Interface "${msg['interface']}" on object "${msg.path}" doesn't exist`
);
return;
}
}
// setMethodCall handlers
handler = self.methodCallHandlers[self.mangle(msg)];
if (handler) {
invoke(null, handler[0], handler[1]);
} else {
self.sendError(
msg,
'org.freedesktop.DBus.Error.UnknownService',
'Uh oh oh'
);
}
}
});
this.setMethodCallHandler = function (objectPath, iface, member, handler) {
var key = self.mangle(objectPath, iface, member);
self.methodCallHandlers[key] = handler;
};
this.exportInterface = function (obj, path, iface) {
var entry;
if (!self.exportedObjects[path]) {
entry = self.exportedObjects[path] = Object.create(null);
} else {
entry = self.exportedObjects[path];
}
entry[iface.name] = [iface, obj];
// monkey-patch obj.emit() so emitted events are also sent as DBus signals.
// Multiple exportInterface calls on the same `obj` share a single wrapper;
// we accumulate (iface, path) pairs on an internal list so the wrapper
// dispatches the signal to every registered interface that declares it.
if (typeof obj.emit === 'function') {
if (!obj.__dbusInterfaces) {
obj.__dbusInterfaces = [];
var oldEmit = obj.emit;
obj.emit = function () {
var args = Array.prototype.slice.apply(arguments);
var signalName = args[0];
if (!signalName) throw new Error('Trying to emit undefined signal');
for (var i = 0; i < obj.__dbusInterfaces.length; i++) {
var entry = obj.__dbusInterfaces[i];
var entryIface = entry.iface;
if (entryIface.signals && entryIface.signals[signalName]) {
var signal = entryIface.signals[signalName];
var signalMsg = {
type: constants.messageType.signal,
serial: self.serial++,
interface: entryIface.name,
path: entry.path,
member: signalName
};
if (signal[0]) {
signalMsg.signature = signal[0];
signalMsg.body = args.slice(1);
}
self.connection.message(signalMsg);
}
}
// note that local emit is likely to be called before signal arrives
// to remote subscriber
oldEmit.apply(obj, args);
};
}
// Replace any existing entry with the same (iface.name, path) pair so
// that re-exporting an object with an updated iface descriptor doesn't
// dispatch duplicate signals from a stale entry.
var existingIdx = -1;
for (var j = 0; j < obj.__dbusInterfaces.length; j++) {
var registered = obj.__dbusInterfaces[j];
if (registered.path === path && registered.iface.name === iface.name) {
existingIdx = j;
break;
}
}
if (existingIdx >= 0) {
obj.__dbusInterfaces[existingIdx] = { iface: iface, path: path };
} else {
obj.__dbusInterfaces.push({ iface: iface, path: path });
}
}
// TODO: emit ObjectManager's InterfaceAdded
};
// register name
if (opts.direct !== true) {
this.invokeDbus({ member: 'Hello' }, function (err, name) {
if (err)
throw new Error((err.name || 'Error') + ': ' + (err.message || err));
self.name = name;
});
} else {
self.name = null;
}
function DBusObject(name, service) {
this.name = name;
this.service = service;
this.as = function (name) {
return this.proxy[name];
};
}
function DBusService(name, bus) {
this.name = name;
this.bus = bus;
this.getObject = function (name, callback) {
if (name === undefined)
return callback(new Error('Object name is null or undefined'));
var obj = new DBusObject(name, this);
introspect(obj, function (err, ifaces, nodes) {
if (err) return callback(err);
obj.proxy = ifaces;
obj.nodes = nodes;
callback(null, obj);
});
};
this.getInterface = function (objName, ifaceName, callback) {
this.getObject(objName, function (err, obj) {
if (err) return callback(err);
callback(null, obj.as(ifaceName));
});
};
}
this.getService = function (name) {
return new DBusService(name, this);
};
this.getObject = function (path, name, callback) {
var service = this.getService(path);
return service.getObject(name, callback);
};
this.getInterface = function (path, objname, name, callback) {
return this.getObject(path, objname, function (err, obj) {
if (err) return callback(err);
callback(null, obj.as(name));
});
};
// TODO: refactor
// bus meta functions
this.addMatch = function (match, callback) {
this.invokeDbus(
{ member: 'AddMatch', signature: 's', body: [match] },
callback
);
};
this.removeMatch = function (match, callback) {
this.invokeDbus(
{ member: 'RemoveMatch', signature: 's', body: [match] },
callback
);
};
this.getId = function (callback) {
this.invokeDbus({ member: 'GetId' }, callback);
};
this.requestName = function (name, flags, callback) {
this.invokeDbus(
{ member: 'RequestName', signature: 'su', body: [name, flags] },
function (err, name) {
if (callback) callback(err, name);
}
);
};
this.releaseName = function (name, callback) {
this.invokeDbus(
{ member: 'ReleaseName', signature: 's', body: [name] },
callback
);
};
this.listNames = function (callback) {
this.invokeDbus({ member: 'ListNames' }, callback);
};
this.listActivatableNames = function (callback) {
this.invokeDbus({ member: 'ListActivatableNames' }, callback);
};
this.updateActivationEnvironment = function (env, callback) {
this.invokeDbus(
{
member: 'UpdateActivationEnvironment',
signature: 'a{ss}',
body: [env]
},
callback
);
};
this.startServiceByName = function (name, flags, callback) {
this.invokeDbus(
{ member: 'StartServiceByName', signature: 'su', body: [name, flags] },
callback
);
};
this.getConnectionUnixUser = function (name, callback) {
this.invokeDbus(
{ member: 'GetConnectionUnixUser', signature: 's', body: [name] },
callback
);
};
this.getConnectionUnixProcessId = function (name, callback) {
this.invokeDbus(
{ member: 'GetConnectionUnixProcessID', signature: 's', body: [name] },
callback
);
};
this.getNameOwner = function (name, callback) {
this.invokeDbus(
{ member: 'GetNameOwner', signature: 's', body: [name] },
callback
);
};
this.nameHasOwner = function (name, callback) {
this.invokeDbus(
{ member: 'NameHasOwner', signature: 's', body: [name] },
callback
);
};
};