nativeMessagingHost.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. #!/usr/bin/env gjs
  2. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  3. //
  4. // SPDX-License-Identifier: GPL-2.0-or-later
  5. 'use strict';
  6. imports.gi.versions.Gio = '2.0';
  7. imports.gi.versions.GLib = '2.0';
  8. imports.gi.versions.GObject = '2.0';
  9. const Gio = imports.gi.Gio;
  10. const GLib = imports.gi.GLib;
  11. const GObject = imports.gi.GObject;
  12. const System = imports.system;
  13. const NativeMessagingHost = GObject.registerClass({
  14. GTypeName: 'GSConnectNativeMessagingHost',
  15. }, class NativeMessagingHost extends Gio.Application {
  16. _init() {
  17. super._init({
  18. application_id: 'org.gnome.Shell.Extensions.GSConnect.NativeMessagingHost',
  19. flags: Gio.ApplicationFlags.NON_UNIQUE,
  20. });
  21. }
  22. get devices() {
  23. if (this._devices === undefined)
  24. this._devices = {};
  25. return this._devices;
  26. }
  27. vfunc_activate() {
  28. super.vfunc_activate();
  29. }
  30. vfunc_startup() {
  31. super.vfunc_startup();
  32. this.hold();
  33. // IO Channels
  34. this._stdin = new Gio.DataInputStream({
  35. base_stream: new Gio.UnixInputStream({fd: 0}),
  36. byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN,
  37. });
  38. this._stdout = new Gio.DataOutputStream({
  39. base_stream: new Gio.UnixOutputStream({fd: 1}),
  40. byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN,
  41. });
  42. const source = this._stdin.base_stream.create_source(null);
  43. source.set_callback(this.receive.bind(this));
  44. source.attach(null);
  45. // Device Manager
  46. try {
  47. this._manager = Gio.DBusObjectManagerClient.new_for_bus_sync(
  48. Gio.BusType.SESSION,
  49. Gio.DBusObjectManagerClientFlags.DO_NOT_AUTO_START,
  50. 'org.gnome.Shell.Extensions.GSConnect',
  51. '/org/gnome/Shell/Extensions/GSConnect',
  52. null,
  53. null
  54. );
  55. } catch (e) {
  56. logError(e);
  57. this.quit();
  58. }
  59. // Add currently managed devices
  60. for (const object of this._manager.get_objects()) {
  61. for (const iface of object.get_interfaces())
  62. this._onInterfaceAdded(this._manager, object, iface);
  63. }
  64. // Watch for new and removed devices
  65. this._manager.connect(
  66. 'interface-added',
  67. this._onInterfaceAdded.bind(this)
  68. );
  69. this._manager.connect(
  70. 'object-removed',
  71. this._onObjectRemoved.bind(this)
  72. );
  73. // Watch for device property changes
  74. this._manager.connect(
  75. 'interface-proxy-properties-changed',
  76. this.sendDeviceList.bind(this)
  77. );
  78. // Watch for service restarts
  79. this._manager.connect(
  80. 'notify::name-owner',
  81. this.sendDeviceList.bind(this)
  82. );
  83. this.send({
  84. type: 'connected',
  85. data: (this._manager.name_owner !== null),
  86. });
  87. }
  88. receive() {
  89. try {
  90. // Read the message
  91. const length = this._stdin.read_int32(null);
  92. const bytes = this._stdin.read_bytes(length, null).toArray();
  93. const message = JSON.parse(new TextDecoder().decode(bytes));
  94. // A request for a list of devices
  95. if (message.type === 'devices') {
  96. this.sendDeviceList();
  97. // A request to invoke an action
  98. } else if (message.type === 'share') {
  99. let actionName;
  100. const device = this.devices[message.data.device];
  101. if (device) {
  102. if (message.data.action === 'share')
  103. actionName = 'shareUri';
  104. else if (message.data.action === 'telephony')
  105. actionName = 'shareSms';
  106. device.actions.activate_action(
  107. actionName,
  108. new GLib.Variant('s', message.data.url)
  109. );
  110. }
  111. }
  112. return GLib.SOURCE_CONTINUE;
  113. } catch (e) {
  114. this.quit();
  115. }
  116. }
  117. send(message) {
  118. try {
  119. const data = JSON.stringify(message);
  120. this._stdout.put_int32(data.length, null);
  121. this._stdout.put_string(data, null);
  122. } catch (e) {
  123. this.quit();
  124. }
  125. }
  126. sendDeviceList() {
  127. // Inform the WebExtension we're disconnected from the service
  128. if (this._manager && this._manager.name_owner === null)
  129. return this.send({type: 'connected', data: false});
  130. // Collect all the devices with supported actions
  131. const available = [];
  132. for (const device of Object.values(this.devices)) {
  133. const share = device.actions.get_action_enabled('shareUri');
  134. const telephony = device.actions.get_action_enabled('shareSms');
  135. if (share || telephony) {
  136. available.push({
  137. id: device.g_object_path,
  138. name: device.name,
  139. type: device.type,
  140. share: share,
  141. telephony: telephony,
  142. });
  143. }
  144. }
  145. this.send({type: 'devices', data: available});
  146. }
  147. _proxyGetter(name) {
  148. try {
  149. return this.get_cached_property(name).unpack();
  150. } catch (e) {
  151. return null;
  152. }
  153. }
  154. _onInterfaceAdded(manager, object, iface) {
  155. Object.defineProperties(iface, {
  156. 'name': {
  157. get: this._proxyGetter.bind(iface, 'Name'),
  158. enumerable: true,
  159. },
  160. // TODO: phase this out for icon-name
  161. 'type': {
  162. get: this._proxyGetter.bind(iface, 'Type'),
  163. enumerable: true,
  164. },
  165. });
  166. iface.actions = Gio.DBusActionGroup.get(
  167. iface.g_connection,
  168. iface.g_name,
  169. iface.g_object_path
  170. );
  171. this.devices[iface.g_object_path] = iface;
  172. this.sendDeviceList();
  173. }
  174. _onObjectRemoved(manager, object) {
  175. delete this.devices[object.g_object_path];
  176. this.sendDeviceList();
  177. }
  178. });
  179. // NOTE: must not pass ARGV
  180. (new NativeMessagingHost()).run([System.programInvocationName]);