@homebridge/dbus-native
Version:
D-bus protocol implementation in native javascript
383 lines (346 loc) • 11 kB
JavaScript
const EventEmitter = require('events').EventEmitter;
const constants = require('./constants');
const stdDbusIfaces = require('./stdifaces');
const introspect = require('./introspect').introspectBus;
module.exports = function bus(conn, opts) {
if (!(this instanceof bus)) {
return new bus(conn);
}
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 = {};
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) {
var methodReturnReply = {
type: constants.messageType.methodReturn,
serial: self.serial++,
destination: msg.sender,
replySerial: msg.serial
};
if (methodReturnResult !== null) {
methodReturnReply.signature = resultSignature;
methodReturnReply.body = [methodReturnResult];
}
self.connection.message(methodReturnReply);
},
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 {
handler.call(props, { name: msg.errorName, message: args }); // body as first argument
}
}
} 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 {
console.error(`Interface ${msg['interface']} is not supported`);
// TODO: respond with standard dbus error
}
}
// 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] = {};
} else {
entry = self.exportedObjects[path];
}
entry[iface.name] = [iface, obj];
// monkey-patch obj.emit()
if (typeof obj.emit === 'function') {
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 signa');
//send signal to bus
var signal;
if (iface.signals && iface.signals[signalName]) {
signal = iface.signals[signalName];
var signalMsg = {
type: constants.messageType.signal,
serial: self.serial++,
interface: iface.name,
path: path,
member: signalName
};
if (signal[0]) {
signalMsg.signature = signal[0];
signalMsg.body = args.slice(1);
}
self.connection.message(signalMsg);
self.serial++;
}
// note that local emit is likely to be called before signal arrives
// to remote subscriber
oldEmit.apply(obj, args);
};
}
// TODO: emit ObjectManager's InterfaceAdded
};
// register name
if (opts.direct !== true) {
this.invokeDbus({ member: 'Hello' }, function (err, name) {
if (err) throw new Error(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
);
};
};