File gnome-shell-grd-ask-logout-for-existing-session.patch of Package gnome-shell

From 3517749537ba322b174bc6167b4dea861f1f47d9 Mon Sep 17 00:00:00 2001
From: Joan Torres <joan.torres@suse.com>
Date: Wed, 7 Feb 2024 12:59:09 +0100
Subject: [PATCH 1/4] loginManager: Add session-removed signal and getSession
 method

These changes will be used by the next commit when displaying a
conflicting session dialog.

session-removed signal will be used to close the conflicting session dialog
if it's not needed anymore.

getSession method will be used when a session is opened, to check if
there's already a conflicting opened session.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3134>
---
 js/misc/loginManager.js | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

Index: gnome-shell-45.3/js/misc/loginManager.js
===================================================================
--- gnome-shell-45.3.orig/js/misc/loginManager.js
+++ gnome-shell-45.3/js/misc/loginManager.js
@@ -101,6 +101,8 @@ class LoginManagerSystemd extends Signal
             '/org/freedesktop/login1/user/self');
         this._proxy.connectSignal('PrepareForSleep',
             this._prepareForSleep.bind(this));
+        this._proxy.connectSignal('SessionRemoved',
+            this._sessionRemoved.bind(this));
     }
 
     async getCurrentSessionProxy() {
@@ -189,6 +191,10 @@ class LoginManagerSystemd extends Signal
         }
     }
 
+    getSession(objectPath) {
+        return new SystemdLoginSession(Gio.DBus.system, 'org.freedesktop.login1', objectPath);
+    }
+
     suspend() {
         this._proxy.SuspendAsync(true);
     }
@@ -206,6 +212,10 @@ class LoginManagerSystemd extends Signal
     _prepareForSleep(proxy, sender, [aboutToSuspend]) {
         this.emit('prepare-for-sleep', aboutToSuspend);
     }
+
+    _sessionRemoved(proxy, sender, [sessionId]) {
+        this.emit('session-removed', sessionId);
+    }
 }
 
 class LoginManagerDummy extends Signals.EventEmitter  {
@@ -237,6 +247,10 @@ class LoginManagerDummy extends Signals.
         return new Promise(resolve => resolve([]));
     }
 
+    getSession(_objectPath) {
+        return null;
+    }
+
     suspend() {
         this.emit('prepare-for-sleep', true);
         this.emit('prepare-for-sleep', false);
Index: gnome-shell-45.3/data/theme/gnome-shell-sass/widgets/_login-lock.scss
===================================================================
--- gnome-shell-45.3.orig/data/theme/gnome-shell-sass/widgets/_login-lock.scss
+++ gnome-shell-45.3/data/theme/gnome-shell-sass/widgets/_login-lock.scss
@@ -37,6 +37,25 @@ $_gdm_dialog_width: 23em;
   spacing: $base_padding*2;
 }
 
+.conflicting-session-dialog-content {
+  spacing: 20px;
+
+  .conflicting-session-dialog-title {
+    text-align: center;
+    @extend %title_2;
+    margin-bottom: 5px;
+  }
+
+  .conflicting-session-dialog-desc {
+    text-align: center;
+  }
+
+  .conflicting-session-dialog-desc-warning {
+    text-align: center;
+    color: $warning_color;
+  }
+}
+
 .login-dialog-logo-bin {
   margin:3em 0;
 }
Index: gnome-shell-45.3/js/gdm/loginDialog.js
===================================================================
--- gnome-shell-45.3.orig/js/gdm/loginDialog.js
+++ gnome-shell-45.3/js/gdm/loginDialog.js
@@ -36,6 +36,8 @@ import * as GdmUtil from './util.js';
 import * as Layout from '../ui/layout.js';
 import * as LoginManager from '../misc/loginManager.js';
 import * as Main from '../ui/main.js';
+import * as MessageTray from '../ui/messageTray.js';
+import * as ModalDialog from '../ui/modalDialog.js';
 import * as PopupMenu from '../ui/popupMenu.js';
 import * as Realmd from './realmd.js';
 import * as UserWidget from '../ui/userWidget.js';
@@ -43,6 +45,7 @@ import * as UserWidget from '../ui/userW
 const _FADE_ANIMATION_TIME = 250;
 const _SCROLL_ANIMATION_TIME = 500;
 const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0;
+const _CONFLICTING_SESSION_DIALOG_TIMEOUT = 60;
 
 export const UserListItem = GObject.registerClass({
     Signals: {'activate': {}},
@@ -408,6 +411,76 @@ const SessionMenuButton = GObject.regist
     }
 });
 
+export const ConflictingSessionDialog = GObject.registerClass({
+    Signals: {
+        'cancel': {},
+        'force-stop': {},
+    },
+}, class ConflictingSessionDialog extends ModalDialog.ModalDialog {
+    _init(conflictingSession, greeterSession, userName) {
+        super._init();
+
+        let bannerText;
+        if (greeterSession.Remote && conflictingSession.Remote)
+            /* Translators: is running for <username> */
+            bannerText = _('Remote login is not possible because a remote session is already running for %s. To login remotely, you must log out from the remote session or force stop it.').format(userName);
+        else if (!greeterSession.Remote && conflictingSession.Remote)
+            /* Translators: is running for <username> */
+            bannerText = _('Login is not possible because a remote session is already running for %s. To login, you must log out from the remote session or force stop it.').format(userName);
+        else if (greeterSession.Remote && !conflictingSession.Remote)
+            /* Translators: is running for <username> */
+            bannerText = _('Remote login is not possible because a local session is already running for %s. To login remotely, you must log out from the local session or force stop it.').format(userName);
+        else
+            /* Translators: is running for <username> */
+            bannerText = _('Login is not possible because a session is already running for %s. To login, you must log out from the session or force stop it.').format(userName);
+
+        let textLayout = new St.BoxLayout({
+            style_class: 'conflicting-session-dialog-content',
+            vertical: true,
+            x_expand: true,
+        });
+
+        let title = new St.Label({
+            text: _('Session Already Running'),
+            style_class: 'conflicting-session-dialog-title',
+        });
+
+        let banner = new St.Label({
+            text: bannerText,
+            style_class: 'conflicting-session-dialog-desc',
+        });
+        banner.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        banner.clutter_text.line_wrap = true;
+
+        let warningBanner = new St.Label({
+            text: _('Force stopping will quit any running apps and processes, and could result in data loss.'),
+            style_class: 'conflicting-session-dialog-desc-warning',
+        });
+        warningBanner.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        warningBanner.clutter_text.line_wrap = true;
+
+        textLayout.add_child(title);
+        textLayout.add_child(banner);
+        textLayout.add_child(warningBanner);
+        this.contentLayout.add_child(textLayout);
+
+        this.addButton({
+            label: _('Cancel'),
+            action: () => {
+                this.emit('cancel');
+            },
+            key: Clutter.KEY_Escape,
+            default: true,
+        });
+        this.addButton({
+            label: _('Force Stop'),
+            action: () => {
+                this.emit('force-stop');
+            },
+        });
+    }
+});
+
 export const LoginDialog = GObject.registerClass({
     Signals: {
         'failed': {},
@@ -1004,6 +1077,57 @@ export const LoginDialog = GObject.regis
         }, this);
     }
 
+    _notifyConflictingSessionDialogClosed(userName) {
+        const source = new MessageTray.SystemNotificationSource();
+        Main.messageTray.add(source);
+
+        this._conflictingSessionNotification = new MessageTray.Notification(source,
+            _('Stop conflicting session dialog closed'),
+            _('Try to login again to start a session for user %s.').format(userName));
+        this._conflictingSessionNotification.setUrgency(MessageTray.Urgency.CRITICAL);
+        this._conflictingSessionNotification.setTransient(true);
+        this._conflictingSessionNotification.connect('destroy', () => {
+            this._conflictingSessionNotification = null;
+        });
+
+        source.showNotification(this._conflictingSessionNotification);
+    }
+
+    _showConflictingSessionDialog(serviceName, conflictingSession) {
+        let conflictingSessionDialog = new ConflictingSessionDialog(conflictingSession,
+            this._greeterSessionProxy,
+            this._user.get_user_name());
+
+        conflictingSessionDialog.connect('cancel', () => {
+            this._authPrompt.reset();
+            conflictingSessionDialog.close();
+        });
+        conflictingSessionDialog.connect('force-stop', () => {
+            this._greeter.call_stop_conflicting_session_sync(null);
+        });
+
+        const loginManager = LoginManager.getLoginManager();
+        loginManager.connectObject('session-removed', (lm, sessionId) => {
+            if (sessionId === conflictingSession.Id) {
+                conflictingSessionDialog.close();
+                this._authPrompt.finish(() => this._startSession(serviceName));
+            }
+        }, conflictingSessionDialog);
+
+        const closeDialogTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, _CONFLICTING_SESSION_DIALOG_TIMEOUT, () => {
+            this._notifyConflictingSessionDialogClosed(this._user.get_user_name());
+            conflictingSessionDialog.close();
+            this._authPrompt.reset();
+            return GLib.SOURCE_REMOVE;
+        });
+
+        conflictingSessionDialog.connect('closed', () => {
+            GLib.source_remove(closeDialogTimeoutId);
+        });
+
+        conflictingSessionDialog.open();
+    }
+
     _startSession(serviceName) {
         this._bindOpacity();
         this.ease({
@@ -1017,7 +1141,40 @@ export const LoginDialog = GObject.regis
         });
     }
 
-    _onSessionOpened(client, serviceName) {
+    async _findConflictingSession(ignoreSessionId) {
+        const userName = this._user.get_user_name();
+        const loginManager = LoginManager.getLoginManager();
+        const sessions = await loginManager.listSessions();
+        for (const session of sessions.map(([id, , user, , path]) => ({id, user, path}))) {
+            if (ignoreSessionId === session.id)
+                continue;
+
+            if (userName !== session.user)
+                continue;
+
+            const sessionProxy = loginManager.getSession(session.path);
+
+            if (sessionProxy.Type !== 'wayland' && sessionProxy.Type !== 'x11')
+                continue;
+
+            if (sessionProxy.State !== 'active' && sessionProxy.State !== 'online')
+                continue;
+
+            return sessionProxy;
+        }
+
+        return null;
+    }
+
+    async _onSessionOpened(client, serviceName, sessionId) {
+        if (sessionId) {
+            const conflictingSession = await this._findConflictingSession(sessionId);
+            if (conflictingSession) {
+                this._showConflictingSessionDialog(serviceName, conflictingSession);
+                return;
+            }
+        }
+
         this._authPrompt.finish(() => this._startSession(serviceName));
     }
 
@@ -1209,6 +1366,9 @@ export const LoginDialog = GObject.regis
 
         this._updateCancelButton();
 
+        if (this._conflictingSessionNotification)
+            this._conflictingSessionNotification.destroy();
+
         const batch = new Batch.ConcurrentBatch(this, [
             GdmUtil.cloneAndFadeOutActor(this._userSelectionBox),
             this._beginVerificationForItem(activatedItem),
openSUSE Build Service is sponsored by