ebe1a4d303
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>
212 lines
5.5 KiB
JavaScript
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();
|
|
}
|
|
}
|