statusNotifierWatcher.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. // This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension
  2. //
  3. // This program is free software; you can redistribute it and/or
  4. // modify it under the terms of the GNU General Public License
  5. // as published by the Free Software Foundation; either version 2
  6. // of the License, or (at your option) any later version.
  7. //
  8. // This program is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with this program; if not, write to the Free Software
  15. // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. import Gio from 'gi://Gio';
  17. import GLib from 'gi://GLib';
  18. import * as AppIndicator from './appIndicator.js';
  19. import * as IndicatorStatusIcon from './indicatorStatusIcon.js';
  20. import * as Interfaces from './interfaces.js';
  21. import * as PromiseUtils from './promiseUtils.js';
  22. import * as Util from './util.js';
  23. import * as DBusMenu from './dbusMenu.js';
  24. import {DBusProxy} from './dbusProxy.js';
  25. // TODO: replace with org.freedesktop and /org/freedesktop when approved
  26. const KDE_PREFIX = 'org.kde';
  27. export const WATCHER_BUS_NAME = `${KDE_PREFIX}.StatusNotifierWatcher`;
  28. const WATCHER_OBJECT = '/StatusNotifierWatcher';
  29. const DEFAULT_ITEM_OBJECT_PATH = '/StatusNotifierItem';
  30. /*
  31. * The StatusNotifierWatcher class implements the StatusNotifierWatcher dbus object
  32. */
  33. export class StatusNotifierWatcher {
  34. constructor(watchDog) {
  35. this._watchDog = watchDog;
  36. this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(Interfaces.StatusNotifierWatcher, this);
  37. try {
  38. this._dbusImpl.export(Gio.DBus.session, WATCHER_OBJECT);
  39. } catch (e) {
  40. Util.Logger.warn(`Failed to export ${WATCHER_OBJECT}`);
  41. logError(e);
  42. }
  43. this._cancellable = new Gio.Cancellable();
  44. this._everAcquiredName = false;
  45. this._ownName = Gio.DBus.session.own_name(WATCHER_BUS_NAME,
  46. Gio.BusNameOwnerFlags.NONE,
  47. this._acquiredName.bind(this),
  48. this._lostName.bind(this));
  49. this._items = new Map();
  50. try {
  51. this._dbusImpl.emit_signal('StatusNotifierHostRegistered', null);
  52. } catch (e) {
  53. Util.Logger.warn(`Failed to notify registered host ${WATCHER_OBJECT}`);
  54. }
  55. this._seekStatusNotifierItems().catch(e => {
  56. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  57. logError(e, 'Looking for StatusNotifierItem\'s');
  58. });
  59. }
  60. _acquiredName() {
  61. this._everAcquiredName = true;
  62. this._watchDog.nameAcquired = true;
  63. }
  64. _lostName() {
  65. if (this._everAcquiredName)
  66. Util.Logger.debug(`Lost name${WATCHER_BUS_NAME}`);
  67. else
  68. Util.Logger.warn(`Failed to acquire ${WATCHER_BUS_NAME}`);
  69. this._watchDog.nameAcquired = false;
  70. }
  71. async _registerItem(service, busName, objPath) {
  72. const id = Util.indicatorId(service, busName, objPath);
  73. if (this._items.has(id)) {
  74. Util.Logger.warn(`Item ${id} is already registered`);
  75. return;
  76. }
  77. Util.Logger.debug(`Registering StatusNotifierItem ${id}`);
  78. try {
  79. const indicator = new AppIndicator.AppIndicator(service, busName, objPath);
  80. this._items.set(id, indicator);
  81. indicator.connect('destroy', () => this._onIndicatorDestroyed(indicator));
  82. indicator.connect('name-owner-changed', async () => {
  83. if (!indicator.hasNameOwner) {
  84. try {
  85. await new PromiseUtils.TimeoutPromise(500,
  86. GLib.PRIORITY_DEFAULT, this._cancellable);
  87. if (this._items.has(id) && !indicator.hasNameOwner)
  88. indicator.destroy();
  89. } catch (e) {
  90. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  91. logError(e);
  92. }
  93. }
  94. });
  95. // if the desktop is not ready delay the icon creation and signal emissions
  96. await Util.waitForStartupCompletion(indicator.cancellable);
  97. const statusIcon = new IndicatorStatusIcon.IndicatorStatusIcon(indicator);
  98. IndicatorStatusIcon.addIconToPanel(statusIcon);
  99. this._dbusImpl.emit_signal('StatusNotifierItemRegistered',
  100. GLib.Variant.new('(s)', [indicator.uniqueId]));
  101. this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems',
  102. GLib.Variant.new('as', this.RegisteredStatusNotifierItems));
  103. } catch (e) {
  104. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  105. logError(e);
  106. throw e;
  107. }
  108. }
  109. async _ensureItemRegistered(service, busName, objPath) {
  110. const id = Util.indicatorId(service, busName, objPath);
  111. const item = this._items.get(id);
  112. if (item) {
  113. // delete the old one and add the new indicator
  114. Util.Logger.debug(`Attempting to re-register ${id}; resetting instead`);
  115. item.reset();
  116. return;
  117. }
  118. await this._registerItem(service, busName, objPath);
  119. }
  120. async _seekStatusNotifierItems() {
  121. // Some indicators (*coff*, dropbox, *coff*) do not re-register again
  122. // when the plugin is enabled/disabled, thus we need to manually look
  123. // for the objects in the session bus that implements the
  124. // StatusNotifierItem interface... However let's do it after a low
  125. // priority idle, so that it won't affect startup.
  126. const cancellable = this._cancellable;
  127. const bus = Gio.DBus.session;
  128. const uniqueNames = await Util.getBusNames(bus, cancellable);
  129. const introspectName = async name => {
  130. const nodes = Util.introspectBusObject(bus, name, cancellable,
  131. ['org.kde.StatusNotifierItem']);
  132. const services = [...uniqueNames.get(name)];
  133. for await (const node of nodes) {
  134. const {path} = node;
  135. const ids = services.map(s => Util.indicatorId(s, name, path));
  136. if (ids.every(id => !this._items.has(id))) {
  137. const service = services.find(s =>
  138. s && s.startsWith('org.kde.StatusNotifierItem')) || services[0];
  139. const id = Util.indicatorId(
  140. path === DEFAULT_ITEM_OBJECT_PATH ? service : null,
  141. name, path);
  142. Util.Logger.warn(`Using Brute-force mode for StatusNotifierItem ${id}`);
  143. this._registerItem(service, name, path);
  144. }
  145. }
  146. };
  147. await Promise.allSettled([...uniqueNames.keys()].map(n => introspectName(n)));
  148. }
  149. async RegisterStatusNotifierItemAsync(params, invocation) {
  150. // it would be too easy if all application behaved the same
  151. // instead, ayatana patched gnome apps to send a path
  152. // while kde apps send a bus name
  153. const [service] = params;
  154. let busName, objPath;
  155. if (service.charAt(0) === '/') { // looks like a path
  156. busName = invocation.get_sender();
  157. objPath = service;
  158. } else if (service.match(Util.BUS_ADDRESS_REGEX)) {
  159. try {
  160. busName = await Util.getUniqueBusName(invocation.get_connection(),
  161. service, this._cancellable);
  162. } catch (e) {
  163. logError(e);
  164. }
  165. objPath = DEFAULT_ITEM_OBJECT_PATH;
  166. }
  167. if (!busName || !objPath) {
  168. const error = `Impossible to register an indicator for parameters '${
  169. service.toString()}'`;
  170. Util.Logger.warn(error);
  171. invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError',
  172. error);
  173. return;
  174. }
  175. try {
  176. await this._ensureItemRegistered(service, busName, objPath);
  177. invocation.return_value(null);
  178. } catch (e) {
  179. if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
  180. logError(e);
  181. invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError',
  182. e.message);
  183. }
  184. }
  185. _onIndicatorDestroyed(indicator) {
  186. const {uniqueId} = indicator;
  187. this._items.delete(uniqueId);
  188. try {
  189. this._dbusImpl.emit_signal('StatusNotifierItemUnregistered',
  190. GLib.Variant.new('(s)', [uniqueId]));
  191. this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems',
  192. GLib.Variant.new('as', this.RegisteredStatusNotifierItems));
  193. } catch (e) {
  194. Util.Logger.warn(`Failed to emit signals: ${e}`);
  195. }
  196. }
  197. RegisterStatusNotifierHostAsync(_service, invocation) {
  198. invocation.return_error_literal(
  199. Gio.DBusError,
  200. Gio.DBusError.NOT_SUPPORTED,
  201. 'Registering additional notification hosts is not supported');
  202. }
  203. IsNotificationHostRegistered() {
  204. return true;
  205. }
  206. get RegisteredStatusNotifierItems() {
  207. return Array.from(this._items.values()).map(i => i.uniqueId);
  208. }
  209. get IsStatusNotifierHostRegistered() {
  210. return true;
  211. }
  212. get ProtocolVersion() {
  213. return 0;
  214. }
  215. destroy() {
  216. if (this._isDestroyed)
  217. return;
  218. // this doesn't do any sync operation and doesn't allow us to hook up
  219. // the event of being finished which results in our unholy debounce hack
  220. // (see extension.js)
  221. this._items.forEach(indicator => indicator.destroy());
  222. this._cancellable.cancel();
  223. try {
  224. this._dbusImpl.emit_signal('StatusNotifierHostUnregistered', null);
  225. } catch (e) {
  226. Util.Logger.warn(`Failed to emit uinregistered signal: ${e}`);
  227. }
  228. Gio.DBus.session.unown_name(this._ownName);
  229. try {
  230. this._dbusImpl.unexport();
  231. } catch (e) {
  232. Util.Logger.warn(`Failed to unexport watcher object: ${e}`);
  233. }
  234. DBusMenu.DBusClient.destroy();
  235. AppIndicator.AppIndicatorProxy.destroy();
  236. DBusProxy.destroy();
  237. Util.destroyDefaultTheme();
  238. this._dbusImpl.run_dispose();
  239. delete this._dbusImpl;
  240. delete this._items;
  241. this._isDestroyed = true;
  242. }
  243. }