#!/usr/bin/env gjs 'use strict'; imports.gi.versions.Gio = '2.0'; imports.gi.versions.GLib = '2.0'; imports.gi.versions.GObject = '2.0'; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; const System = imports.system; var 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 Gio.UnixInputStream({fd: 0}), byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN }); this.stdout = new Gio.DataOutputStream({ base_stream: new Gio.UnixOutputStream({fd: 1}), byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN }); let source = this.stdin.base_stream.create_source(null); source.set_callback(this.receive.bind(this)); source.attach(null); this._init_async(); } async _init_async(obj, res) { try { this.manager = await new Promise((resolve, reject) => { Gio.DBusObjectManagerClient.new_for_bus( Gio.BusType.SESSION, Gio.DBusObjectManagerClientFlags.DO_NOT_AUTO_START, 'org.gnome.Shell.Extensions.GSConnect', '/org/gnome/Shell/Extensions/GSConnect', null, null, (manager, res) => { try { resolve(Gio.DBusObjectManagerClient.new_for_bus_finish(res)); } catch (e) { reject(e); } } ); }); // Add currently managed devices for (let object of this.manager.get_objects()) { for (let 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: true}); } catch (e) { this.quit(); } } receive() { try { // Read the message let length = this.stdin.read_int32(null); let bytes = this.stdin.read_bytes(length, null).toArray(); let message = JSON.parse(imports.byteArray.toString(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; let 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 true; } catch (e) { this.quit(); } } send(message) { try { let 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) { this.send({type: 'connected', data: false}); return; } let available = []; for (let device of Object.values(this.devices)) { let share = device.actions.get_action_enabled('shareUri'); let 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 (new NativeMessagingHost()).run([System.programInvocationName]);