// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- /* * 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 . */ const Mainloop = imports.mainloop; const { AccountsService, Clutter, Gio, GLib, GObject, Pango, Polkit, Shell, St } = imports.gi; const CheckBox = imports.ui.checkBox; 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 = 48; const _DIALOG_ICON_SIZE = 48; 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") }], iconStyleClass: 'end-session-dialog-logout-icon', 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: 'ConfirmedReboot', label: C_("button", "Restart") }, { signal: 'ConfirmedShutdown', label: C_("button", "Power Off") }], iconName: 'system-shutdown-symbolic', iconStyleClass: 'end-session-dialog-shutdown-icon', showOtherSessions: true, }; const restartDialogContent = { subject: C_("title", "Restart"), description(seconds) { return ngettext("The system will restart automatically in %d second.", "The system will restart automatically in %d seconds.", seconds).format(seconds); }, showBatteryWarning: false, confirmButtons: [{ signal: 'ConfirmedReboot', label: C_("button", "Restart") }], iconName: 'view-refresh-symbolic', iconStyleClass: 'end-session-dialog-shutdown-icon', 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 & Install") }], unusedFutureButtonForTranslation: C_("button", "Install & Power Off"), unusedFutureCheckBoxForTranslation: C_("checkbox", "Power off after updates are installed"), iconName: 'view-refresh-symbolic', iconStyleClass: 'end-session-dialog-shutdown-icon', 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 & Install") }], iconName: 'view-refresh-symbolic', iconStyleClass: 'end-session-dialog-shutdown-icon', 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'); 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: %s'.format(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 _setLabelText(label, text) { if (text) { label.set_text(text); label.show(); } else { label.set_text(''); label.hide(); } } function _setCheckBoxLabel(checkBox, text) { let label = checkBox.getLabelActor(); if (text) { label.set_text(text); checkBox.actor.show(); } else { label.set_text(''); checkBox.actor.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._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', (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.connect('destroy', this._onDestroy.bind(this)); this.connect('opened', this._onOpened.bind(this)); this._userLoadedId = this._user.connect('notify::is_loaded', this._sync.bind(this)); this._userChangedId = this._user.connect('changed', this._sync.bind(this)); let mainContentLayout = new St.BoxLayout({ vertical: false }); this.contentLayout.add(mainContentLayout, { x_fill: true, y_fill: false }); this._iconBin = new St.Bin(); mainContentLayout.add(this._iconBin, { x_fill: true, y_fill: false, x_align: St.Align.END, y_align: St.Align.START }); let messageLayout = new St.BoxLayout({ vertical: true, style_class: 'end-session-dialog-layout' }); mainContentLayout.add(messageLayout, { y_align: St.Align.START }); this._subjectLabel = new St.Label({ style_class: 'end-session-dialog-subject' }); messageLayout.add(this._subjectLabel, { x_fill: false, y_fill: false, x_align: St.Align.START, y_align: St.Align.START }); this._descriptionLabel = new St.Label({ style_class: 'end-session-dialog-description' }); this._descriptionLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; this._descriptionLabel.clutter_text.line_wrap = true; messageLayout.add(this._descriptionLabel, { y_fill: true, y_align: St.Align.START }); this._checkBox = new CheckBox.CheckBox(); this._checkBox.actor.connect('clicked', this._sync.bind(this)); messageLayout.add(this._checkBox.actor); this._batteryWarning = new St.Label({ style_class: 'end-session-dialog-warning', text: _("Running on battery power: please plug in before installing updates.") }); this._batteryWarning.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; this._batteryWarning.clutter_text.line_wrap = true; messageLayout.add(this._batteryWarning); this._scrollView = new St.ScrollView({ style_class: 'end-session-dialog-list' }); this._scrollView.set_policy(St.PolicyType.NEVER, St.PolicyType.AUTOMATIC); this.contentLayout.add(this._scrollView, { x_fill: true, y_fill: true }); this._scrollView.hide(); this._inhibitorSection = new St.BoxLayout({ vertical: true, style_class: 'end-session-dialog-inhibitor-layout' }); this._scrollView.add_actor(this._inhibitorSection); this._applicationHeader = new St.Label({ style_class: 'end-session-dialog-list-header', text: _("Some applications are busy or have unsaved work.") }); this._applicationList = new St.BoxLayout({ style_class: 'end-session-dialog-app-list', vertical: true }); this._inhibitorSection.add_actor(this._applicationHeader); this._inhibitorSection.add_actor(this._applicationList); this._sessionHeader = new St.Label({ style_class: 'end-session-dialog-list-header', text: _("Other users are logged in.") }); this._sessionList = new St.BoxLayout({ style_class: 'end-session-dialog-session-list', vertical: true }); this._inhibitorSection.add_actor(this._sessionHeader); this._inhibitorSection.add_actor(this._sessionList); this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(EndSessionDialogIface, this); this._dbusImpl.export(Gio.DBus.session, '/org/gnome/SessionManager/EndSessionDialog'); } _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 = Polkit.Permission.new_sync( 'org.freedesktop.packagekit.trigger-offline-update', null, null); } catch (e) { log('No permission to trigger offline updates: %s'.format(e.toString())); } } _onDestroy() { this._user.disconnect(this._userLoadedId); this._user.disconnect(this._userChangedId); } _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.actor.checked) subject = dialogContent.subjectWithUpdates; if (dialogContent.showBatteryWarning) { // Warn when running on battery power if (this._powerProxy.OnBattery && this._checkBox.actor.checked) this._batteryWarning.opacity = 255; else this._batteryWarning.opacity = 0; } 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 (this._pkOfflineProxy && dialogContent.upgradeDescription) { let name = this._pkOfflineProxy.PreparedUpgrade['name'].deep_unpack(); let version = this._pkOfflineProxy.PreparedUpgrade['version'].deep_unpack(); if (name != null && version != null) description = dialogContent.upgradeDescription(name, version); } // Fall back to regular description if (!description) description = dialogContent.description(displayTime); _setLabelText(this._descriptionLabel, description); _setLabelText(this._subjectLabel, subject); if (dialogContent.iconName) { this._iconBin.child = new St.Icon({ icon_name: dialogContent.iconName, icon_size: _DIALOG_ICON_SIZE, style_class: dialogContent.iconStyleClass }); } else { let avatarWidget = new UserWidget.Avatar(this._user, { iconSize: _DIALOG_ICON_SIZE, styleClass: dialogContent.iconStyleClass }); this._iconBin.child = avatarWidget.actor; avatarWidget.update(); } let hasApplications = this._applications.length > 0; let hasSessions = this._sessions.length > 0; this._scrollView.visible = hasApplications || hasSessions; this._applicationHeader.visible = hasApplications; this._sessionHeader.visible = hasSessions; } _updateButtons() { let dialogContent = DialogContent[this._type]; let buttons = [{ action: this.cancel.bind(this), label: _("Cancel"), key: Clutter.Escape }]; for (let i = 0; i < dialogContent.confirmButtons.length; i++) { let signal = dialogContent.confirmButtons[i].signal; let label = dialogContent.confirmButtons[i].label; buttons.push({ action: () => { this.close(true); let signalId = this.connect('closed', () => { this.disconnect(signalId); this._confirm(signal); }); }, label: label }); } this.setButtons(buttons); } close(skipSignal) { super.close(); if (!skipSignal) this._dbusImpl.emit_signal('Closed', null); } cancel() { this._stopTimer(); this._dbusImpl.emit_signal('Canceled', null); this.close(); } _confirm(signal) { let callback = () => { this._fadeOutDialog(); this._stopTimer(); this._dbusImpl.emit_signal(signal, null); }; // Offline update not available; just emit the signal if (!this._checkBox.actor.visible) { callback(); return; } // Trigger the offline update as requested if (this._checkBox.actor.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 = Mainloop.timeout_add_seconds(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) { Mainloop.source_remove(this._timerId); this._timerId = 0; } this._secondsLeft = 0; } _constructListItemForApp(inhibitor, app) { let actor = new St.BoxLayout({ style_class: 'end-session-dialog-app-list-item', can_focus: true }); actor.add(app.create_icon_texture(_ITEM_ICON_SIZE)); let textLayout = new St.BoxLayout({ vertical: true, y_expand: true, y_align: Clutter.ActorAlign.CENTER }); actor.add(textLayout); let nameLabel = new St.Label({ text: app.get_name(), style_class: 'end-session-dialog-app-list-item-name' }); textLayout.add(nameLabel); actor.label_actor = nameLabel; let [reason] = inhibitor.GetReasonSync(); if (reason) { let reasonLabel = new St.Label({ text: reason, style_class: 'end-session-dialog-app-list-item-description' }); textLayout.add(reasonLabel); } return actor; } _onInhibitorLoaded(inhibitor) { if (!this._applications.includes(inhibitor)) { // Stale inhibitor return; } let app = findAppFromInhibitor(inhibitor); if (app) { let actor = this._constructListItemForApp(inhibitor, app); this._applicationList.add(actor); } else { // inhibiting app is a service, not an application this._applications.splice(this._applications.indexOf(inhibitor), 1); } this._sync(); } _constructListItemForSession(session) { let avatar = new UserWidget.Avatar(session.user, { iconSize: _ITEM_ICON_SIZE }); avatar.update(); let userName = session.user.get_real_name() ? 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 actor = new St.BoxLayout({ style_class: 'end-session-dialog-session-list-item', can_focus: true }); actor.add(avatar.actor); let nameLabel = new St.Label({ text: userLabelText, style_class: 'end-session-dialog-session-list-item-name', y_expand: true, y_align: Clutter.ActorAlign.CENTER }); actor.add(nameLabel); actor.label_actor = nameLabel; return actor; } _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; let session = { user: this._userManager.get_user(userName), username: userName, type: proxy.Type, remote: proxy.Remote }; this._sessions.push(session); let actor = this._constructListItemForSession(session); this._sessionList.add(actor); // limit the number of entries n++; if (n == MAX_USERS_IN_SESSION_DIALOG) break; } this._sync(); }); } OpenAsync(parameters, invocation) { let [type, timestamp, totalSecondsToStayOpen, inhibitorObjectPaths] = parameters; this._totalSecondsToStayOpen = totalSecondsToStayOpen; this._type = type; // Only consider updates and upgrades if PackageKit is available. if (this._pkOfflineProxy && this._type == DialogType.RESTART) { if (this._pkOfflineProxy.UpdateTriggered) this._type = DialogType.UPDATE_RESTART; else if (this._pkOfflineProxy.UpgradeTriggered) this._type = DialogType.UPGRADE_RESTART; } this._applications = []; this._applicationList.destroy_all_children(); this._sessions = []; this._sessionList.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(); // Only consider updates and upgrades if PackageKit is available. let updateTriggered = this._pkOfflineProxy ? this._pkOfflineProxy.UpdateTriggered : false; let updatePrepared = this._pkOfflineProxy ? this._pkOfflineProxy.UpdatePrepared : false; let updatesAllowed = this._updatesPermission && this._updatesPermission.allowed; _setCheckBoxLabel(this._checkBox, dialogContent.checkBoxText || ''); this._checkBox.actor.visible = (dialogContent.checkBoxText && updatePrepared && updatesAllowed); this._checkBox.actor.checked = (updatePrepared && updateTriggered); // We show the warning either together with the checkbox, or when // updates have already been triggered, but the user doesn't have // enough permissions to cancel them. this._batteryWarning.visible = (dialogContent.showBatteryWarning && (this._checkBox.actor.visible || updatePrepared && updateTriggered && !updatesAllowed)); 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(); } });