remote.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. 'use strict';
  5. const Gio = imports.gi.Gio;
  6. const GLib = imports.gi.GLib;
  7. const GObject = imports.gi.GObject;
  8. const SERVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect';
  9. const SERVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect';
  10. const DEVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect.Device';
  11. const DEVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect/Device';
  12. const _PROPERTIES = {
  13. 'Connected': 'connected',
  14. 'EncryptionInfo': 'encryption-info',
  15. 'IconName': 'icon-name',
  16. 'Id': 'id',
  17. 'Name': 'name',
  18. 'Paired': 'paired',
  19. 'Type': 'type',
  20. };
  21. function _proxyInit(proxy, cancellable = null) {
  22. if (proxy.__initialized !== undefined)
  23. return Promise.resolve();
  24. return new Promise((resolve, reject) => {
  25. proxy.init_async(
  26. GLib.PRIORITY_DEFAULT,
  27. cancellable,
  28. (proxy, res) => {
  29. try {
  30. proxy.init_finish(res);
  31. proxy.__initialized = true;
  32. resolve();
  33. } catch (e) {
  34. Gio.DBusError.strip_remote_error(e);
  35. reject(e);
  36. }
  37. }
  38. );
  39. });
  40. }
  41. /**
  42. * A simple proxy wrapper for devices exported over DBus.
  43. */
  44. var Device = GObject.registerClass({
  45. GTypeName: 'GSConnectRemoteDevice',
  46. Implements: [Gio.DBusInterface],
  47. Properties: {
  48. 'connected': GObject.ParamSpec.boolean(
  49. 'connected',
  50. 'Connected',
  51. 'Whether the device is connected',
  52. GObject.ParamFlags.READABLE,
  53. null
  54. ),
  55. 'encryption-info': GObject.ParamSpec.string(
  56. 'encryption-info',
  57. 'Encryption Info',
  58. 'A formatted string with the local and remote fingerprints',
  59. GObject.ParamFlags.READABLE,
  60. null
  61. ),
  62. 'icon-name': GObject.ParamSpec.string(
  63. 'icon-name',
  64. 'Icon Name',
  65. 'Icon name representing the device',
  66. GObject.ParamFlags.READABLE,
  67. null
  68. ),
  69. 'id': GObject.ParamSpec.string(
  70. 'id',
  71. 'deviceId',
  72. 'The device hostname or other unique id',
  73. GObject.ParamFlags.READABLE,
  74. ''
  75. ),
  76. 'name': GObject.ParamSpec.string(
  77. 'name',
  78. 'deviceName',
  79. 'The device name',
  80. GObject.ParamFlags.READABLE,
  81. null
  82. ),
  83. 'paired': GObject.ParamSpec.boolean(
  84. 'paired',
  85. 'Paired',
  86. 'Whether the device is paired',
  87. GObject.ParamFlags.READABLE,
  88. null
  89. ),
  90. 'type': GObject.ParamSpec.string(
  91. 'type',
  92. 'deviceType',
  93. 'The device type',
  94. GObject.ParamFlags.READABLE,
  95. null
  96. ),
  97. },
  98. }, class Device extends Gio.DBusProxy {
  99. _init(service, object_path) {
  100. this._service = service;
  101. super._init({
  102. g_connection: service.g_connection,
  103. g_name: SERVICE_NAME,
  104. g_object_path: object_path,
  105. g_interface_name: DEVICE_NAME,
  106. });
  107. }
  108. vfunc_g_properties_changed(changed, invalidated) {
  109. try {
  110. for (const name in changed.deepUnpack())
  111. this.notify(_PROPERTIES[name]);
  112. } catch (e) {
  113. logError(e);
  114. }
  115. }
  116. _get(name, fallback = null) {
  117. try {
  118. return this.get_cached_property(name).unpack();
  119. } catch (e) {
  120. return fallback;
  121. }
  122. }
  123. get connected() {
  124. return this._get('Connected', false);
  125. }
  126. get encryption_info() {
  127. return this._get('EncryptionInfo', '');
  128. }
  129. get icon_name() {
  130. return this._get('IconName', 'computer');
  131. }
  132. get id() {
  133. return this._get('Id', '0');
  134. }
  135. get name() {
  136. return this._get('Name', 'Unknown');
  137. }
  138. get paired() {
  139. return this._get('Paired', false);
  140. }
  141. get service() {
  142. return this._service;
  143. }
  144. get type() {
  145. return this._get('Type', 'desktop');
  146. }
  147. async start() {
  148. try {
  149. await _proxyInit(this);
  150. // For GActions & GMenu we pass the service's name owner to avoid
  151. // any mixup with instances.
  152. this.action_group = Gio.DBusActionGroup.get(
  153. this.g_connection,
  154. this.service.g_name_owner,
  155. this.g_object_path
  156. );
  157. this.menu = Gio.DBusMenuModel.get(
  158. this.g_connection,
  159. this.service.g_name_owner,
  160. this.g_object_path
  161. );
  162. // Poke the GMenu to ensure it's ready for us
  163. await new Promise((resolve, reject) => {
  164. this.g_connection.call(
  165. SERVICE_NAME,
  166. this.g_object_path,
  167. 'org.gtk.Menus',
  168. 'Start',
  169. new GLib.Variant('(au)', [[0]]),
  170. null,
  171. Gio.DBusCallFlags.NONE,
  172. -1,
  173. null,
  174. (proxy, res) => {
  175. try {
  176. resolve(proxy.call_finish(res));
  177. } catch (e) {
  178. Gio.DBusError.strip_remote_error(e);
  179. reject(e);
  180. }
  181. }
  182. );
  183. });
  184. } catch (e) {
  185. this.destroy();
  186. throw e;
  187. }
  188. }
  189. destroy() {
  190. GObject.signal_handlers_destroy(this);
  191. }
  192. });
  193. /**
  194. * A simple proxy wrapper for the GSConnect service.
  195. */
  196. var Service = GObject.registerClass({
  197. GTypeName: 'GSConnectRemoteService',
  198. Implements: [Gio.DBusInterface],
  199. Properties: {
  200. 'active': GObject.ParamSpec.boolean(
  201. 'active',
  202. 'Active',
  203. 'Whether the service is active',
  204. GObject.ParamFlags.READABLE,
  205. false
  206. ),
  207. },
  208. Signals: {
  209. 'device-added': {
  210. flags: GObject.SignalFlags.RUN_FIRST,
  211. param_types: [Device.$gtype],
  212. },
  213. 'device-removed': {
  214. flags: GObject.SignalFlags.RUN_FIRST,
  215. param_types: [Device.$gtype],
  216. },
  217. },
  218. }, class Service extends Gio.DBusProxy {
  219. _init() {
  220. super._init({
  221. g_bus_type: Gio.BusType.SESSION,
  222. g_name: SERVICE_NAME,
  223. g_object_path: SERVICE_PATH,
  224. g_interface_name: 'org.freedesktop.DBus.ObjectManager',
  225. g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION,
  226. });
  227. this._active = false;
  228. this._devices = new Map();
  229. this._starting = false;
  230. // Watch the service
  231. this._nameOwnerChangedId = this.connect(
  232. 'notify::g-name-owner',
  233. this._onNameOwnerChanged.bind(this)
  234. );
  235. }
  236. get active() {
  237. return this._active;
  238. }
  239. get devices() {
  240. return Array.from(this._devices.values());
  241. }
  242. vfunc_g_signal(sender_name, signal_name, parameters) {
  243. try {
  244. // Don't emit signals until the ObjectManager has started
  245. if (!this.active)
  246. return;
  247. parameters = parameters.deepUnpack();
  248. switch (true) {
  249. case (signal_name === 'InterfacesAdded'):
  250. this._onInterfacesAdded(...parameters);
  251. break;
  252. case (signal_name === 'InterfacesRemoved'):
  253. this._onInterfacesRemoved(...parameters);
  254. break;
  255. }
  256. } catch (e) {
  257. logError(e);
  258. }
  259. }
  260. /**
  261. * org.freedesktop.DBus.ObjectManager.InterfacesAdded
  262. *
  263. * @param {string} object_path - Path interfaces have been added to
  264. * @param {Object} interfaces - A dictionary of interface objects
  265. */
  266. async _onInterfacesAdded(object_path, interfaces) {
  267. try {
  268. // An empty list means only the object has been added
  269. if (Object.values(interfaces).length === 0)
  270. return;
  271. // Skip existing proxies
  272. if (this._devices.has(object_path))
  273. return;
  274. // Create a proxy
  275. const device = new Device(this, object_path);
  276. await device.start();
  277. // Hold the proxy and emit ::device-added
  278. this._devices.set(object_path, device);
  279. this.emit('device-added', device);
  280. } catch (e) {
  281. logError(e, object_path);
  282. }
  283. }
  284. /**
  285. * org.freedesktop.DBus.ObjectManager.InterfacesRemoved
  286. *
  287. * @param {string} object_path - Path interfaces have been removed from
  288. * @param {string[]} interfaces - List of interface names removed
  289. */
  290. _onInterfacesRemoved(object_path, interfaces) {
  291. try {
  292. // An empty interface list means the object is being removed
  293. if (interfaces.length === 0)
  294. return;
  295. // Get the proxy
  296. const device = this._devices.get(object_path);
  297. if (device === undefined)
  298. return;
  299. // Release the proxy and emit ::device-removed
  300. this._devices.delete(object_path);
  301. this.emit('device-removed', device);
  302. // Destroy the device and force disposal
  303. device.destroy();
  304. } catch (e) {
  305. logError(e, object_path);
  306. }
  307. }
  308. async _addDevices() {
  309. const objects = await new Promise((resolve, reject) => {
  310. this.call(
  311. 'GetManagedObjects',
  312. null,
  313. Gio.DBusCallFlags.NONE,
  314. -1,
  315. null,
  316. (proxy, res) => {
  317. try {
  318. const variant = proxy.call_finish(res);
  319. resolve(variant.deepUnpack()[0]);
  320. } catch (e) {
  321. Gio.DBusError.strip_remote_error(e);
  322. reject(e);
  323. }
  324. }
  325. );
  326. });
  327. for (const [object_path, object] of Object.entries(objects))
  328. await this._onInterfacesAdded(object_path, object);
  329. }
  330. _clearDevices() {
  331. for (const [object_path, device] of this._devices) {
  332. this._devices.delete(object_path);
  333. this.emit('device-removed', device);
  334. device.destroy();
  335. }
  336. }
  337. async _onNameOwnerChanged() {
  338. try {
  339. // If the service stopped, remove each device and mark it inactive
  340. if (this.g_name_owner === null) {
  341. this._clearDevices();
  342. this._active = false;
  343. this.notify('active');
  344. // If the service started, mark it active and add each device
  345. } else {
  346. this._active = true;
  347. this.notify('active');
  348. await this._addDevices();
  349. }
  350. } catch (e) {
  351. logError(e);
  352. }
  353. }
  354. /**
  355. * Reload all devices without affecting the remote service. This amounts to
  356. * removing and adding each device while emitting the appropriate signals.
  357. */
  358. async reload() {
  359. try {
  360. if (this._starting === false) {
  361. this._starting = true;
  362. this._clearDevices();
  363. await _proxyInit(this);
  364. await this._onNameOwnerChanged();
  365. this._starting = false;
  366. }
  367. } catch (e) {
  368. this._starting = false;
  369. throw e;
  370. }
  371. }
  372. /**
  373. * Start the service
  374. */
  375. async start() {
  376. try {
  377. if (this._starting === false && this.active === false) {
  378. this._starting = true;
  379. await _proxyInit(this);
  380. await this._onNameOwnerChanged();
  381. // Activate the service if it's not already running
  382. if (!this.active) {
  383. await new Promise((resolve, reject) => {
  384. this.g_connection.call(
  385. SERVICE_NAME,
  386. SERVICE_PATH,
  387. 'org.freedesktop.Application',
  388. 'Activate',
  389. GLib.Variant.new('(a{sv})', [{}]),
  390. null,
  391. Gio.DBusCallFlags.NONE,
  392. -1,
  393. null,
  394. (proxy, res) => {
  395. try {
  396. resolve(proxy.call_finish(res));
  397. } catch (e) {
  398. Gio.DBusError.strip_remote_error(e);
  399. reject(e);
  400. }
  401. }
  402. );
  403. });
  404. }
  405. this._starting = false;
  406. }
  407. } catch (e) {
  408. this._starting = false;
  409. throw e;
  410. }
  411. }
  412. /**
  413. * Stop the service
  414. */
  415. stop() {
  416. if (this.active)
  417. this.activate_action('quit');
  418. }
  419. activate_action(name, parameter = null) {
  420. try {
  421. const paramArray = [];
  422. if (parameter instanceof GLib.Variant)
  423. paramArray[0] = parameter;
  424. const connection = this.g_connection || Gio.DBus.session;
  425. connection.call(
  426. SERVICE_NAME,
  427. SERVICE_PATH,
  428. 'org.freedesktop.Application',
  429. 'ActivateAction',
  430. GLib.Variant.new('(sava{sv})', [name, paramArray, {}]),
  431. null,
  432. Gio.DBusCallFlags.NONE,
  433. -1,
  434. null,
  435. null
  436. );
  437. } catch (e) {
  438. logError(e);
  439. }
  440. }
  441. destroy() {
  442. if (this._nameOwnerChangedId > 0) {
  443. this.disconnect(this._nameOwnerChangedId);
  444. this._nameOwnerChangedId = 0;
  445. this._clearDevices();
  446. this._active = false;
  447. GObject.signal_handlers_destroy(this);
  448. }
  449. }
  450. });