#!/usr/bin/env -S gjs -m // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect // // SPDX-License-Identifier: GPL-2.0-or-later import Gio from 'gi://Gio?version=2.0'; import GLib from 'gi://GLib?version=2.0'; import GObject from 'gi://GObject?version=2.0'; import system from 'system'; // Retain compatibility with GLib < 2.80, which lacks GioUnix let GioUnix; try { GioUnix = (await import('gi://GioUnix?version=2.0')).default; } catch (e) { GioUnix = { InputStream: Gio.UnixInputStream, OutputStream: Gio.UnixOutputStream, }; } const NativeMessagingHost = GObject.registerClass({ GTypeName: 'GSConnectNativeMessagingHost', }, class NativeMessagingHost extends Gio.Application { _init() { super._init({ application_id: 'org.gnome.Shell.Extensions.GSConnect.NativeMessagingHost', flags: Gio.ApplicationFlags.NON_UNIQUE, }); } get devices() { if (this._devices === undefined) this._devices = {}; return this._devices; } vfunc_activate() { super.vfunc_activate(); } vfunc_startup() { super.vfunc_startup(); this.hold(); // IO Channels this._stdin = new Gio.DataInputStream({ base_stream: new GioUnix.InputStream({fd: 0}), byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN, }); this._stdout = new Gio.DataOutputStream({ base_stream: new GioUnix.OutputStream({fd: 1}), byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN, }); const source = this._stdin.base_stream.create_source(null); source.set_callback(this.receive.bind(this)); source.attach(null); // Device Manager try { this._manager = Gio.DBusObjectManagerClient.new_for_bus_sync( Gio.BusType.SESSION, Gio.DBusObjectManagerClientFlags.DO_NOT_AUTO_START, 'org.gnome.Shell.Extensions.GSConnect', '/org/gnome/Shell/Extensions/GSConnect', null, null ); } catch (e) { logError(e); this.quit(); } // Add currently managed devices for (const object of this._manager.get_objects()) { for (const iface of object.get_interfaces()) this._onInterfaceAdded(this._manager, object, iface); } // Watch for new and removed devices this._manager.connect( 'interface-added', this._onInterfaceAdded.bind(this) ); this._manager.connect( 'object-removed', this._onObjectRemoved.bind(this) ); // Watch for device property changes this._manager.connect( 'interface-proxy-properties-changed', this.sendDeviceList.bind(this) ); // Watch for service restarts this._manager.connect( 'notify::name-owner', this.sendDeviceList.bind(this) ); this.send({ type: 'connected', data: (this._manager.name_owner !== null), }); } receive() { try { // Read the message const length = this._stdin.read_int32(null); const bytes = this._stdin.read_bytes(length, null).toArray(); const message = JSON.parse(new TextDecoder().decode(bytes)); // A request for a list of devices if (message.type === 'devices') { this.sendDeviceList(); // A request to invoke an action } else if (message.type === 'share') { let actionName; const device = this.devices[message.data.device]; if (device) { if (message.data.action === 'share') actionName = 'shareUri'; else if (message.data.action === 'telephony') actionName = 'shareSms'; device.actions.activate_action( actionName, new GLib.Variant('s', message.data.url) ); } } return GLib.SOURCE_CONTINUE; } catch (e) { this.quit(); } } send(message) { try { const data = JSON.stringify(message); this._stdout.put_int32(data.length, null); this._stdout.put_string(data, null); } catch (e) { this.quit(); } } sendDeviceList() { // Inform the WebExtension we're disconnected from the service if (this._manager && this._manager.name_owner === null) return this.send({type: 'connected', data: false}); // Collect all the devices with supported actions const available = []; for (const device of Object.values(this.devices)) { const share = device.actions.get_action_enabled('shareUri'); const telephony = device.actions.get_action_enabled('shareSms'); if (share || telephony) { available.push({ id: device.g_object_path, name: device.name, type: device.type, share: share, telephony: telephony, }); } } this.send({type: 'devices', data: available}); } _proxyGetter(name) { try { return this.get_cached_property(name).unpack(); } catch (e) { return null; } } _onInterfaceAdded(manager, object, iface) { Object.defineProperties(iface, { 'name': { get: this._proxyGetter.bind(iface, 'Name'), enumerable: true, }, // TODO: phase this out for icon-name 'type': { get: this._proxyGetter.bind(iface, 'Type'), enumerable: true, }, }); iface.actions = Gio.DBusActionGroup.get( iface.g_connection, iface.g_name, iface.g_object_path ); this.devices[iface.g_object_path] = iface; this.sendDeviceList(); } _onObjectRemoved(manager, object) { delete this.devices[object.g_object_path]; this.sendDeviceList(); } }); // NOTE: must not pass ARGV await (new NativeMessagingHost()).runAsync([system.programInvocationName]);