UNPKG

@quadratclown/dbus-next

Version:
497 lines (469 loc) 16.8 kB
/** * A module for exporting interfaces on a name on the message bus. * * @module interface */ const { parseSignature, collapseSignature } = require('../signature'); const variant = require('../variant'); const Variant = variant.Variant; /** * Used for [`Interface`]{@link module:interface~Interface} [property]{@link * module:interface.property} options to specify that clients have read access * to the property. * * @static */ const ACCESS_READ = 'read'; /** * Used for [`Interface`]{@link module:interface~Interface} [property]{@link * module:interface.property} options to specify that clients have write access * to the property. * * @static */ const ACCESS_WRITE = 'write'; /** * Used for [`Interface`]{@link module:interface~Interface} [property]{@link * module:interface.property} options to specify that clients have read and * write access to the property. * * @static */ const ACCESS_READWRITE = 'readwrite'; const EventEmitter = require('events'); const { assertInterfaceNameValid, assertMemberNameValid } = require('../validators'); /** * A decorator function to define an [`Interface`]{@link * module:interface~Interface} class member as a property. The property will * be gotten and set from the class when users call the standard DBus methods * `org.freedesktop.DBus.Properties.Get`, * `org.freedesktop.DBus.Properties.Set`, and * `org.freedesktop.DBus.Properties.GetAll`. The property getters and setters * may throw a {@link DBusError} with an error name and message to return the * error to the client. * @see {@link https://dbus.freedesktop.org/doc/dbus-specification.html#type-system} * * @static * * @example * class MyInterface extends Interface { * // uncomment below to use the decorator (jsdoc bug) * //@property({signature: 's'}) * get MyProp() { * return this.myProp; * } * set MyProp(value) { * this.myProp = value; * } * } * * @param {object} options - The options for this property. * @param {string} options.signature - The DBus type signature for this property. * @param {access} [options.access=ACCESS_READWRITE] - The read and write * access of the property for clients (effects `Get` and `Set` property methods). * @param {string} [options.name] - The name of this property on the bus. * Defaults to the name of the class member being decorated. * @param {bool} [options.disabled=false] - Whether or not this property * will be advertised on the bus. */ function property (options) { options.access = options.access || ACCESS_READWRITE; if (!options.signature) { throw new Error('missing signature for property'); } options.signatureTree = parseSignature(options.signature); return function (descriptor) { options.name = options.name || descriptor.key; assertMemberNameValid(options.name); descriptor.finisher = function (klass) { klass.prototype.$properties = klass.prototype.$properties || []; klass.prototype.$properties[descriptor.key] = options; }; return descriptor; }; } /** * A decorator function to define an [`Interface`]{@link * module:interface~Interface} class member as a method. The method will be * called when the client calls it on the bus with the given arguments with * types specified by the `inSignature` in the method options. The method * should return a result specified by the `outSignature` which will be * returned to the client over the message bus. If multiple output parameters * are specified in the `outSignature`, they should be returned within an * array. * * The method may also be `async` or return a `Promise` with the result and the * reply will be sent once the promise returns with a response body. * * The method may throw a {@link DBusError} with an error name and * message to return the error to the client. * @see {@link https://dbus.freedesktop.org/doc/dbus-specification.html#type-system} * * @static * * @example * // uncomment the decorators to use them (jsdoc bug) * * class MyInterface extends Interface { * //@method({inSignature: 's', outSignature: 's'}) * async Echo(what) { * return what; * } * * //@method({inSignature: 'ss', outSignature: 'vv'}) * ReturnsMultiple(what, what2) { * return [ * new Variant('s', what), * new Variant('s', what2) * ]; * } * * //@method({inSignature: '', outSignature: ''}) * ThrowsError() { * // the error is returned to the client * throw new DBusError('org.test.iface.Error', 'something went wrong'); * } * } * * @param {object} options - The options for this method. * @param {string} [options.inSignature=""] - The DBus type signature for the * input to this method. * @param {string} [options.outSignature=""] - The DBus type signature for the * output of this method. * @param {string} [options.name] - The name of this method on the bus. * Defaults to the name of the class member being decorated. * @param {bool} [options.disabled=false] - Whether or not this property * will be advertised on the bus. */ function method (options) { // TODO allow overriding of methods? // TODO introspect the names of the arguments for the function: // https://stackoverflow.com/questions/1007981/how-to-get-function-parameter-names-values-dynamically options.disabled = !!options.disabled; options.inSignature = options.inSignature || ''; options.outSignature = options.outSignature || ''; options.inSignatureTree = parseSignature(options.inSignature); options.outSignatureTree = parseSignature(options.outSignature); return function (descriptor) { options.name = options.name || descriptor.key; assertMemberNameValid(options.name); options.fn = descriptor.descriptor.value; descriptor.finisher = function (klass) { klass.prototype.$methods = klass.prototype.$methods || []; klass.prototype.$methods[descriptor.key] = options; }; return descriptor; }; } /** * A decorator function to define an [`Interface`]{@link * module:interface~Interface} class member as a signal. To emit the signal on * the bus to listeners, just call the decorated method and the signal will be * emitted with the returned value with types specified by the `signature` in * the signal options. If the signal has multiple output parameters, they * should be returned in an array. * @see {@link https://dbus.freedesktop.org/doc/dbus-specification.html#type-system} * * @static * * @example * // uncomment the decorators to use them (jsdoc bug) * class MyInterface extends Interface { * //@signal({signature: 's'}) * HelloWorld(value) { * return value; * } * * //@signal({signature: 'ss'}) * SignalMultiple(x) { * return [ * 'hello', * 'world' * ]; * } * } * * @param {object} options - The options for this property. * @param {string} options.signature - The DBus type signature for this signal. * @param {string} [options.name] - The name of this signal on the bus. * Defaults to the name of the class member being decorated. * @param {bool} [options.disabled=false] - Whether or not this property * will be advertised on the bus. */ function signal (options) { // TODO introspect the names of the arguments for the function: // https://stackoverflow.com/questions/1007981/how-to-get-function-parameter-names-values-dynamically options.signature = options.signature || ''; options.signatureTree = parseSignature(options.signature); return function (descriptor) { options.name = options.name || descriptor.key; assertMemberNameValid(options.name); options.fn = descriptor.descriptor.value; descriptor.descriptor.value = function () { if (options.disabled) { throw new Error('tried to call a disabled signal'); } const result = options.fn.apply(this, arguments); this.$emitter.emit('signal', options, result); }; descriptor.finisher = function (klass) { klass.prototype.$signals = klass.prototype.$signals || []; klass.prototype.$signals[descriptor.key] = options; }; return descriptor; }; } /** * The `Interface` is an abstract class used for defining and exporting an * interface on a DBus name. You can override this class to make your own DBus * interfaces. Use the decorators within this module to define the * [properties]{@link module:interface.property}, [methods]{@link * module:interface.method}, and [signals]{@link module:interface.signal} that * the interface has. These will be advertised to users in the introspection * xml gotten by the `org.freedesktop.DBus.Introspect` method on the name. See * the documentation for the decorators for more information. The constructor * of the `Interface` should call `super()` with the name of the interface that * will be exported. * * @example * class MyInterface extends Interface { * constructor() { * super('org.test.interface_name'); * } * // define properties, methods, and signals with decorated functions * } * let bus = dbus.sessionBus(); * let name = await bus.requestName('org.test.bus_name'); * let iface = new MyInterface(); * name.export('/org/test/path', iface); */ class Interface { /** * Create an interface. This should be called with the name of the interface * in the class that extends it. */ constructor (name) { assertInterfaceNameValid(name); this.$name = name; this.$emitter = new EventEmitter(); } /** * An alternative to the decorator functions to configure * [`Interface`]{@link module:interface~Interface} DBus members when * decorators cannot be supported. * * *Calling this method twice on the same `Interface` or mixing this method * with the decorator interface will result in undefined behavior that may be * specified at a future time.* * * @static * @example * ConfiguredInterface.configureMembers({ * properties: { * SomeProperty: { * signature: 's' * } * }, * methods: { * Echo: { * inSignature: 'v', * outSignature: 'v' * } * }, * signals: { * HelloWorld: { * signature: 'ss' * } * } * }); * * @param members {Object} - Member configuration object. * @param members.properties {Object} - The class methods to define as * properties. The key should be a method defined on the class and the value * should be the options for a [property]{@link module:interface.property} * decorator. * @param members.methods {Object} - The class methods to define as DBus * methods. The key should be a method defined on the class and the value * should be the options for a [method]{@link module:interface.method} * decorator. * @param members.signals {Object} - The class methods to define as signals. * The key should be a method defined on the class and hte value should be * options for a [signal]{@link module:interface.signal} decorator. */ static configureMembers (members) { const properties = members.properties || {}; const methods = members.methods || {}; const signals = members.signals || {}; this.prototype.$properties = {}; this.prototype.$methods = {}; this.prototype.$signals = {}; for (const k of Object.keys(properties)) { const options = properties[k]; options.name = options.name || k; options.access = options.access || ACCESS_READWRITE; if (!options.signature) { throw new Error('missing signature for property'); } options.signatureTree = parseSignature(options.signature); assertMemberNameValid(options.name); this.prototype.$properties[options.name] = options; } for (const k of Object.keys(methods)) { const options = methods[k]; options.name = options.name || k; assertMemberNameValid(options.name); options.disabled = !!options.disabled; options.inSignature = options.inSignature || ''; options.outSignature = options.outSignature || ''; options.inSignatureTree = parseSignature(options.inSignature); options.outSignatureTree = parseSignature(options.outSignature); options.fn = this.prototype[k]; this.prototype.$methods[options.name] = options; } for (const k of Object.keys(signals)) { const options = signals[k]; options.name = options.name || k; assertMemberNameValid(options.name); options.fn = this.prototype[k]; options.signature = options.signature || ''; options.signatureTree = parseSignature(options.signature); this.prototype[k] = function () { if (options.disabled) { throw new Error('tried to call a disabled signal'); } const result = options.fn.apply(this, arguments); this.$emitter.emit('signal', options, result); }; this.prototype.$signals[options.name] = options; } } /** * Emit the `PropertiesChanged` signal on an [`Interface`s]{@link * module:interface~Interface} associated standard * `org.freedesktop.DBus.Properties` interface with a map of new values and * invalidated properties. Pass the properties as JavaScript values. * * @static * @example * Interface.emitPropertiesChanged({ SomeProperty: 'bar' }, ['InvalidedProperty']); * * @param {module:interface~Interface} - the `Interface` to emit the `PropertiesChanged` signal on * @param {Object} - A map of property names and new property values that are changed. * @param {string[]} - A list of invalidated properties. */ static emitPropertiesChanged (iface, changedProperties, invalidatedProperties = []) { if (!Array.isArray(invalidatedProperties) || !invalidatedProperties.every((p) => typeof p === 'string')) { throw new Error('invalidated properties must be an array of strings'); } // we transform them to variants here based on property signatures so they // don't have to const properties = iface.$properties || {}; const changedPropertiesVariants = {}; for (const p of Object.keys(changedProperties)) { if (properties[p] === undefined) { throw new Error(`got properties changed with unknown property: ${p}`); } changedPropertiesVariants[p] = new Variant(properties[p].signature, changedProperties[p]); } iface.$emitter.emit('properties-changed', changedPropertiesVariants, invalidatedProperties); } $introspect () { // TODO cache xml when the interface is declared const xml = { $: { name: this.$name } }; const properties = this.$properties || {}; for (const p of Object.keys(properties) || []) { const property = properties[p]; if (property.disabled) { continue; } xml.property = xml.property || []; xml.property.push({ $: { name: property.name, type: property.signature, access: property.access } }); } const methods = this.$methods || {}; for (const m of Object.keys(methods) || []) { const method = methods[m]; if (method.disabled) { continue; } xml.method = xml.method || []; const methodXml = { $: { name: method.name }, arg: [], annotation: [] }; for (const signature of method.inSignatureTree) { methodXml.arg.push({ $: { direction: 'in', type: collapseSignature(signature) } }); } for (const signature of method.outSignatureTree) { methodXml.arg.push({ $: { direction: 'out', type: collapseSignature(signature) } }); } if (method.noReply) { methodXml.annotation.push({ $: { name: 'org.freedesktop.DBus.Method.NoReply', value: 'true' } }); } xml.method.push(methodXml); } const signals = this.$signals || {}; for (const s of Object.keys(signals) || []) { const signal = signals[s]; if (signal.disabled) { continue; } xml.signal = xml.signal || []; const signalXml = { $: { name: signal.name }, arg: [] }; for (const signature of signal.signatureTree) { signalXml.arg.push({ $: { type: collapseSignature(signature) } }); } xml.signal.push(signalXml); } return xml; } } module.exports = { ACCESS_READ: ACCESS_READ, ACCESS_WRITE: ACCESS_WRITE, ACCESS_READWRITE: ACCESS_READWRITE, property: property, method: method, signal: signal, Interface: Interface };