// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import * as AppIndicator from './appIndicator.js'; import * as IndicatorStatusIcon from './indicatorStatusIcon.js'; import * as Interfaces from './interfaces.js'; import * as PromiseUtils from './promiseUtils.js'; import * as Util from './util.js'; import * as DBusUtils from './dbusUtils.js'; import * as DBusMenu from './dbusMenu.js'; import {DBusProxy} from './dbusProxy.js'; Gio._promisify(Gio.Subprocess.prototype, 'wait_async'); Gio._promisify(Gio.Subprocess.prototype, 'communicate_async'); Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async', 'read_line_finish_utf8'); // TODO: replace with org.freedesktop and /org/freedesktop when approved const KDE_PREFIX = 'org.kde'; export const WATCHER_BUS_NAME = `${KDE_PREFIX}.StatusNotifierWatcher`; const WATCHER_OBJECT = '/StatusNotifierWatcher'; const DEFAULT_ITEM_OBJECT_PATH = '/StatusNotifierItem'; /* * The StatusNotifierWatcher class implements the StatusNotifierWatcher dbus object */ export class StatusNotifierWatcher { constructor(extension, watchDog) { this._watchDog = watchDog; this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(Interfaces.StatusNotifierWatcher, this); try { this._dbusImpl.export(Gio.DBus.session, WATCHER_OBJECT); } catch (e) { Util.Logger.warn(`Failed to export ${WATCHER_OBJECT}`); logError(e); } this._cancellable = new Gio.Cancellable(); this._everAcquiredName = false; this._ownName = Gio.DBus.session.own_name(WATCHER_BUS_NAME, Gio.BusNameOwnerFlags.NONE, this._acquiredName.bind(this), this._lostName.bind(this)); this._items = new Map(); try { this._dbusImpl.emit_signal('StatusNotifierHostRegistered', null); } catch (e) { Util.Logger.warn(`Failed to notify registered host ${WATCHER_OBJECT}`); } this._seekStatusNotifierItems(extension).catch(e => { if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) logError(e, 'Looking for StatusNotifierItem\'s'); }); } _acquiredName() { this._everAcquiredName = true; this._watchDog.nameAcquired = true; } _lostName() { if (this._everAcquiredName) Util.Logger.debug(`Lost name${WATCHER_BUS_NAME}`); else Util.Logger.warn(`Failed to acquire ${WATCHER_BUS_NAME}`); this._watchDog.nameAcquired = false; } async _registerItem(service, busName, objPath) { const id = Util.indicatorId(service, busName, objPath); if (this._items.has(id)) { Util.Logger.warn(`Item ${id} is already registered`); return; } Util.Logger.debug(`Registering StatusNotifierItem ${id}`); try { const indicator = new AppIndicator.AppIndicator(service, busName, objPath); this._items.set(id, indicator); indicator.connect('destroy', () => this._onIndicatorDestroyed(indicator)); indicator.connect('name-owner-changed', async () => { if (!indicator.hasNameOwner) { try { await new PromiseUtils.TimeoutPromise(500, GLib.PRIORITY_DEFAULT, this._cancellable); if (this._items.has(id) && !indicator.hasNameOwner) indicator.destroy(); } catch (e) { if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) logError(e); } } }); // if the desktop is not ready delay the icon creation and signal emissions await Util.waitForStartupCompletion(indicator.cancellable); const statusIcon = new IndicatorStatusIcon.IndicatorStatusIcon(indicator); IndicatorStatusIcon.addIconToPanel(statusIcon); this._dbusImpl.emit_signal('StatusNotifierItemRegistered', GLib.Variant.new('(s)', [indicator.uniqueId])); this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems', GLib.Variant.new('as', this.RegisteredStatusNotifierItems)); } catch (e) { if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) logError(e); throw e; } } async _ensureItemRegistered(service, busName, objPath) { const id = Util.indicatorId(service, busName, objPath); const item = this._items.get(id); if (item) { // delete the old one and add the new indicator Util.Logger.debug(`Attempting to re-register ${id}; resetting instead`); item.reset(); return; } await this._registerItem(service, busName, objPath); } async _seekStatusNotifierItems(extension) { // Some indicators (*coff*, dropbox, *coff*) do not re-register again // when the plugin is enabled/disabled, thus we need to manually look // for the objects in the session bus that implements the // StatusNotifierItem interface... However let's do it after a low // priority timeout, and using an external process so that it won't // affect startup or memory (as it seems that gjs is not great at // handling the memory of the bus analyzer async code). const cancellable = this._cancellable; await new PromiseUtils.TimeoutSecondsPromise(2, GLib.PRIORITY_LOW, cancellable); const busAnalyzer = GLib.build_filenamev([ extension.path, 'tools', 'busAnalyzer.js', ]); const subProcess = Gio.Subprocess.new(['gjs', '-m', busAnalyzer], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE); const stdOut = subProcess.get_stdout_pipe(); const dataInputStream = new Gio.DataInputStream({base_stream: stdOut}); const textDecoder = new TextDecoder(); while (true) { // eslint-disable-next-line no-await-in-loop const [line] = await dataInputStream.read_line_async(GLib.PRIORITY_DEFAULT, cancellable); if (!line) break; try { const {services, name, path} = JSON.parse(textDecoder.decode(line)); const ids = [null, ...services].map(s => Util.indicatorId(s, name, path)); if (ids.every(id => !this._items.has(id))) { const service = services.find(s => s && s.startsWith('org.kde.StatusNotifierItem')) || services[0]; const id = Util.indicatorId( path === DEFAULT_ITEM_OBJECT_PATH ? service : null, name, path); Util.Logger.warn(`Using Brute-force mode for StatusNotifierItem ${id}`); // eslint-disable-next-line no-await-in-loop await this._registerItem(service, name, path); } } catch (e) { logError(e); } } const [, stdErr] = await subProcess.communicate_async(null, cancellable); await subProcess.wait_async(cancellable); if (subProcess.get_exit_status() !== 0) { const errorLines = textDecoder.decode(stdErr.toArray()).split('\n'); const error = new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED, errorLines[0]); error.stack = `${errorLines.slice(3).join('\n')}${error.stack}`; throw error; } } async RegisterStatusNotifierItemAsync(params, invocation) { // it would be too easy if all application behaved the same // instead, ayatana patched gnome apps to send a path // while kde apps send a bus name const [service] = params; let busName, objPath; if (service.charAt(0) === '/') { // looks like a path busName = invocation.get_sender(); objPath = service; } else if (service.match(DBusUtils.BUS_ADDRESS_REGEX)) { try { busName = await DBusUtils.getUniqueBusName(invocation.get_connection(), service, this._cancellable); } catch (e) { logError(e); } objPath = DEFAULT_ITEM_OBJECT_PATH; } if (!busName || !objPath) { const error = `Impossible to register an indicator for parameters '${ service.toString()}'`; Util.Logger.warn(error); invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError', error); return; } try { await this._ensureItemRegistered(service, busName, objPath); invocation.return_value(null); } catch (e) { if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) logError(e); invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError', e.message); } } _onIndicatorDestroyed(indicator) { const {uniqueId} = indicator; this._items.delete(uniqueId); try { this._dbusImpl.emit_signal('StatusNotifierItemUnregistered', GLib.Variant.new('(s)', [uniqueId])); this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems', GLib.Variant.new('as', this.RegisteredStatusNotifierItems)); } catch (e) { Util.Logger.warn(`Failed to emit signals: ${e}`); } } RegisterStatusNotifierHostAsync(_service, invocation) { invocation.return_error_literal( Gio.DBusError, Gio.DBusError.NOT_SUPPORTED, 'Registering additional notification hosts is not supported'); } IsNotificationHostRegistered() { return true; } get RegisteredStatusNotifierItems() { return Array.from(this._items.values()).map(i => i.uniqueId); } get IsStatusNotifierHostRegistered() { return true; } get ProtocolVersion() { return 0; } destroy() { if (this._isDestroyed) return; // this doesn't do any sync operation and doesn't allow us to hook up // the event of being finished which results in our unholy debounce hack // (see extension.js) this._items.forEach(indicator => indicator.destroy()); this._cancellable.cancel(); try { this._dbusImpl.emit_signal('StatusNotifierHostUnregistered', null); } catch (e) { Util.Logger.warn(`Failed to emit uinregistered signal: ${e}`); } Gio.DBus.session.unown_name(this._ownName); try { this._dbusImpl.unexport(); } catch (e) { Util.Logger.warn(`Failed to unexport watcher object: ${e}`); } DBusMenu.DBusClient.destroy(); AppIndicator.AppIndicatorProxy.destroy(); DBusProxy.destroy(); Util.destroyDefaultTheme(); this._dbusImpl.run_dispose(); delete this._dbusImpl; delete this._items; this._isDestroyed = true; } }