dbus.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. import Gio from 'gi://Gio';
  5. import GjsPrivate from 'gi://GjsPrivate';
  6. import GLib from 'gi://GLib';
  7. import GObject from 'gi://GObject';
  8. /*
  9. * Some utility methods
  10. */
  11. function toDBusCase(string) {
  12. return string.replace(/(?:^\w|[A-Z]|\b\w)/g, (ltr, offset) => {
  13. return ltr.toUpperCase();
  14. }).replace(/[\s_-]+/g, '');
  15. }
  16. function toUnderscoreCase(string) {
  17. return string.replace(/(?:^\w|[A-Z]|_|\b\w)/g, (ltr, offset) => {
  18. if (ltr === '_')
  19. return '';
  20. return (offset > 0) ? `_${ltr.toLowerCase()}` : ltr.toLowerCase();
  21. }).replace(/[\s-]+/g, '');
  22. }
  23. /**
  24. * DBus.Interface represents a DBus interface bound to an object instance, meant
  25. * to be exported over DBus.
  26. */
  27. export const Interface = GObject.registerClass({
  28. GTypeName: 'GSConnectDBusInterface',
  29. Implements: [Gio.DBusInterface],
  30. Properties: {
  31. 'g-instance': GObject.ParamSpec.object(
  32. 'g-instance',
  33. 'Instance',
  34. 'The delegate GObject',
  35. GObject.ParamFlags.READWRITE,
  36. GObject.Object.$gtype
  37. ),
  38. },
  39. }, class Interface extends GjsPrivate.DBusImplementation {
  40. _init(params) {
  41. super._init({
  42. g_instance: params.g_instance,
  43. g_interface_info: params.g_interface_info,
  44. });
  45. // Cache member lookups
  46. this._instanceHandlers = [];
  47. this._instanceMethods = {};
  48. this._instanceProperties = {};
  49. const info = this.get_info();
  50. this.connect('handle-method-call', this._call.bind(this._instance, info));
  51. this.connect('handle-property-get', this._get.bind(this._instance, info));
  52. this.connect('handle-property-set', this._set.bind(this._instance, info));
  53. // Automatically forward known signals
  54. const id = this._instance.connect('notify', this._notify.bind(this));
  55. this._instanceHandlers.push(id);
  56. for (const signal of info.signals) {
  57. const type = `(${signal.args.map(arg => arg.signature).join('')})`;
  58. const id = this._instance.connect(
  59. signal.name,
  60. this._emit.bind(this, signal.name, type)
  61. );
  62. this._instanceHandlers.push(id);
  63. }
  64. // Export if connection and object path were given
  65. if (params.g_connection && params.g_object_path)
  66. this.export(params.g_connection, params.g_object_path);
  67. }
  68. get g_instance() {
  69. if (this._instance === undefined)
  70. this._instance = null;
  71. return this._instance;
  72. }
  73. set g_instance(instance) {
  74. this._instance = instance;
  75. }
  76. /**
  77. * Invoke an instance's method for a DBus method call.
  78. *
  79. * @param {Gio.DBusInterfaceInfo} info - The DBus interface
  80. * @param {DBus.Interface} iface - The DBus interface
  81. * @param {string} name - The DBus method name
  82. * @param {GLib.Variant} parameters - The method parameters
  83. * @param {Gio.DBusMethodInvocation} invocation - The method invocation info
  84. */
  85. async _call(info, iface, name, parameters, invocation) {
  86. let retval;
  87. // Invoke the instance method
  88. try {
  89. const args = parameters.unpack().map(parameter => {
  90. if (parameter.get_type_string() === 'h') {
  91. const message = invocation.get_message();
  92. const fds = message.get_unix_fd_list();
  93. const idx = parameter.deepUnpack();
  94. return fds.get(idx);
  95. } else {
  96. return parameter.recursiveUnpack();
  97. }
  98. });
  99. retval = await this[name](...args);
  100. } catch (e) {
  101. if (e instanceof GLib.Error) {
  102. invocation.return_gerror(e);
  103. } else {
  104. // likely to be a normal JS error
  105. if (!e.name.includes('.'))
  106. e.name = `org.gnome.gjs.JSError.${e.name}`;
  107. invocation.return_dbus_error(e.name, e.message);
  108. }
  109. logError(e, `${this}: ${name}`);
  110. return;
  111. }
  112. // `undefined` is an empty tuple on DBus
  113. if (retval === undefined)
  114. retval = new GLib.Variant('()', []);
  115. // Return the instance result or error
  116. try {
  117. if (!(retval instanceof GLib.Variant)) {
  118. const args = info.lookup_method(name).out_args;
  119. retval = new GLib.Variant(
  120. `(${args.map(arg => arg.signature).join('')})`,
  121. (args.length === 1) ? [retval] : retval
  122. );
  123. }
  124. invocation.return_value(retval);
  125. } catch (e) {
  126. invocation.return_dbus_error(
  127. 'org.gnome.gjs.JSError.ValueError',
  128. 'Service implementation returned an incorrect value type'
  129. );
  130. logError(e, `${this}: ${name}`);
  131. }
  132. }
  133. _nativeProp(obj, name) {
  134. if (this._instanceProperties[name] === undefined) {
  135. let propName = name;
  136. if (propName in obj)
  137. this._instanceProperties[name] = propName;
  138. if (this._instanceProperties[name] === undefined) {
  139. propName = toUnderscoreCase(name);
  140. if (propName in obj)
  141. this._instanceProperties[name] = propName;
  142. }
  143. }
  144. return this._instanceProperties[name];
  145. }
  146. _emit(name, type, obj, ...args) {
  147. this.emit_signal(name, new GLib.Variant(type, args));
  148. }
  149. _get(info, iface, name) {
  150. const nativeValue = this[iface._nativeProp(this, name)];
  151. const propertyInfo = info.lookup_property(name);
  152. if (nativeValue === undefined || propertyInfo === null)
  153. return null;
  154. return new GLib.Variant(propertyInfo.signature, nativeValue);
  155. }
  156. _set(info, iface, name, value) {
  157. const nativeValue = value.recursiveUnpack();
  158. this[iface._nativeProp(this, name)] = nativeValue;
  159. }
  160. _notify(obj, pspec) {
  161. const name = toDBusCase(pspec.name);
  162. const propertyInfo = this.get_info().lookup_property(name);
  163. if (propertyInfo === null)
  164. return;
  165. this.emit_property_changed(
  166. name,
  167. new GLib.Variant(
  168. propertyInfo.signature,
  169. // Adjust for GJS's '-'/'_' conversion
  170. this._instance[pspec.name.replace(/-/gi, '_')]
  171. )
  172. );
  173. }
  174. destroy() {
  175. try {
  176. for (const id of this._instanceHandlers)
  177. this._instance.disconnect(id);
  178. this._instanceHandlers = [];
  179. this.flush();
  180. this.unexport();
  181. } catch (e) {
  182. logError(e);
  183. }
  184. }
  185. });
  186. /**
  187. * Get a new, dedicated DBus connection on @busType
  188. *
  189. * @param {Gio.BusType} [busType] - a Gio.BusType constant
  190. * @param {Gio.Cancellable} [cancellable] - an optional Gio.Cancellable
  191. * @return {Promise<Gio.DBusConnection>} A new DBus connection
  192. */
  193. export function newConnection(busType = Gio.BusType.SESSION, cancellable = null) {
  194. return new Promise((resolve, reject) => {
  195. Gio.DBusConnection.new_for_address(
  196. Gio.dbus_address_get_for_bus_sync(busType, cancellable),
  197. Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT |
  198. Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,
  199. null,
  200. cancellable,
  201. (connection, res) => {
  202. try {
  203. resolve(Gio.DBusConnection.new_for_address_finish(res));
  204. } catch (e) {
  205. reject(e);
  206. }
  207. }
  208. );
  209. });
  210. }