// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect // // SPDX-License-Identifier: GPL-2.0-or-later import Gio from 'gi://Gio'; import GjsPrivate from 'gi://GjsPrivate'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; /* * Some utility methods */ function toDBusCase(string) { return string.replace(/(?:^\w|[A-Z]|\b\w)/g, (ltr, offset) => { return ltr.toUpperCase(); }).replace(/[\s_-]+/g, ''); } function toUnderscoreCase(string) { return string.replace(/(?:^\w|[A-Z]|_|\b\w)/g, (ltr, offset) => { if (ltr === '_') return ''; return (offset > 0) ? `_${ltr.toLowerCase()}` : ltr.toLowerCase(); }).replace(/[\s-]+/g, ''); } /** * DBus.Interface represents a DBus interface bound to an object instance, meant * to be exported over DBus. */ export const Interface = GObject.registerClass({ GTypeName: 'GSConnectDBusInterface', Implements: [Gio.DBusInterface], Properties: { 'g-instance': GObject.ParamSpec.object( 'g-instance', 'Instance', 'The delegate GObject', GObject.ParamFlags.READWRITE, GObject.Object.$gtype ), }, }, class Interface extends GjsPrivate.DBusImplementation { _init(params) { super._init({ g_instance: params.g_instance, g_interface_info: params.g_interface_info, }); // Cache member lookups this._instanceHandlers = []; this._instanceMethods = {}; this._instanceProperties = {}; const info = this.get_info(); this.connect('handle-method-call', this._call.bind(this._instance, info)); this.connect('handle-property-get', this._get.bind(this._instance, info)); this.connect('handle-property-set', this._set.bind(this._instance, info)); // Automatically forward known signals const id = this._instance.connect('notify', this._notify.bind(this)); this._instanceHandlers.push(id); for (const signal of info.signals) { const type = `(${signal.args.map(arg => arg.signature).join('')})`; const id = this._instance.connect( signal.name, this._emit.bind(this, signal.name, type) ); this._instanceHandlers.push(id); } // Export if connection and object path were given if (params.g_connection && params.g_object_path) this.export(params.g_connection, params.g_object_path); } get g_instance() { if (this._instance === undefined) this._instance = null; return this._instance; } set g_instance(instance) { this._instance = instance; } /** * Invoke an instance's method for a DBus method call. * * @param {Gio.DBusInterfaceInfo} info - The DBus interface * @param {DBus.Interface} iface - The DBus interface * @param {string} name - The DBus method name * @param {GLib.Variant} parameters - The method parameters * @param {Gio.DBusMethodInvocation} invocation - The method invocation info */ async _call(info, iface, name, parameters, invocation) { let retval; // Invoke the instance method try { const args = parameters.unpack().map(parameter => { if (parameter.get_type_string() === 'h') { const message = invocation.get_message(); const fds = message.get_unix_fd_list(); const idx = parameter.deepUnpack(); return fds.get(idx); } else { return parameter.recursiveUnpack(); } }); retval = await this[name](...args); } catch (e) { if (e instanceof GLib.Error) { invocation.return_gerror(e); } else { // likely to be a normal JS error if (!e.name.includes('.')) e.name = `org.gnome.gjs.JSError.${e.name}`; invocation.return_dbus_error(e.name, e.message); } logError(e, `${this}: ${name}`); return; } // `undefined` is an empty tuple on DBus if (retval === undefined) retval = new GLib.Variant('()', []); // Return the instance result or error try { if (!(retval instanceof GLib.Variant)) { const args = info.lookup_method(name).out_args; retval = new GLib.Variant( `(${args.map(arg => arg.signature).join('')})`, (args.length === 1) ? [retval] : retval ); } invocation.return_value(retval); } catch (e) { invocation.return_dbus_error( 'org.gnome.gjs.JSError.ValueError', 'Service implementation returned an incorrect value type' ); logError(e, `${this}: ${name}`); } } _nativeProp(obj, name) { if (this._instanceProperties[name] === undefined) { let propName = name; if (propName in obj) this._instanceProperties[name] = propName; if (this._instanceProperties[name] === undefined) { propName = toUnderscoreCase(name); if (propName in obj) this._instanceProperties[name] = propName; } } return this._instanceProperties[name]; } _emit(name, type, obj, ...args) { this.emit_signal(name, new GLib.Variant(type, args)); } _get(info, iface, name) { const nativeValue = this[iface._nativeProp(this, name)]; const propertyInfo = info.lookup_property(name); if (nativeValue === undefined || propertyInfo === null) return null; return new GLib.Variant(propertyInfo.signature, nativeValue); } _set(info, iface, name, value) { const nativeValue = value.recursiveUnpack(); this[iface._nativeProp(this, name)] = nativeValue; } _notify(obj, pspec) { const name = toDBusCase(pspec.name); const propertyInfo = this.get_info().lookup_property(name); if (propertyInfo === null) return; this.emit_property_changed( name, new GLib.Variant( propertyInfo.signature, // Adjust for GJS's '-'/'_' conversion this._instance[pspec.name.replace(/-/gi, '_')] ) ); } destroy() { try { for (const id of this._instanceHandlers) this._instance.disconnect(id); this._instanceHandlers = []; this.flush(); this.unexport(); } catch (e) { logError(e); } } }); /** * Get a new, dedicated DBus connection on @busType * * @param {Gio.BusType} [busType] - a Gio.BusType constant * @param {Gio.Cancellable} [cancellable] - an optional Gio.Cancellable * @return {Promise} A new DBus connection */ export function newConnection(busType = Gio.BusType.SESSION, cancellable = null) { return new Promise((resolve, reject) => { Gio.DBusConnection.new_for_address( Gio.dbus_address_get_for_bus_sync(busType, cancellable), Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT | Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION, null, cancellable, (connection, res) => { try { resolve(Gio.DBusConnection.new_for_address_finish(res)); } catch (e) { reject(e); } } ); }); }