nativeMessagingHost.js 6.4 KB

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