util.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  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 GObject from 'gi://GObject';
  19. import St from 'gi://St';
  20. const ByteArray = imports.byteArray;
  21. import * as Main from 'resource:///org/gnome/shell/ui/main.js';
  22. import * as Config from 'resource:///org/gnome/shell/misc/config.js';
  23. import * as Signals from 'resource:///org/gnome/shell/misc/signals.js';
  24. import {BaseStatusIcon} from './indicatorStatusIcon.js';
  25. export const BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/;
  26. Gio._promisify(Gio.DBusConnection.prototype, 'call');
  27. Gio._promisify(Gio._LocalFilePrototype, 'read');
  28. Gio._promisify(Gio.InputStream.prototype, 'read_bytes_async');
  29. export function indicatorId(service, busName, objectPath) {
  30. if (service !== busName && service?.match(BUS_ADDRESS_REGEX))
  31. return service;
  32. return `${busName}@${objectPath}`;
  33. }
  34. export async function getUniqueBusName(bus, name, cancellable) {
  35. if (name[0] === ':')
  36. return name;
  37. if (!bus)
  38. bus = Gio.DBus.session;
  39. const variantName = new GLib.Variant('(s)', [name]);
  40. const [unique] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
  41. 'GetNameOwner', variantName, new GLib.VariantType('(s)'),
  42. Gio.DBusCallFlags.NONE, -1, cancellable)).deep_unpack();
  43. return unique;
  44. }
  45. export async function getBusNames(bus, cancellable) {
  46. if (!bus)
  47. bus = Gio.DBus.session;
  48. const [names] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus',
  49. 'ListNames', null, new GLib.VariantType('(as)'), Gio.DBusCallFlags.NONE,
  50. -1, cancellable)).deep_unpack();
  51. const uniqueNames = new Map();
  52. const requests = names.map(name => getUniqueBusName(bus, name, cancellable));
  53. const results = await Promise.allSettled(requests);
  54. for (let i = 0; i < results.length; i++) {
  55. const result = results[i];
  56. if (result.status === 'fulfilled') {
  57. let namesForBus = uniqueNames.get(result.value);
  58. if (!namesForBus) {
  59. namesForBus = new Set();
  60. uniqueNames.set(result.value, namesForBus);
  61. }
  62. namesForBus.add(result.value !== names[i] ? names[i] : null);
  63. } else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
  64. Logger.debug(`Impossible to get the unique name of ${names[i]}: ${result.reason}`);
  65. }
  66. }
  67. return uniqueNames;
  68. }
  69. async function getProcessId(connectionName, cancellable = null, bus = Gio.DBus.session) {
  70. const res = await bus.call('org.freedesktop.DBus', '/',
  71. 'org.freedesktop.DBus', 'GetConnectionUnixProcessID',
  72. new GLib.Variant('(s)', [connectionName]),
  73. new GLib.VariantType('(u)'),
  74. Gio.DBusCallFlags.NONE,
  75. -1,
  76. cancellable);
  77. const [pid] = res.deepUnpack();
  78. return pid;
  79. }
  80. export async function getProcessName(connectionName, cancellable = null,
  81. priority = GLib.PRIORITY_DEFAULT, bus = Gio.DBus.session) {
  82. const pid = await getProcessId(connectionName, cancellable, bus);
  83. const cmdFile = Gio.File.new_for_path(`/proc/${pid}/cmdline`);
  84. const inputStream = await cmdFile.read_async(priority, cancellable);
  85. const bytes = await inputStream.read_bytes_async(2048, priority, cancellable);
  86. return ByteArray.toString(bytes.toArray().map(v => !v ? 0x20 : v));
  87. }
  88. export async function* introspectBusObject(bus, name, cancellable,
  89. interfaces = undefined, path = undefined) {
  90. if (!path)
  91. path = '/';
  92. const [introspection] = (await bus.call(name, path, 'org.freedesktop.DBus.Introspectable',
  93. 'Introspect', null, new GLib.VariantType('(s)'), Gio.DBusCallFlags.NONE,
  94. 5000, cancellable)).deep_unpack();
  95. const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspection);
  96. if (!interfaces || dbusNodeImplementsInterfaces(nodeInfo, interfaces))
  97. yield {nodeInfo, path};
  98. if (path === '/')
  99. path = '';
  100. for (const subNodeInfo of nodeInfo.nodes) {
  101. const subPath = `${path}/${subNodeInfo.path}`;
  102. yield* introspectBusObject(bus, name, cancellable, interfaces, subPath);
  103. }
  104. }
  105. function dbusNodeImplementsInterfaces(nodeInfo, interfaces) {
  106. if (!(nodeInfo instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces))
  107. return false;
  108. return interfaces.some(iface => nodeInfo.lookup_interface(iface));
  109. }
  110. export class NameWatcher extends Signals.EventEmitter {
  111. constructor(name) {
  112. super();
  113. this._watcherId = Gio.DBus.session.watch_name(name,
  114. Gio.BusNameWatcherFlags.NONE, () => {
  115. this._nameOnBus = true;
  116. Logger.debug(`Name ${name} appeared`);
  117. this.emit('changed');
  118. this.emit('appeared');
  119. }, () => {
  120. this._nameOnBus = false;
  121. Logger.debug(`Name ${name} vanished`);
  122. this.emit('changed');
  123. this.emit('vanished');
  124. });
  125. }
  126. destroy() {
  127. this.emit('destroy');
  128. Gio.DBus.session.unwatch_name(this._watcherId);
  129. delete this._watcherId;
  130. }
  131. get nameOnBus() {
  132. return !!this._nameOnBus;
  133. }
  134. }
  135. function connectSmart3A(src, signal, handler) {
  136. const id = src.connect(signal, handler);
  137. let destroyId = 0;
  138. if (src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src))) {
  139. destroyId = src.connect('destroy', () => {
  140. src.disconnect(id);
  141. src.disconnect(destroyId);
  142. });
  143. }
  144. return [id, destroyId];
  145. }
  146. function connectSmart4A(src, signal, target, method) {
  147. if (typeof method !== 'function')
  148. throw new TypeError('Unsupported function');
  149. method = method.bind(target);
  150. const signalId = src.connect(signal, method);
  151. const onDestroy = () => {
  152. src.disconnect(signalId);
  153. if (srcDestroyId)
  154. src.disconnect(srcDestroyId);
  155. if (tgtDestroyId)
  156. target.disconnect(tgtDestroyId);
  157. };
  158. // GObject classes might or might not have a destroy signal
  159. // JS Classes will not complain when connecting to non-existent signals
  160. const srcDestroyId = src.connect && (!(src instanceof GObject.Object) ||
  161. GObject.signal_lookup('destroy', src)) ? src.connect('destroy', onDestroy) : 0;
  162. const tgtDestroyId = target.connect && (!(target instanceof GObject.Object) ||
  163. GObject.signal_lookup('destroy', target)) ? target.connect('destroy', onDestroy) : 0;
  164. return [signalId, srcDestroyId, tgtDestroyId];
  165. }
  166. // eslint-disable-next-line valid-jsdoc
  167. /**
  168. * Connect signals to slots, and remove the connection when either source or
  169. * target are destroyed
  170. *
  171. * Usage:
  172. * Util.connectSmart(srcOb, 'signal', tgtObj, 'handler')
  173. * or
  174. * Util.connectSmart(srcOb, 'signal', () => { ... })
  175. */
  176. export function connectSmart(...args) {
  177. if (arguments.length === 4)
  178. return connectSmart4A(...args);
  179. else
  180. return connectSmart3A(...args);
  181. }
  182. function disconnectSmart3A(src, signalIds) {
  183. const [id, destroyId] = signalIds;
  184. src.disconnect(id);
  185. if (destroyId)
  186. src.disconnect(destroyId);
  187. }
  188. function disconnectSmart4A(src, tgt, signalIds) {
  189. const [signalId, srcDestroyId, tgtDestroyId] = signalIds;
  190. disconnectSmart3A(src, [signalId, srcDestroyId]);
  191. if (tgtDestroyId)
  192. tgt.disconnect(tgtDestroyId);
  193. }
  194. export function disconnectSmart(...args) {
  195. if (arguments.length === 2)
  196. return disconnectSmart3A(...args);
  197. else if (arguments.length === 3)
  198. return disconnectSmart4A(...args);
  199. throw new TypeError('Unexpected number of arguments');
  200. }
  201. let _defaultTheme;
  202. export function getDefaultTheme() {
  203. if (_defaultTheme)
  204. return _defaultTheme;
  205. _defaultTheme = new St.IconTheme();
  206. return _defaultTheme;
  207. }
  208. export function destroyDefaultTheme() {
  209. _defaultTheme = null;
  210. }
  211. // eslint-disable-next-line valid-jsdoc
  212. /**
  213. * Helper function to wait for the system startup to be completed.
  214. * Adding widgets before the desktop is ready to accept them can result in errors.
  215. */
  216. export async function waitForStartupCompletion(cancellable) {
  217. if (Main.layoutManager._startingUp)
  218. await Main.layoutManager.connect_once('startup-complete', cancellable);
  219. }
  220. /**
  221. * Helper class for logging stuff
  222. */
  223. export class Logger {
  224. static _logStructured(logLevel, message, extraFields = {}) {
  225. if (!Object.values(GLib.LogLevelFlags).includes(logLevel)) {
  226. Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING,
  227. 'logLevel is not a valid GLib.LogLevelFlags');
  228. return;
  229. }
  230. if (!Logger._levels.includes(logLevel))
  231. return;
  232. let fields = {
  233. 'SYSLOG_IDENTIFIER': this.uuid,
  234. 'MESSAGE': `${message}`,
  235. };
  236. let thisFile = null;
  237. const {stack} = new Error();
  238. for (let stackLine of stack.split('\n')) {
  239. stackLine = stackLine.replace('resource:///org/gnome/Shell/', '');
  240. const [code, line] = stackLine.split(':');
  241. const [func, file] = code.split(/@(.+)/);
  242. if (!thisFile || thisFile === file) {
  243. thisFile = file;
  244. continue;
  245. }
  246. fields = Object.assign(fields, {
  247. 'CODE_FILE': file || '',
  248. 'CODE_LINE': line || '',
  249. 'CODE_FUNC': func || '',
  250. });
  251. break;
  252. }
  253. GLib.log_structured(Logger._domain, logLevel, Object.assign(fields, extraFields));
  254. }
  255. static init(extension) {
  256. if (Logger._domain)
  257. return;
  258. const allLevels = Object.values(GLib.LogLevelFlags);
  259. const domains = GLib.getenv('G_MESSAGES_DEBUG');
  260. const {name: domain} = extension.metadata;
  261. this.uuid = extension.metadata.uuid;
  262. Logger._domain = domain.replaceAll(' ', '-');
  263. if (domains === 'all' || (domains && domains.split(' ').includes(Logger._domain))) {
  264. Logger._levels = allLevels;
  265. } else {
  266. Logger._levels = allLevels.filter(
  267. l => l <= GLib.LogLevelFlags.LEVEL_WARNING);
  268. }
  269. }
  270. static debug(message) {
  271. Logger._logStructured(GLib.LogLevelFlags.LEVEL_DEBUG, message);
  272. }
  273. static message(message) {
  274. Logger._logStructured(GLib.LogLevelFlags.LEVEL_MESSAGE, message);
  275. }
  276. static warn(message) {
  277. Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING, message);
  278. }
  279. static error(message) {
  280. Logger._logStructured(GLib.LogLevelFlags.LEVEL_ERROR, message);
  281. }
  282. static critical(message) {
  283. Logger._logStructured(GLib.LogLevelFlags.LEVEL_CRITICAL, message);
  284. }
  285. }
  286. export function versionCheck(required) {
  287. const current = Config.PACKAGE_VERSION;
  288. const currentArray = current.split('.');
  289. const [major] = currentArray;
  290. return major >= required;
  291. }
  292. export function tryCleanupOldIndicators() {
  293. const indicatorType = BaseStatusIcon;
  294. const indicators = Object.values(Main.panel.statusArea).filter(i => i instanceof indicatorType);
  295. try {
  296. const panelBoxes = [
  297. Main.panel._leftBox, Main.panel._centerBox, Main.panel._rightBox,
  298. ];
  299. panelBoxes.forEach(box =>
  300. indicators.push(...box.get_children().filter(i => i instanceof indicatorType)));
  301. } catch (e) {
  302. logError(e);
  303. }
  304. new Set(indicators).forEach(i => i.destroy());
  305. }
  306. export const CancellableChild = GObject.registerClass({
  307. Properties: {
  308. 'parent': GObject.ParamSpec.object(
  309. 'parent', 'parent', 'parent',
  310. GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
  311. Gio.Cancellable.$gtype),
  312. },
  313. },
  314. class CancellableChild extends Gio.Cancellable {
  315. _init(parent) {
  316. if (parent && !(parent instanceof Gio.Cancellable))
  317. throw TypeError('Not a valid cancellable');
  318. super._init({parent});
  319. if (parent) {
  320. if (parent.is_cancelled()) {
  321. this.cancel();
  322. return;
  323. }
  324. this._connectToParent();
  325. }
  326. }
  327. _connectToParent() {
  328. this._connectId = this.parent.connect(() => {
  329. this._realCancel();
  330. if (this._disconnectIdle)
  331. return;
  332. this._disconnectIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
  333. delete this._disconnectIdle;
  334. this._disconnectFromParent();
  335. return GLib.SOURCE_REMOVE;
  336. });
  337. });
  338. }
  339. _disconnectFromParent() {
  340. if (this._connectId && !this._disconnectIdle) {
  341. this.parent.disconnect(this._connectId);
  342. delete this._connectId;
  343. }
  344. }
  345. _realCancel() {
  346. Gio.Cancellable.prototype.cancel.call(this);
  347. }
  348. cancel() {
  349. this._disconnectFromParent();
  350. this._realCancel();
  351. }
  352. });