gnome-shell/js/dbusServices/dbusService.js
Florian Müllner ebe1a4d303 dbusService: Exempt org.gnome.Shell from sender tracking
If an interface has any signals, we don't want to auto-shutdown
while a caller is still connected to a signal.

Unfortunately we can't tell whether there are any signal connections,
so we track all callers instead, and keep the service alive while
any of them is still on the bus.

For services that we call from gnome-shell itself - like screencasts
or extensions - this has the unintended side effect of effectively
disabling auto-shutdown.

Address this by exempting the org.gnome.Shell name from sender
tracking.

Services that we expect to keep running for the lifetime of the
shell already disable auto-shutdown, so the only downside is a
small startup delay to resolve the well-known shell name.

https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/7250

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3052>
2023-12-13 01:27:51 +01:00

212 lines
5.5 KiB
JavaScript

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import {programArgs} from 'system';
const Signals = imports.signals;
const IDLE_SHUTDOWN_TIME = 2; // s
export class ServiceImplementation {
constructor(info, objectPath) {
this._objectPath = objectPath;
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(info, this);
this._injectTracking('return_dbus_error');
this._injectTracking('return_error_literal');
this._injectTracking('return_gerror');
this._injectTracking('return_value');
this._injectTracking('return_value_with_unix_fd_list');
this._senders = new Map();
this._holdCount = 0;
this._shellName = this._getUniqueShellName();
this._hasSignals = this._dbusImpl.get_info().signals.length > 0;
this._shutdownTimeoutId = 0;
// subclasses may override this to disable automatic shutdown
this._autoShutdown = true;
this._queueShutdownCheck();
}
// subclasses may override this to own additional names
register() {
}
export() {
this._dbusImpl.export(Gio.DBus.session, this._objectPath);
}
unexport() {
this._dbusImpl.unexport();
}
hold() {
this._holdCount++;
}
release() {
if (this._holdCount === 0) {
logError(new Error('Unmatched call to release()'));
return;
}
this._holdCount--;
if (this._holdCount === 0)
this._queueShutdownCheck();
}
/**
* Complete @invocation with an appropriate error if @error is set;
* useful for implementing early returns from method implementations.
*
* @param {Gio.DBusMethodInvocation}
* @param {Error}
*
* @returns {bool} - true if @invocation was completed
*/
_handleError(invocation, error) {
if (error === null)
return false;
if (error instanceof GLib.Error) {
invocation.return_gerror(error);
} else {
let name = error.name;
if (!name.includes('.')) // likely a normal JS error
name = `org.gnome.gjs.JSError.${name}`;
invocation.return_dbus_error(name, error.message);
}
return true;
}
_maybeShutdown() {
if (!this._autoShutdown)
return;
if (GLib.getenv('SHELL_DBUS_PERSIST'))
return;
if (this._holdCount > 0)
return;
this.emit('shutdown');
}
_queueShutdownCheck() {
if (this._shutdownTimeoutId)
GLib.source_remove(this._shutdownTimeoutId);
this._shutdownTimeoutId = GLib.timeout_add_seconds(
GLib.PRIORITY_DEFAULT, IDLE_SHUTDOWN_TIME,
() => {
this._shutdownTimeoutId = 0;
this._maybeShutdown();
return GLib.SOURCE_REMOVE;
});
}
_trackSender(sender) {
if (this._senders.has(sender))
return;
if (sender === this._shellName)
return; // don't track the shell
this.hold();
this._senders.set(sender,
this._dbusImpl.get_connection().watch_name(
sender,
Gio.BusNameWatcherFlags.NONE,
null,
() => this._untrackSender(sender)));
}
_untrackSender(sender) {
const id = this._senders.get(sender);
if (id)
this._dbusImpl.get_connection().unwatch_name(id);
if (this._senders.delete(sender))
this.release();
}
_injectTracking(methodName) {
const {prototype} = Gio.DBusMethodInvocation;
const origMethod = prototype[methodName];
const that = this;
prototype[methodName] = function (...args) {
origMethod.apply(this, args);
if (that._hasSignals)
that._trackSender(this.get_sender());
that._queueShutdownCheck();
};
}
_getUniqueShellName() {
try {
const res = Gio.DBus.session.call_sync(
'org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus',
'GetNameOwner',
new GLib.Variant('(s)', ['org.gnome.Shell']),
null,
Gio.DBusCallFlags.NONE,
-1,
null);
const [name] = res.deepUnpack();
return name;
} catch (e) {
console.warn(`Failed to resolve shell name: ${e.message}`);
return '';
}
}
}
Signals.addSignalMethods(ServiceImplementation.prototype);
export class DBusService {
constructor(name, service) {
this._name = name;
this._service = service;
this._loop = new GLib.MainLoop(null, false);
this._service.connect('shutdown', () => this._loop.quit());
}
async runAsync() {
// Bail out when not running under gnome-shell
Gio.DBus.watch_name(Gio.BusType.SESSION,
'org.gnome.Shell',
Gio.BusNameWatcherFlags.NONE,
null,
() => this._loop.quit());
this._service.register();
let flags = Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT;
if (programArgs.includes('--replace'))
flags |= Gio.BusNameOwnerFlags.REPLACE;
Gio.DBus.own_name(Gio.BusType.SESSION,
this._name,
flags,
() => this._service.export(),
null,
() => this._loop.quit());
await this._loop.runAsync();
}
}