gnome-shell/js/ui/endSessionDialog.js
Florian Müllner 26235bbe54 js: Use (dis)connectObject()
Start using the new methods to simplify signal cleanup. For now,
focus on replacing existing cleanups; in most cases this means
signals connected in the constructor and disconnected on destroy,
but also other cases with a similarly defined lifetime (say: from
show to hide).

This doesn't change signal connections that only exist for a short
time (say: once), handlers that are connected on-demand (say: the
first time a particular method is called), or connections that
aren't tracked (read: disconnected) at all.

We will eventually replace the latter with connectObject() as
well - especially from actor subclasses - but the changeset is
already big enough as-is :-)

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1953>
2022-03-04 14:14:37 +00:00

814 lines
28 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported init, EndSessionDialog */
/*
* Copyright 2010-2016 Red Hat, Inc
*
* 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, 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, see <http://www.gnu.org/licenses/>.
*/
const {
AccountsService, Clutter, Gio, GLib, GObject,
Pango, Polkit, Shell, St, UPowerGlib: UPower,
} = imports.gi;
const CheckBox = imports.ui.checkBox;
const Dialog = imports.ui.dialog;
const GnomeSession = imports.misc.gnomeSession;
const LoginManager = imports.misc.loginManager;
const ModalDialog = imports.ui.modalDialog;
const UserWidget = imports.ui.userWidget;
const { loadInterfaceXML } = imports.misc.fileUtils;
const _ITEM_ICON_SIZE = 64;
const LOW_BATTERY_THRESHOLD = 30;
const EndSessionDialogIface = loadInterfaceXML('org.gnome.SessionManager.EndSessionDialog');
const logoutDialogContent = {
subjectWithUser: C_("title", "Log Out %s"),
subject: C_("title", "Log Out"),
descriptionWithUser(user, seconds) {
return ngettext(
'%s will be logged out automatically in %d second.',
'%s will be logged out automatically in %d seconds.',
seconds).format(user, seconds);
},
description(seconds) {
return ngettext(
'You will be logged out automatically in %d second.',
'You will be logged out automatically in %d seconds.',
seconds).format(seconds);
},
showBatteryWarning: false,
confirmButtons: [{
signal: 'ConfirmedLogout',
label: C_('button', 'Log Out'),
}],
showOtherSessions: false,
};
const shutdownDialogContent = {
subject: C_("title", "Power Off"),
subjectWithUpdates: C_("title", "Install Updates & Power Off"),
description(seconds) {
return ngettext(
'The system will power off automatically in %d second.',
'The system will power off automatically in %d seconds.',
seconds).format(seconds);
},
checkBoxText: C_("checkbox", "Install pending software updates"),
showBatteryWarning: true,
confirmButtons: [{
signal: 'ConfirmedShutdown',
label: C_('button', 'Power Off'),
}],
iconName: 'system-shutdown-symbolic',
showOtherSessions: true,
};
const restartDialogContent = {
subject: C_("title", "Restart"),
subjectWithUpdates: C_('title', 'Install Updates & Restart'),
description(seconds) {
return ngettext(
'The system will restart automatically in %d second.',
'The system will restart automatically in %d seconds.',
seconds).format(seconds);
},
checkBoxText: C_('checkbox', 'Install pending software updates'),
showBatteryWarning: true,
confirmButtons: [{
signal: 'ConfirmedReboot',
label: C_('button', 'Restart'),
}],
iconName: 'view-refresh-symbolic',
showOtherSessions: true,
};
const restartUpdateDialogContent = {
subject: C_("title", "Restart & Install Updates"),
description(seconds) {
return ngettext(
'The system will automatically restart and install updates in %d second.',
'The system will automatically restart and install updates in %d seconds.',
seconds).format(seconds);
},
showBatteryWarning: true,
confirmButtons: [{
signal: 'ConfirmedReboot',
label: C_('button', 'Restart &amp; Install'),
}],
unusedFutureButtonForTranslation: C_("button", "Install &amp; Power Off"),
unusedFutureCheckBoxForTranslation: C_("checkbox", "Power off after updates are installed"),
iconName: 'view-refresh-symbolic',
showOtherSessions: true,
};
const restartUpgradeDialogContent = {
subject: C_("title", "Restart & Install Upgrade"),
upgradeDescription(distroName, distroVersion) {
/* Translators: This is the text displayed for system upgrades in the
shut down dialog. First %s gets replaced with the distro name and
second %s with the distro version to upgrade to */
return _("%s %s will be installed after restart. Upgrade installation can take a long time: ensure that you have backed up and that the computer is plugged in.").format(distroName, distroVersion);
},
disableTimer: true,
showBatteryWarning: false,
confirmButtons: [{
signal: 'ConfirmedReboot',
label: C_('button', 'Restart &amp; Install'),
}],
iconName: 'view-refresh-symbolic',
showOtherSessions: true,
};
const DialogType = {
LOGOUT: 0 /* GSM_SHELL_END_SESSION_DIALOG_TYPE_LOGOUT */,
SHUTDOWN: 1 /* GSM_SHELL_END_SESSION_DIALOG_TYPE_SHUTDOWN */,
RESTART: 2 /* GSM_SHELL_END_SESSION_DIALOG_TYPE_RESTART */,
UPDATE_RESTART: 3,
UPGRADE_RESTART: 4,
};
const DialogContent = {
0 /* DialogType.LOGOUT */: logoutDialogContent,
1 /* DialogType.SHUTDOWN */: shutdownDialogContent,
2 /* DialogType.RESTART */: restartDialogContent,
3 /* DialogType.UPDATE_RESTART */: restartUpdateDialogContent,
4 /* DialogType.UPGRADE_RESTART */: restartUpgradeDialogContent,
};
var MAX_USERS_IN_SESSION_DIALOG = 5;
const LogindSessionIface = loadInterfaceXML('org.freedesktop.login1.Session');
const LogindSession = Gio.DBusProxy.makeProxyWrapper(LogindSessionIface);
const PkOfflineIface = loadInterfaceXML('org.freedesktop.PackageKit.Offline');
const PkOfflineProxy = Gio.DBusProxy.makeProxyWrapper(PkOfflineIface);
const UPowerIface = loadInterfaceXML('org.freedesktop.UPower.Device');
const UPowerProxy = Gio.DBusProxy.makeProxyWrapper(UPowerIface);
function findAppFromInhibitor(inhibitor) {
let desktopFile;
try {
[desktopFile] = inhibitor.GetAppIdSync();
} catch (e) {
// XXX -- sometimes JIT inhibitors generated by gnome-session
// get removed too soon. Don't fail in this case.
log(`gnome-session gave us a dead inhibitor: ${inhibitor.get_object_path()}`);
return null;
}
if (!GLib.str_has_suffix(desktopFile, '.desktop'))
desktopFile += '.desktop';
return Shell.AppSystem.get_default().lookup_heuristic_basename(desktopFile);
}
// The logout timer only shows updates every 10 seconds
// until the last 10 seconds, then it shows updates every
// second. This function takes a given time and returns
// what we should show to the user for that time.
function _roundSecondsToInterval(totalSeconds, secondsLeft, interval) {
let time;
time = Math.ceil(secondsLeft);
// Final count down is in decrements of 1
if (time <= interval)
return time;
// Round up higher than last displayable time interval
time += interval - 1;
// Then round down to that time interval
if (time > totalSeconds)
time = Math.ceil(totalSeconds);
else
time -= time % interval;
return time;
}
function _setCheckBoxLabel(checkBox, text) {
let label = checkBox.getLabelActor();
if (text) {
label.set_text(text);
checkBox.show();
} else {
label.set_text('');
checkBox.hide();
}
}
function init() {
// This always returns the same singleton object
// By instantiating it initially, we register the
// bus object, etc.
new EndSessionDialog();
}
var EndSessionDialog = GObject.registerClass(
class EndSessionDialog extends ModalDialog.ModalDialog {
_init() {
super._init({
styleClass: 'end-session-dialog',
destroyOnClose: false,
});
this._loginManager = LoginManager.getLoginManager();
this._loginManager.canRebootToBootLoaderMenu(
(canRebootToBootLoaderMenu, unusedNeedsAuth) => {
this._canRebootToBootLoaderMenu = canRebootToBootLoaderMenu;
});
this._userManager = AccountsService.UserManager.get_default();
this._user = this._userManager.get_user(GLib.get_user_name());
this._updatesPermission = null;
this._pkOfflineProxy = new PkOfflineProxy(Gio.DBus.system,
'org.freedesktop.PackageKit',
'/org/freedesktop/PackageKit',
this._onPkOfflineProxyCreated.bind(this));
this._powerProxy = new UPowerProxy(Gio.DBus.system,
'org.freedesktop.UPower',
'/org/freedesktop/UPower/devices/DisplayDevice',
(proxy, error) => {
if (error) {
log(error.message);
return;
}
this._powerProxy.connect('g-properties-changed',
this._sync.bind(this));
this._sync();
});
this._secondsLeft = 0;
this._totalSecondsToStayOpen = 0;
this._applications = [];
this._sessions = [];
this._capturedEventId = 0;
this._rebootButton = null;
this._rebootButtonAlt = null;
this.connect('opened',
this._onOpened.bind(this));
this._user.connectObject(
'notify::is-loaded', this._sync.bind(this),
'changed', this._sync.bind(this), this);
this._messageDialogContent = new Dialog.MessageDialogContent();
this._checkBox = new CheckBox.CheckBox();
this._checkBox.connect('clicked', this._sync.bind(this));
this._messageDialogContent.add_child(this._checkBox);
this._batteryWarning = new St.Label({
style_class: 'end-session-dialog-battery-warning',
text: _('Low battery power: please plug in before installing updates.'),
});
this._batteryWarning.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
this._batteryWarning.clutter_text.line_wrap = true;
this._messageDialogContent.add_child(this._batteryWarning);
this.contentLayout.add_child(this._messageDialogContent);
this._applicationSection = new Dialog.ListSection({
title: _('Some applications are busy or have unsaved work'),
});
this.contentLayout.add_child(this._applicationSection);
this._sessionSection = new Dialog.ListSection({
title: _('Other users are logged in'),
});
this.contentLayout.add_child(this._sessionSection);
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(EndSessionDialogIface, this);
this._dbusImpl.export(Gio.DBus.session, '/org/gnome/SessionManager/EndSessionDialog');
}
async _onPkOfflineProxyCreated(proxy, error) {
if (error) {
log(error.message);
return;
}
// Creating a D-Bus proxy won't propagate SERVICE_UNKNOWN or NAME_HAS_NO_OWNER
// errors if PackageKit is not available, but the GIO implementation will make
// sure in that case that the proxy's g-name-owner is set to null, so check that.
if (this._pkOfflineProxy.g_name_owner === null) {
this._pkOfflineProxy = null;
return;
}
// It only makes sense to check for this permission if PackageKit is available.
try {
this._updatesPermission = await Polkit.Permission.new(
'org.freedesktop.packagekit.trigger-offline-update', null, null);
} catch (e) {
log(`No permission to trigger offline updates: ${e}`);
}
}
_isDischargingBattery() {
return this._powerProxy.IsPresent &&
this._powerProxy.State !== UPower.DeviceState.CHARGING &&
this._powerProxy.State !== UPower.DeviceState.FULLY_CHARGED;
}
_isBatteryLow() {
return this._isDischargingBattery() && this._powerProxy.Percentage < LOW_BATTERY_THRESHOLD;
}
_shouldShowLowBatteryWarning(dialogContent) {
if (!dialogContent.showBatteryWarning)
return false;
if (!this._isBatteryLow())
return false;
if (this._checkBox.checked)
return true;
// Show the warning if updates have already been triggered, but
// the user doesn't have enough permissions to cancel them.
let updatesAllowed = this._updatesPermission && this._updatesPermission.allowed;
return this._updateInfo.UpdatePrepared && this._updateInfo.UpdateTriggered && !updatesAllowed;
}
_sync() {
let open = this.state == ModalDialog.State.OPENING || this.state == ModalDialog.State.OPENED;
if (!open)
return;
let dialogContent = DialogContent[this._type];
let subject = dialogContent.subject;
// Use different title when we are installing updates
if (dialogContent.subjectWithUpdates && this._checkBox.checked)
subject = dialogContent.subjectWithUpdates;
this._batteryWarning.visible = this._shouldShowLowBatteryWarning(dialogContent);
let description;
let displayTime = _roundSecondsToInterval(this._totalSecondsToStayOpen,
this._secondsLeft,
10);
if (this._user.is_loaded) {
let realName = this._user.get_real_name();
if (realName != null) {
if (dialogContent.subjectWithUser)
subject = dialogContent.subjectWithUser.format(realName);
if (dialogContent.descriptionWithUser)
description = dialogContent.descriptionWithUser(realName, displayTime);
}
}
// Use a different description when we are installing a system upgrade
// if the PackageKit proxy is available (i.e. PackageKit is available).
if (dialogContent.upgradeDescription) {
const { name, version } = this._updateInfo.PreparedUpgrade;
if (name != null && version != null)
description = dialogContent.upgradeDescription(name, version);
}
// Fall back to regular description
if (!description)
description = dialogContent.description(displayTime);
this._messageDialogContent.title = subject;
this._messageDialogContent.description = description;
let hasApplications = this._applications.length > 0;
let hasSessions = this._sessions.length > 0;
this._applicationSection.visible = hasApplications;
this._sessionSection.visible = hasSessions;
}
_onCapturedEvent(actor, event) {
let altEnabled = false;
let type = event.type();
if (type !== Clutter.EventType.KEY_PRESS && type !== Clutter.EventType.KEY_RELEASE)
return Clutter.EVENT_PROPAGATE;
let key = event.get_key_symbol();
if (key !== Clutter.KEY_Alt_L && key !== Clutter.KEY_Alt_R)
return Clutter.EVENT_PROPAGATE;
if (type === Clutter.EventType.KEY_PRESS)
altEnabled = true;
this._rebootButton.visible = !altEnabled;
this._rebootButtonAlt.visible = altEnabled;
return Clutter.EVENT_PROPAGATE;
}
_updateButtons() {
this.clearButtons();
this.addButton({
action: this.cancel.bind(this),
label: _('Cancel'),
key: Clutter.KEY_Escape,
});
let dialogContent = DialogContent[this._type];
for (let i = 0; i < dialogContent.confirmButtons.length; i++) {
let signal = dialogContent.confirmButtons[i].signal;
let label = dialogContent.confirmButtons[i].label;
let button = this.addButton({
action: () => {
this.close(true);
let signalId = this.connect('closed', () => {
this.disconnect(signalId);
this._confirm(signal);
});
},
label,
});
// Add Alt "Boot Options" option to the Reboot button
if (this._canRebootToBootLoaderMenu && signal === 'ConfirmedReboot') {
this._rebootButton = button;
this._rebootButtonAlt = this.addButton({
action: () => {
this.close(true);
let signalId = this.connect('closed', () => {
this.disconnect(signalId);
this._confirmRebootToBootLoaderMenu();
});
},
label: C_('button', 'Boot Options'),
});
this._rebootButtonAlt.visible = false;
this._capturedEventId = this.connect('captured-event',
this._onCapturedEvent.bind(this));
}
}
}
_stopAltCapture() {
if (this._capturedEventId > 0) {
global.stage.disconnect(this._capturedEventId);
this._capturedEventId = 0;
}
this._rebootButton = null;
this._rebootButtonAlt = null;
}
close(skipSignal) {
super.close();
if (!skipSignal)
this._dbusImpl.emit_signal('Closed', null);
}
cancel() {
this._stopTimer();
this._stopAltCapture();
this._dbusImpl.emit_signal('Canceled', null);
this.close();
}
_confirmRebootToBootLoaderMenu() {
this._loginManager.setRebootToBootLoaderMenu();
this._confirm('ConfirmedReboot');
}
_confirm(signal) {
let callback = () => {
this._fadeOutDialog();
this._stopTimer();
this._stopAltCapture();
this._dbusImpl.emit_signal(signal, null);
};
// Offline update not available; just emit the signal
if (!this._checkBox.visible) {
callback();
return;
}
// Trigger the offline update as requested
if (this._checkBox.checked) {
switch (signal) {
case "ConfirmedReboot":
this._triggerOfflineUpdateReboot(callback);
break;
case "ConfirmedShutdown":
// To actually trigger the offline update, we need to
// reboot to do the upgrade. When the upgrade is complete,
// the computer will shut down automatically.
signal = "ConfirmedReboot";
this._triggerOfflineUpdateShutdown(callback);
break;
default:
callback();
break;
}
} else {
this._triggerOfflineUpdateCancel(callback);
}
}
_onOpened() {
this._sync();
}
_triggerOfflineUpdateReboot(callback) {
// Handle this gracefully if PackageKit is not available.
if (!this._pkOfflineProxy) {
callback();
return;
}
this._pkOfflineProxy.TriggerRemote('reboot', (result, error) => {
if (error)
log(error.message);
callback();
});
}
_triggerOfflineUpdateShutdown(callback) {
// Handle this gracefully if PackageKit is not available.
if (!this._pkOfflineProxy) {
callback();
return;
}
this._pkOfflineProxy.TriggerRemote('power-off', (result, error) => {
if (error)
log(error.message);
callback();
});
}
_triggerOfflineUpdateCancel(callback) {
// Handle this gracefully if PackageKit is not available.
if (!this._pkOfflineProxy) {
callback();
return;
}
this._pkOfflineProxy.CancelRemote((result, error) => {
if (error)
log(error.message);
callback();
});
}
_startTimer() {
let startTime = GLib.get_monotonic_time();
this._secondsLeft = this._totalSecondsToStayOpen;
this._timerId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
let currentTime = GLib.get_monotonic_time();
let secondsElapsed = (currentTime - startTime) / 1000000;
this._secondsLeft = this._totalSecondsToStayOpen - secondsElapsed;
if (this._secondsLeft > 0) {
this._sync();
return GLib.SOURCE_CONTINUE;
}
let dialogContent = DialogContent[this._type];
let button = dialogContent.confirmButtons[dialogContent.confirmButtons.length - 1];
this._confirm(button.signal);
this._timerId = 0;
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(this._timerId, '[gnome-shell] this._confirm');
}
_stopTimer() {
if (this._timerId > 0) {
GLib.source_remove(this._timerId);
this._timerId = 0;
}
this._secondsLeft = 0;
}
_onInhibitorLoaded(inhibitor) {
if (!this._applications.includes(inhibitor)) {
// Stale inhibitor
return;
}
let app = findAppFromInhibitor(inhibitor);
const [flags] = app ? inhibitor.GetFlagsSync() : [0];
if (app && flags & GnomeSession.InhibitFlags.LOGOUT) {
let [description] = inhibitor.GetReasonSync();
let listItem = new Dialog.ListSectionItem({
icon_actor: app.create_icon_texture(_ITEM_ICON_SIZE),
title: app.get_name(),
description,
});
this._applicationSection.list.add_child(listItem);
} else {
// inhibiting app is a service (not an application) or is not
// inhibiting logout/shutdown
this._applications.splice(this._applications.indexOf(inhibitor), 1);
}
this._sync();
}
_loadSessions() {
this._loginManager.listSessions(result => {
let n = 0;
for (let i = 0; i < result.length; i++) {
let [id_, uid_, userName, seat_, sessionPath] = result[i];
let proxy = new LogindSession(Gio.DBus.system, 'org.freedesktop.login1', sessionPath);
if (proxy.Class != 'user')
continue;
if (proxy.State == 'closing')
continue;
let sessionId = GLib.getenv('XDG_SESSION_ID');
if (!sessionId) {
this._loginManager.getCurrentSessionProxy(currentSessionProxy => {
sessionId = currentSessionProxy.Id;
log(`endSessionDialog: No XDG_SESSION_ID, fetched from logind: ${sessionId}`);
});
}
if (proxy.Id == sessionId)
continue;
const session = {
user: this._userManager.get_user(userName),
username: userName,
type: proxy.Type,
remote: proxy.Remote,
};
this._sessions.push(session);
let userAvatar = new UserWidget.Avatar(session.user, { iconSize: _ITEM_ICON_SIZE });
userAvatar.update();
userName = session.user.get_real_name() ?? session.username;
let userLabelText;
if (session.remote)
/* Translators: Remote here refers to a remote session, like a ssh login */
userLabelText = _('%s (remote)').format(userName);
else if (session.type === 'tty')
/* Translators: Console here refers to a tty like a VT console */
userLabelText = _('%s (console)').format(userName);
else
userLabelText = userName;
let listItem = new Dialog.ListSectionItem({
icon_actor: userAvatar,
title: userLabelText,
});
this._sessionSection.list.add_child(listItem);
// limit the number of entries
n++;
if (n == MAX_USERS_IN_SESSION_DIALOG)
break;
}
this._sync();
});
}
async _getUpdateInfo() {
const connection = this._pkOfflineProxy.get_connection();
const reply = await connection.call(
this._pkOfflineProxy.g_name,
this._pkOfflineProxy.g_object_path,
'org.freedesktop.DBus.Properties',
'GetAll',
new GLib.Variant('(s)', [this._pkOfflineProxy.g_interface_name]),
null,
Gio.DBusCallFlags.NONE,
-1,
null);
const [info] = reply.recursiveUnpack();
return info;
}
async OpenAsync(parameters, invocation) {
let [type, timestamp, totalSecondsToStayOpen, inhibitorObjectPaths] = parameters;
this._totalSecondsToStayOpen = totalSecondsToStayOpen;
this._type = type;
try {
this._updateInfo = await this._getUpdateInfo();
} catch (e) {
if (this._pkOfflineProxy !== null)
log(`Failed to get update info from PackageKit: ${e.message}`);
this._updateInfo = {
UpdateTriggered: false,
UpdatePrepared: false,
UpgradeTriggered: false,
PreparedUpgrade: {},
};
}
// Only consider updates and upgrades if PackageKit is available.
if (this._pkOfflineProxy && this._type == DialogType.RESTART) {
if (this._updateInfo.UpdateTriggered)
this._type = DialogType.UPDATE_RESTART;
else if (this._updateInfo.UpgradeTriggered)
this._type = DialogType.UPGRADE_RESTART;
}
this._applications = [];
this._applicationSection.list.destroy_all_children();
this._sessions = [];
this._sessionSection.list.destroy_all_children();
if (!(this._type in DialogContent)) {
invocation.return_dbus_error('org.gnome.Shell.ModalDialog.TypeError',
"Unknown dialog type requested");
return;
}
let dialogContent = DialogContent[this._type];
for (let i = 0; i < inhibitorObjectPaths.length; i++) {
let inhibitor = new GnomeSession.Inhibitor(inhibitorObjectPaths[i], proxy => {
this._onInhibitorLoaded(proxy);
});
this._applications.push(inhibitor);
}
if (dialogContent.showOtherSessions)
this._loadSessions();
let updatesAllowed = this._updatesPermission && this._updatesPermission.allowed;
_setCheckBoxLabel(this._checkBox, dialogContent.checkBoxText || '');
this._checkBox.visible = dialogContent.checkBoxText && this._updateInfo.UpdatePrepared && updatesAllowed;
if (this._type === DialogType.UPGRADE_RESTART)
this._checkBox.checked = this._checkBox.visible && this._updateInfo.UpdateTriggered && !this._isDischargingBattery();
else
this._checkBox.checked = this._checkBox.visible && !this._isBatteryLow();
this._batteryWarning.visible = this._shouldShowLowBatteryWarning(dialogContent);
this._updateButtons();
if (!this.open(timestamp)) {
invocation.return_dbus_error('org.gnome.Shell.ModalDialog.GrabError',
"Cannot grab pointer and keyboard");
return;
}
if (!dialogContent.disableTimer)
this._startTimer();
this._sync();
let signalId = this.connect('opened', () => {
invocation.return_value(null);
this.disconnect(signalId);
});
}
Close(_parameters, _invocation) {
this.close();
}
});