diff --git a/examples/client-properties.js b/examples/client-properties.js new file mode 100644 index 00000000..14bf5908 --- /dev/null +++ b/examples/client-properties.js @@ -0,0 +1,143 @@ +'use strict'; + +const dbus = require ('../index.js') + +/* + This example shows how to query a DBus service for its properties (no methods nor signals here), get their value ( + and display them) and then change them. + Since we're acting only as a client, there is not need to request a name: we don't need to register a service + against the bus (but we could!), so we only act as a client. + + NOTE: this file is made to query the service that is exposed in the file 'server-properties.js', so be sure to + start it first (node server-properties.js in another terminal should be enough) +) +*/ + +// This is the DBus service we will query (server-properties.js) +const targetServiceName = 'com.dbus.native.properties' + +// This is the service's interface we will query +const targetIfaceName = targetServiceName // note that is it equal to the service name, but this is not mandatory at all + +// This is the service's DBus object path that we will query for the properties +const targetObjectPath = '/' + targetServiceName.replace (/\./g, '/') + +// First, connect to the session bus (works the same on the system bus, it's just less permissive) +const sessionBus = dbus.sessionBus() + +// Check the connection was successful +if (!sessionBus) { + throw new Error ('Could not connect to the DBus session bus.') +} + +// First, we must query the bus for the desired DBus service: +let targetService = sessionBus.getService (targetServiceName) + +// Then we must query it's interface, this is callback-based +targetService.getInterface (targetObjectPath, targetIfaceName, (e, iface) => { + // we need to check for error + if (e || !iface) { + console.error ('Could not query interface \'' + targetIfaceName + '\', the error was: ' + err ? err : '(no error)') + process.exit (1) + } + + /* + Now, the service's object's interface is represented in 'iface'. + Properties are accessed via callback (so it can be a bit verbose) + */ + iface.SingleString ((e, propValue) => { + // Be careful not to check for `! propValue` because, what if propValue is a boolean whose value is false?! + if (e || typeof propValue === 'undefined') { + console.error ('Could not get propery \'SingleString\', the error was: ' + err ? err : '(no error)') + process.exit (1) + } + + // now it's safe: we can display the value + console.log ('SingleString: ' + propValue) + + /* + Move to the next example (you can comment the line if you want to go step-by-step) + Also, since this is all callback-based, I'm nesting the 'stepX' calls so that what is displayed on your + console is in the same order as the calls here. But in YOUR applications you can do otherwise of course. + */ + step1() + }) + + // Show how to get and change the value of 'SingleInt32' + function step1() { + iface.SingleInt32 ((e, propValue) => { + // Be careful not to check for `! propValue` because, what if propValue is a boolean whose value is false?! + if (e || typeof propValue === 'undefined') { + console.error ('Could not get propery \'SingleInt32\', the error was: ' + err ? err : '(no error)') + process.exit (1) + } + + console.log ('SingleInt32 (before change): ' + propValue) + + // Changing a property value is a simple matter of assignment: + iface.SingleInt32 = 33 + + /* + Let's display it again (you will notice that this callback-based accessor is verbose, I advise to make + a helper that automatically checks for error) + */ + iface.SingleInt32 ((e, propValue) => { + // Be careful not to check for `! propValue` because, what if propValue is a boolean whose value is false?! + if (e || typeof propValue === 'undefined') { + console.error ('Could not get propery \'SingleInt32\', the error was: ' + err ? err : '(no error)') + process.exit (1) + } + + console.log ('SingleInt32 (after change): ' + propValue) + + /* + Move to the next example (you can comment the line if you want to go step-by-step) + Also, since this is all callback-based, I'm nesting the 'stepX' calls so that what is displayed on your + console is in the same order as the calls here. But in YOUR applications you can do otherwise of course. + */ + step2() + }) + }) + } + + function step2() { + iface.ArrayOfUint16 ((e, propValue) => { + // Be careful not to check for `! propValue` because, what if propValue is a boolean whose value is false?! + if (e || typeof propValue === 'undefined') { + console.error ('Could not get propery \'ArrayOfUint16\', the error was: ' + err ? err : '(no error)') + process.exit (1) + } + + console.log ('ArrayOfUint16 (before change): ' + propValue) + + /* + Remember our typing convention here: since an array is a "complex / container" type, it must be enclosed + in brackets; that's the first (outer) pair. Then, the second (inner) pair of brackets is the actual + array. + Please see comments in 'service-properties.js' for more information on this. + */ + iface.ArrayOfUint16 = [[20,21,21,22]] + + /* + Let's display it again (you will notice that this callback-based accessor is verbose, I advise to make + a helper that automatically checks for error) + */ + iface.ArrayOfUint16 ((e, propValue) => { + // Be careful not to check for `! propValue` because, what if propValue is a boolean whose value is false?! + if (e || typeof propValue === 'undefined') { + console.error ('Could not get propery \'ArrayOfUint16\', the error was: ' + err ? err : '(no error)') + process.exit (1) + } + + console.log ('ArrayOfUint16 (after change): ' + propValue) + + /* + Move to the next example (you can comment the line if you want to go step-by-step) + Also, since this is all callback-based, I'm nesting the 'stepX' calls so that what is displayed on your + console is in the same order as the calls here. But in YOUR applications you can do otherwise of course. + */ + + }) + }) + } +}) diff --git a/examples/return-types.js b/examples/return-types.js new file mode 100644 index 00000000..5f0e6c4f --- /dev/null +++ b/examples/return-types.js @@ -0,0 +1,190 @@ +'use strict'; + +const dbus = require ('../index.js') + +/* + This test file's purpose is to show example of possible return types for functions. + In order to do that, we connect to the session bus and create a DBus service exposing + a certain number of function calls (no signals nor properties) that you can call with + any DBus-speaking software. + + For instance you can use `gdbus` to introspect a service and make function calls. + - introspect: `gdbus introspect -e -d com.dbus.native.return.types -o /com/dbus/native/return/types` + - make a method call: `gdbus introspect -e -d com.dbus.native.return.types -o /com/dbus/native/return/types -m com.dbus.native.return.types.FunctionName` +*/ + +const serviceName = 'com.dbus.native.return.types' // our DBus service name +/* + The interface under which we will expose our functions (chose to be the same as the service name, but we can + choose whatever name we want, provided it respects the rules, see DBus naming documentation) +*/ +const interfaceName = serviceName +/* + The object pat hthat we want to expose on the bus. Here we chose to have the same path as the service (and + interface) name, with the dots replaced by slashes (because objects path must be on the form of UNIX paths) + But again, we could chose anything. This is just a demo here. +*/ +const objectPath = '/' + serviceName.replace (/\./g, '/') + +// First, connect to the session bus (works the same on the system bus, it's just less permissive) +const sessionBus = dbus.sessionBus() + +// Check the connection was successful +if (!sessionBus) { + throw new Error ('Could not connect to the DBus session bus.') +} + +/* + Then request our service name to the bus. + The 0x4 flag means that we don't want to be queued if the service name we are requesting is already + owned by another service ;we want to fail instead. +*/ +sessionBus.requestName (serviceName, 0x4, (e, retCode) => { + // If there was an error, warn user and fail + if (e) { + throw new Error (`Could not request service name ${serviceName}, the error was: ${e}.`) + } + + // Return code 0x1 means we successfully had the name + if (retCode === 1) { + console.log (`Successfully requested service name "${serviceName}"!`) + proceed() + } + /* Other return codes means various errors, check here + (https://dbus.freedesktop.org/doc/api/html/group__DBusShared.html#ga37a9bc7c6eb11d212bf8d5e5ff3b50f9) for more + information + */ + else { + throw new Error (`Failed to request service name "${serviceName}". Check what return code "${retCode}" means.`) + } +}) + +// Function called when we have successfully got the service name we wanted +function proceed() { + let ifaceDesc + let iface + + // First, we need to create our interface description (here we will only expose method calls) + ifaceDesc = { + name: interfaceName, + methods: { + // Simple types + SayHello: ['', 's', [], ['hello_sentence']], // Takes no input and returns a single string + GetInt16: ['', 'n', [], ['Int16_number']], // Takes no input and returns an int16 integers + GetUInt16: ['', 'q', [], ['UInt16_number']], // Takes no input and returns an uint16 integers + GetInt32: ['', 'i', [], ['Int32_number']], // Takes no input, returns an int32 integer + GetUInt32: ['', 'u', [], ['UInt32_number']], // Takes no input, returns an uint32 integer + // 64 numbers being not handled natively in Javascript, they are not yet handled by this library (WIP) + //GetInt64: ['', 'x', [], ['Int32_number']], // Takes no input, returns an int64 integer + //GetUInt64: ['', 't', [], ['UInt32_number']], // Takes no input, returns an uint64 integer + GetBool: ['', 'b', [], ['Bool_value']], // Takes no input, returns a boolean + GetDouble: ['', 'd', [], ['Double_value']], // Takes no input, returns a double + GetByte: ['', 'y', [], ['Byte_value']], // Takes no input, returns a byte + + // Complex-types + GetArrayOfStrings: ['y', 'as', ['nb_elems'], ['strings']], // Take a number and return an array of N strings + // Takes no input, returns a structure with a string, an int32 and a bool + GetCustomStruct: ['', '(sib)', [], ['struct']], + // Takes no input, returns a dictionary (hash-table) whose keys are strings and values int32 + GetDictEntry: ['', 'a{si}', [], ['dict_entry']], + }, + // No signals nor properties for this example + signals: {}, + properties: {} + } + + // Then we need to create the interface implementation (with actual functions) + iface = { + SayHello: function() { + return 'Hello, world!' // This is how to return a single string + }, + GetInt16: function() { + let min = -0x7FFF-1 + let max = 0x7FFF + return Math.round (Math.random() * (max - min) + min) + }, + GetUInt16: function() { + let min = 0 + let max = 0xFFFF + return Math.round (Math.random() * (max - min) + min) + }, + GetInt32: function() { + let min = -0x7FFFFFFF-1 + let max = 0x7FFFFFFF + return Math.round (Math.random() * (max - min) + min) + }, + GetUInt32: function() { + let min = 0 + let max = 0xFFFFFFFF + return Math.round (Math.random() * (max - min) + min) + }, + GetBool: function() { + return Math.random() >= 0.5 ? true : false + }, + GetDouble: function() { + /* + We are only returning a number between 0 and 1 here, but this is just for the test. + Javascript can handle number between Number.MIN_VALUE and Number.MAX_VALUE, which are 5e-234 and 1.7976931348623157e+308 respectively. + There would be no point in returing such big numbers for this demo, but this is perfectly okay with DBus. + */ + return Math.random() + }, + GetByte: function() { + let min = 0x00 + let max = 0xFF + return Math.round (Math.random() * (max - min) + min) + }, + GetArrayOfStrings: function (n) { + let ret = [] + + // Check that we requested a positive number of elements, and not a too big one + if (n < 0 || n > 255) { + // Return a DBus error to indicate a problem (shows how to send DBus errors) + return new Error ('Incorrect number of elements supplied (0 < n < 256)!') + } + + while (n--) { + ret.unshift ('String #' + n) + } + + return ret // 'ret' is an array, to return an array, we simply return it + }, + GetCustomStruct: function () { + let min = -0x7FFFFFFF-1 + let max = 0x7FFFFFFF + let string = 'I\m sorry, my responses are limited, you must ask the right question.' + let int32 = Math.round (Math.random() * (max - min) + min) + let bool = Math.random() >= 0.5 ? true : false + + /* + Important note here: for the DBus type STRUCT, you need to return a Javascript ARRAY, with the field in + the right order for the declared struct. + */ + return [string, int32, bool] + }, + GetDictEntry: function () { + let min = -0x7FFFFFFF-1 + let max = 0x7FFFFFFF + let key1 = 'str1' + let key2 = 'str2' + let key3 = 'str3' + let i1 = Math.round (Math.random() * (max - min) + min) + let i2 = Math.round (Math.random() * (max - min) + min) + let i3 = Math.round (Math.random() * (max - min) + min) + + /* + This is how DICT_ENTRIES are returned: in JS side, it's an array of arrays. + Each of the arrays must have TWO values, the first being the key (here a string ; keys + MUST be single types, so string, integers, double, booleans, etc.) and the second being + the value (here, an int32 ; keys can be any type, including complex one: struct, etc.) + */ + return [[key1, i1], [key2, i2], [key3, i3]] + } + } + + // Now we need to actually export our interface on our object + sessionBus.exportInterface (iface, objectPath, ifaceDesc) + + // Say our service is ready to receive function calls (you can use `gdbus call` to make function calls) + console.log ('Interface exposed to DBus, ready to receive function calls!') +} diff --git a/examples/service-properties.js b/examples/service-properties.js new file mode 100644 index 00000000..7c8cf485 --- /dev/null +++ b/examples/service-properties.js @@ -0,0 +1,202 @@ +'use strict'; + +const dbus = require ('../index.js') + +/* + This example file's purpose is to show how to get and set DBus properties. + In order to do that, we connect to the session bus and create a DBus service exposing + a certain number of properties (no methods nor signals) that you can call with + any DBus-speaking software. + + For instance you can use `gdbus` to get and set properties: + - get: `gdbus call -e -d com.dbus.native.properties -o /com/dbus/native/properties -m org.freedesktop.DBus.Properties.Get 'com.dbus.native.properties' '' + - set: `gdbus call -e -d com.dbus.native.properties -o /com/dbus/native/properties -m org.freedesktop.DBus.Properties.Set 'com.dbus.native.properties' '' '' +*/ + +const serviceName = 'com.dbus.native.properties' // our DBus service name +/* + The interface under which we will expose our functions (chose to be the same as the service name, but we can + choose whatever name we want, provided it respects the rules, see DBus naming documentation) +*/ +const interfaceName = serviceName +/* + The object path that we want to expose on the bus. Here we chose to have the same path as the service (and + interface) name, with the dots replaced by slashes (because objects path must be on the form of UNIX paths) + But again, we could chose anything. This is just a demo here. +*/ +const objectPath = '/' + serviceName.replace (/\./g, '/') + +// First, connect to the session bus (works the same on the system bus, it's just less permissive) +const sessionBus = dbus.sessionBus() + +// Check the connection was successful +if (!sessionBus) { + throw new Error ('Could not connect to the DBus session bus.') +} + +/* + Then request our service name to the bus. + The 0x4 flag means that we don't want to be queued if the service name we are requesting is already + owned by another service ;we want to fail instead. +*/ +sessionBus.requestName (serviceName, 0x4, (e, retCode) => { + // If there was an error, warn user and fail + if (e) { + throw new Error (`Could not request service name ${serviceName}, the error was: ${e}.`) + } + + // Return code 0x1 means we successfully had the name + if (retCode === 1) { + console.log (`Successfully requested service name "${serviceName}"!`) + proceed() + } + /* Other return codes means various errors, check here + (https://dbus.freedesktop.org/doc/api/html/group__DBusShared.html#ga37a9bc7c6eb11d212bf8d5e5ff3b50f9) for more + information + */ + else { + throw new Error (`Failed to request service name "${serviceName}". Check what return code "${retCode}" means.`) + } +}) + +// Function called when we have successfully got the service name we wanted +function proceed() { + let ifaceDesc + let iface + + // First, we need to create our interface description (here we will only expose method calls) + ifaceDesc = { + name: interfaceName, + // No methods nor signals for this example + methods: {}, + signals: {}, + properties: { + // Single types + SingleString: 's', + SingleInt32: 'i', + SingleBool: 'b', + + // Arrays + ArrayOfStrings: 'as', + ArrayOfUint16: 'aq', + // Nested arrays! + ArrayOfArrayOfInt32: 'aai', + + // Structs + Structiii: '(iii)', + Structsb: '(sb)', + Structsai: '(sai)', // struct who first item is a string (s) and the second is an array of int32 (ai) + + // Dict entries, note that dict entries are ALWAYS array of dict entries + Dictsi: 'a{si}', + Dictsb: 'a{sb}', + } + } + + // Then we need to create the interface implementation (with actual values) + iface = { + // Single types + SingleString: 'Hello, world!', + SingleInt32: 1089, + SingleBool: true, + + /* + _ + / \ _ __ _ __ __ _ _ _ ___ + / _ \ | '__| '__/ _` | | | / __| + / ___ \| | | | | (_| | |_| \__ \ + /_/ \_\_| |_| \__,_|\__, |___/ + |___/ + */ + /* + Note the double brackets, here is how it works: + - First (outer) pair of brackets means "complex / container type" + - Second (inner) pair of brackets is the _array_ + */ + ArrayOfStrings: [['Array', 'of', 'Strings']], + ArrayOfUint16: [[10, 100, 1000]], + // Nested array@ + /* + Note the triple brackets, works the same as before: + - First (outer) brackets means "complex / container type" + - Second brackets is the 'Array' (of something) + - Third is the 'Array of Int32' + */ + ArrayOfArrayOfInt32: [[[1,2], [3,4,5], [6,7]]], + + /* + ____ _ _ + / ___|| |_ _ __ _ _ ___| |_ ___ + \___ \| __| '__| | | |/ __| __/ __| + ___) | |_| | | |_| | (__| |_\__ \ + |____/ \__|_| \__,_|\___|\__|___/ + + + */ + /* + Note the double brackets: + - First (outer) brackets means "complex / container" type + - Second (inner) brackets is how we represent the DBus "struct" type: it's an array whose elements are in + the same order as the elements in the DBus struct + */ + Structiii: [[1000, 2000, 3000]], + Structsb: [['string', false]], + /* + Note the triple brackets, works the same as before: + - First (outer) brackets means "complex / container type" + - Second (inner) brackets is the way we represent a struct in JS + - Then comes the first param, the string, and the third pair of brackets is the actual array of int32 + */ + Structsai: [['other string', [33, 1089]]], + + /* + ____ _ _ + | _ \(_) ___| |_ ___ + | | | | |/ __| __/ __| + | |_| | | (__| |_\__ \ + |____/|_|\___|\__|___/ + + */ + /* + Note the triple brackets, again, same as before: + - First (outer) means "complex / container type" + - Second is the array (remember dict entries are always, in fact, arrays of dict entries), see Dictsb for an + exanple with actual several dict entries + - Third (inner) is how we represent a dict entry in Javascript: it's an array whose first value is the key, + and the second is the value + */ + Dictsi: [[['age', 33]]], + Dictsb: [[['isAwesome', true], ['amOwner', false]]], + + } + + /* + For the lazy who don't want to actually start the service and type the `gdbus introspect` command, here is the + output you'd have (note: since this is for the lazy, I don't garantee that it's going to be up-to-date with + the example below, in doubt, don't be lazy and type the goddamn command!) + + interface com.dbus.native.properties { + methods: + signals: + properties: + readwrite s SingleString = 'Hello, world!'; + readwrite i SingleInt32 = 1089; + readwrite b SingleBool = true; + readwrite as ArrayOfStrings = ['Array', 'of', 'Strings']; + readwrite aq ArrayOfUint16 = [10, 100, 1000]; + readwrite aai ArrayOfArrayOfInt32 = [[1, 2], [3, 4, 5], [6, 7]]; + readwrite (iii) Structiii = (1000, 2000, 3000); + readwrite (sb) Structsb = ('string', false); + readwrite (sai) Structsai = ('other string', [33, 1089]); + readwrite a{si} Dictsi = {'age': 33}; + readwrite a{sb} Dictsb = {'isAwesome': true, 'amOwner': false}; + }; + + */ + + // Now we need to actually export our interface on our object + sessionBus.exportInterface (iface, objectPath, ifaceDesc) + + // Say our service is ready to receive function calls (you can use `gdbus call` to make function calls) + console.log ('Interface exposed to DBus, ready to receive function calls!') +} diff --git a/index.js b/index.js index 0895d166..7475dbc8 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,21 @@ // dbus.freedesktop.org/doc/dbus-specification.html -var EventEmitter = require('events').EventEmitter; -var net = require('net'); +const net = require('net') +const utils = require ('./lib/utils') +const inspect = require ('util').inspect +const message = require('./lib/message') +const constants = require('./lib/constants') +const EventEmitter = require('events').EventEmitter +const helloMessage = require('./lib/hello-message.js') +const clientHandshake = require('./lib/handshake.js') +const serverHandshake = require('./lib/server-handshake.js') + +// Whether to set this file's functions into debugging (verbose) mode +const DEBUG_THIS_FILE = false + +// Allows for setting all files to debug in once statement instead of manually setting every flag +const DEBUG = DEBUG_THIS_FILE || utils.GLOBAL_DEBUG -var constants = require('./lib/constants'); -var message = require('./lib/message'); -var clientHandshake = require('./lib/handshake.js'); -var serverHandshake = require('./lib/server-handshake.js'); -var helloMessage = require('./lib/hello-message.js'); function createStream(opts) { if (opts.stream) @@ -85,11 +93,13 @@ function createConnection(opts) { stream.setNoDelay(); stream.on('error', function(err) { + if (DEBUG) console.error ('Stream.error(): ' + err) // forward network and stream errors self.emit('error', err); }); stream.on('end', function() { + if (DEBUG) console.error ('Stream.end(): ' + inspect (arguments)) self.emit('end'); self.message = function() { console.warn("Didn't write bytes to closed stream"); diff --git a/lib/bus.js b/lib/bus.js index cbbe929a..f4da538c 100644 --- a/lib/bus.js +++ b/lib/bus.js @@ -1,6 +1,17 @@ -var EventEmitter = require('events').EventEmitter; -var constants = require('./constants'); -var stdDbusIfaces= require('./stdifaces'); +const utils = require ('./utils') +const inspect = require ('util').inspect +const constants = require('./constants') +const EventEmitter = require('events').EventEmitter +const stdDbusIfaces = require('./stdifaces') + +const DBUS_NAME_REGEX = utils.DBUS_NAME_REGEX +const DBUS_MAX_NAME_LENGTH = utils.DBUS_MAX_NAME_LENGTH + +// Whether to set this file's functions into debugging (verbose) mode +const DEBUG_THIS_FILE = false + +// Allows for setting all files to debug in once statement instead of manually setting every flag +const DEBUG = DEBUG_THIS_FILE || utils.GLOBAL_DEBUG module.exports = function bus(conn, opts) { if (!(this instanceof bus)) { @@ -68,8 +79,15 @@ module.exports = function bus(conn, opts) { self.connection.message(signalMsg); } - // Warning: errorName must respect the same rules as interface names (must contain a dot) - this.sendError = function(msg, errorName, errorText) { + this.sendError = function(msg = mandatory(), errorName = mandatory(), errorText) { + /* + Check that the error name respects the naming syntax, which is the same as interfaces, see + https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface + */ + if (!utils.isNameValid (errorName)) { + throw new TypeError ('Error\'s name missing or invalid (see http://bit.ly/2cFC6Vx for naming rules).') + } + var reply = { type: constants.messageType.error, replySerial: msg.serial, @@ -118,7 +136,7 @@ module.exports = function bus(conn, opts) { } else if (msg.type == constants.messageType.signal) { self.signals.emit(self.mangle(msg), msg.body, msg.signature); } else { // methodCall - + if (DEBUG) console.log ('Message call received:\n', inspect (msg)) if (stdDbusIfaces(msg, self)) return; @@ -193,8 +211,18 @@ module.exports = function bus(conn, opts) { self.methodCallHandlers[key] = handler; }; - this.exportInterface = function(obj, path, iface) { + this.exportInterface = function(obj = mandatory(), path = mandatory(), iface = mandatory()) { var entry; + + /* + Check that the interface to expose does have a name (otherwise it makes 'undefined' interfaces) + and that the name respects DBus specs: + https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface + */ + if (!utils.isNameValid (iface.name)) { + throw new TypeError ('Interface\'s name missing or invalid (see http://bit.ly/2cFC6Vx for naming rules).') + } + if (!self.exportedObjects[path]) entry = self.exportedObjects[path] = {}; else diff --git a/lib/signature.js b/lib/signature.js index 49dbd00b..0e122955 100644 --- a/lib/signature.js +++ b/lib/signature.js @@ -1,15 +1,26 @@ // parse signature from string to tree +const utils = require ('./utils.js') +const inspect = require ('util').inspect -var match = { +// Whether to set this file's functions into debugging (verbose) mode +const DEBUG_THIS_FILE = false + +// Allows for setting all files to debug in once statement instead of manually setting every flag +const DEBUG = DEBUG_THIS_FILE || utils.GLOBAL_DEBUG + + +const match = { '{' : '}', '(' : ')' -}; +} -var knownTypes = {}; +const knownTypes = {} '(){}ybnqiuxtdsogarvehm*?@&^'.split('').forEach(function(c) { knownTypes[c] = true; }); +const singleTypes = 'ybnqiuxtdsog' + var exports = module.exports = function(signature) { var index = 0; @@ -73,6 +84,84 @@ module.exports.fromTree = function(tree) { return res; } +module.exports.valueFromTree = function valueFromTree (msgBody) { + let tree = msgBody[0] + let data = msgBody[1] + let type = tree[0].type + + if (false && DEBUG) { + console.log ('msgBody:\n', inspect (msgBody, {depth: Infinity})) + } + + if (DEBUG) { + console.log ('type: ' + type) + console.log ('data: ' + data) + } + // If the tree contains a single type, just read it and return it + if (singleTypes.includes (type)) { + return data[0] + } + // If this is an array + else if (type === 'a') { + // let ret = [] // initialize empty array + let ret + + if (DEBUG) { + console.log ('>>> ARRAY <<<') + console.log (data) + } + + ret = data[0] + + if (DEBUG) { + console.log ('This is what will be returned:\n' + inspect (ret)) + } + + return ret + } + // If this is a STRUCT + else if (type === '(') { + let ret + let recursiveRet = [] + + if (DEBUG) { + console.log ('>>> STRUCT <<<') + console.log (data) + } + + for (let i in tree[0].child) { + let rec = [ [ tree[0].child[i] ], [ data[0][i] ] ] + + let recA = valueFromTree (rec) + + // Each recursive return value is pushed to the array (because container data is stored in Javascript arrays) + if (DEBUG) { + console.log ('recursiveRet (before): ' + inspect (recursiveRet)) + } + recursiveRet.push (recA) + if (DEBUG) { + console.log ('recursiveRet (after): ' + inspect (recursiveRet)) + } + + if (false && DEBUG) { + console.log ('Recursive call valueFromTree (' + inspect (rec) + ') = ' + recA) + } + } + + /* + Return 'recursiveRet' as-is for recursivity compatibility. If this is the last call, it must be array-ified + if needed, as seen in stdifaces.js (look for the comments near the call to 'valueFormTree') + */ + ret = recursiveRet + return ret + } + else { + // For unsupported types or errors, return undefined, caller must check for undefined + if (DEBUG) console.error ('Unsupported complex type!') + return undefined + } +} + // command-line test //console.log(JSON.stringify(module.exports(process.argv[2]), null, 4)); //var tree = module.exports('a(ssssbbbbbbbbuasa{ss}sa{sv})a(ssssssbbssa{ss}sa{sv})a(ssssssbsassa{sv})'); diff --git a/lib/stdifaces.js b/lib/stdifaces.js index 708e0581..3f1f4e9d 100644 --- a/lib/stdifaces.js +++ b/lib/stdifaces.js @@ -1,5 +1,13 @@ -var constants = require('./constants'); -var parseSignature = require('./signature'); +const utils = require ('./utils') +const inspect = require ('util').inspect +const constants = require ('./constants') +const parseSignature = require ('./signature') + +// Whether to set this file's functions into debugging (verbose) mode +const DEBUG_THIS_FILE = false + +// Allows for setting all files to debug in once statement instead of manually setting every flag +const DEBUG = DEBUG_THIS_FILE || utils.GLOBAL_DEBUG // TODO: use xmlbuilder @@ -89,7 +97,7 @@ module.exports = function(msg, bus) { body: [resultXml.join('\n')] }; bus.connection.message(reply); - return 1; + return true } else if (msg['interface'] === 'org.freedesktop.DBus.Properties') { var interfaceName = msg.body[0]; var obj = bus.exportedObjects[msg.path]; @@ -98,7 +106,7 @@ module.exports = function(msg, bus) { { // TODO: bus.sendError(msg, 'org.freedesktop.DBus.Error.UnknownMethod', 'Uh oh oh'); - return 1; + return true } var impl = obj[interfaceName][1]; @@ -108,14 +116,73 @@ module.exports = function(msg, bus) { destination: msg.sender }; if (msg.member === 'Get' || msg.member === 'Set') { + if (DEBUG) { + console.log ('Get - Set') + console.log (inspect(msg, {depth: Infinity})) + console.log ('----') + } + var propertyName = msg.body[1]; var propType = obj[interfaceName][0].properties[propertyName]; + if (msg.member === 'Get') { + // The ifaceDesc object should contain a property with the name of the property var propValue = impl[propertyName]; + + // Check if the property does exist + if (propValue === undefined) { + let errName = 'org.freedesktop.DBus.Error.InvalidArgs' + let errText = 'No such property \'' + propertyName + '\'' + bus.sendError (msg, errName, errText) + return true + } reply.signature = 'v'; reply.body = [[propType, propValue]]; } else { - impl[propertyName] = 1234; // TODO: read variant and set property value + let msgBody = msg.body[2] + let data + + if (DEBUG) console.log ('=== Set ===') + + // Extract actual value to assign to the property. + data = parseSignature.valueFromTree (msgBody) + if (data === undefined) { + // If we could not extract a value, return a proper error letting the user know + let errName = 'org.freedesktop.DBus.Error.InvalidArgs' + let errText = 'Setting complex-type properties is not yet supported.' + + if (DEBUG) { + console.log ('Could not parse data, Set operation aborted. (Data was:\n' + inspect (msgBody) + ')') + } + /* + For now, return a DBus Error to tell the user that setting complex data-type properties + is not yet supported. + */ + bus.sendError (msg, errName, errText) + return true + } + + if (false && DEBUG) { + console.log ('Parsed Data:\n', inspect (data)) + console.log ('\nimpl[propertyName] (before):\n' + inspect (impl[propertyName])) + } + + /* + This line is correct, it is indeed IF 'data' is ALREADY an array, then add brackets '[' & ']' + and IF it is NOT already an array, DON'T add the brackets. + Explanation: all container types (array, STRUCT () and DICT {} are implemented as Javascript arrays + and single types are not. + For recursivity reasons, 'valueFromTree' returns the value as Javascript (so single value for single values and array of values for the other types). Then our implementation requires that container + types MUST be enclosed in brackets. + So the story is simple: if 'data' is an array, it means it's a container so wrap it in brackets, + but if it's not, then it is a single type, and it can be assigned directly, as-is. + */ + impl[propertyName] = Array.isArray (data) ? [data] : data + + if (false && DEBUG) { + console.log ('\nimpl[propertyName] (after):\n' + inspect (impl[propertyName])) + } + // impl[propertyName] = 1234; // TODO: read variant and set property value } } else if (msg.member == 'GetAll') { @@ -128,7 +195,7 @@ module.exports = function(msg, bus) { reply.body = [props]; } bus.connection.message(reply); - return 1; + return true } else if (msg['interface'] === 'org.freedesktop.DBus.Peer') { // TODO: implement bus.replyTo(srcMsg, signature, body) method var reply = { @@ -143,9 +210,14 @@ module.exports = function(msg, bus) { reply.body = ['This is a machine id. TODO: implement']; } bus.connection.message(reply); - return 1; + return true } - return 0; + + /* + If none of these checks passed, it means it's not a standard interface, so return false so that parsing + can continue for the other, custom interfaces. + */ + return false }; // TODO: move to introspect.js diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..dfd325aa --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,55 @@ +'use strict'; + +/** @module Utils */ + +/* + This module contains util functions, wrappers and constants that are useful for the whole lib +*/ + +// Set to true to switch all library files into debug mode +const GLOBAL_DEBUG = false + +/** + * Maximum name length for interface or error name that DBus allows + * @type {number} + */ +const DBUS_MAX_NAME_LENGTH = 255 + +/** + * Regex that validates an interface or error name (have to check max length first) + * @type {regex} + */ +const DBUS_NAME_REGEX = /^[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)+$/ + +/** + * Test whether a name respects DBus naming convention.
+ * + * @param {string} name - The name to check for validity + * @returns {boolean} Whether the name is valid or not, according to DBus naming rules + * @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface + */ +function isNameValid (name) { + if (typeof name !== 'string' || name.length >= DBUS_MAX_NAME_LENGTH || ! DBUS_NAME_REGEX.test (name)) { + return false + } else { + return true + } +} + +/** + * Convenient function to put as default value for function's argument that we want to make mandatory.
+ * It throws an error so that the user knows he missed a mandatory argument. + * + * @throws {TypeError} + */ +function mandatory () { + throw new TypeError ('Missed a mandatory argument in function call!') +} + +module.exports = { + DBUS_MAX_NAME_LENGTH, + DBUS_NAME_REGEX, + GLOBAL_DEBUG, + mandatory, + isNameValid +}