dbus.js 8.0 KB

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