Jonas Ådahl 44856ba
From a8a0d952293337544da4681f0c896052eafd9d0f Mon Sep 17 00:00:00 2001
Jonas Ådahl 44856ba
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
Jonas Ådahl 44856ba
Date: Fri, 5 Apr 2024 16:44:07 +0200
Jonas Ådahl 44856ba
Subject: [PATCH] Add headless session files
Jonas Ådahl 44856ba
Jonas Ådahl 44856ba
It consists of a python script for running the session, and a systemd
Jonas Ådahl 44856ba
system service template.
Jonas Ådahl 44856ba
---
Jonas Ådahl 44856ba
 data/gnome-headless-session@.service |   6 +
Jonas Ådahl 44856ba
 data/meson.build                     |   4 +
Jonas Ådahl 44856ba
 utils/gdm-headless-login-session     | 157 +++++++++++++++++++++++++++
Jonas Ådahl 44856ba
 utils/meson.build                    |   5 +
Jonas Ådahl 44856ba
 4 files changed, 172 insertions(+)
Jonas Ådahl 44856ba
 create mode 100644 data/gnome-headless-session@.service
Jonas Ådahl 44856ba
 create mode 100644 utils/gdm-headless-login-session
Jonas Ådahl 44856ba
Jonas Ådahl 44856ba
diff --git a/data/gnome-headless-session@.service b/data/gnome-headless-session@.service
Jonas Ådahl 44856ba
new file mode 100644
Jonas Ådahl 44856ba
index 000000000..269d16288
Jonas Ådahl 44856ba
--- /dev/null
Jonas Ådahl 44856ba
+++ b/data/gnome-headless-session@.service
Jonas Ådahl 44856ba
@@ -0,0 +1,6 @@
Jonas Ådahl 44856ba
+[Unit]
Jonas Ådahl 44856ba
+Description=Headless desktop session
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+[Service]
Jonas Ådahl 44856ba
+ExecStart=/usr/libexec/gdm-headless-login-session --user=%i
Jonas Ådahl 44856ba
+Restart=on-failure
Jonas Ådahl 44856ba
diff --git a/data/meson.build b/data/meson.build
Jonas Ådahl 44856ba
index 2211e98b5..2df07cd32 100644
Jonas Ådahl 44856ba
--- a/data/meson.build
Jonas Ådahl 44856ba
+++ b/data/meson.build
Jonas Ådahl 44856ba
@@ -221,3 +221,7 @@ if get_option('gdm-xsession')
Jonas Ådahl 44856ba
     install_dir: gdmconfdir,
Jonas Ådahl 44856ba
   )
Jonas Ådahl 44856ba
 endif
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+headless_session_service = install_data('gnome-headless-session@.service',
Jonas Ådahl 44856ba
+    install_dir: systemd_systemunitdir,
Jonas Ådahl 44856ba
+  )
Jonas Ådahl 44856ba
diff --git a/utils/gdm-headless-login-session b/utils/gdm-headless-login-session
Jonas Ådahl 44856ba
new file mode 100644
Jonas Ådahl 44856ba
index 000000000..e108be523
Jonas Ådahl 44856ba
--- /dev/null
Jonas Ådahl 44856ba
+++ b/utils/gdm-headless-login-session
Jonas Ådahl 44856ba
@@ -0,0 +1,157 @@
Jonas Ådahl 44856ba
+#!/usr/bin/env python3
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+import argparse
Jonas Ådahl 44856ba
+import pam
Jonas Ådahl 44856ba
+import pwd
Jonas Ådahl 44856ba
+import os
Jonas Ådahl 44856ba
+import signal
Jonas Ådahl 44856ba
+import sys
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+import gi
Jonas Ådahl 44856ba
+gi.require_version('AccountsService', '1.0')
Jonas Ådahl 44856ba
+from gi.repository import AccountsService, GLib
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+def run_desktop_in_new_session(pam_environment, user, session_desktop, tty_input, tty_output):
Jonas Ådahl 44856ba
+    keyfile = GLib.KeyFile()
Jonas Ådahl 44856ba
+    keyfile.load_from_data_dirs(f'wayland-sessions/{session_desktop}.desktop',
Jonas Ådahl 44856ba
+                                GLib.KeyFileFlags.NONE)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    try:
Jonas Ådahl 44856ba
+        can_run_headless = keyfile.get_boolean(GLib.KEY_FILE_DESKTOP_GROUP,
Jonas Ådahl 44856ba
+                                               'X-GDM-CanRunHeadless')
Jonas Ådahl 44856ba
+    except GLib.GError:
Jonas Ådahl 44856ba
+            raise Exception(f"Session {session_desktop} can't run headlessly")
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    if not can_run_headless:
Jonas Ådahl 44856ba
+        raise Exception(f"Session {session_desktop} can't run headlessly")
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    executable = keyfile.get_string(GLib.KEY_FILE_DESKTOP_GROUP,
Jonas Ådahl 44856ba
+                                    GLib.KEY_FILE_DESKTOP_KEY_TRY_EXEC)
Jonas Ådahl 44856ba
+    if GLib.find_program_in_path(executable) is None:
Jonas Ådahl 44856ba
+        raise Exception(f"Invalid session {session_desktop}")
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    command = keyfile.get_string(GLib.KEY_FILE_DESKTOP_GROUP,
Jonas Ådahl 44856ba
+                                 GLib.KEY_FILE_DESKTOP_KEY_EXEC)
Jonas Ådahl 44856ba
+    [success, args] = GLib.shell_parse_argv(command)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    pam_handle = pam.pam()
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    for key, value in pam_environment.items():
Jonas Ådahl 44856ba
+        pam_handle.putenv(f'{key}={value}')
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    if not pam_handle.authenticate(user, '', service='gdm-autologin', call_end=False):
Jonas Ådahl 44856ba
+        raise Exception("Authentication failed")
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    for key, value in pam_environment.items():
Jonas Ådahl 44856ba
+        pam_handle.putenv(f'{key}={value}')
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    if pam_handle.open_session() != pam.PAM_SUCCESS:
Jonas Ådahl 44856ba
+        raise Exception("Failed to open PAM session")
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    session_environment = os.environ.copy()
Jonas Ådahl 44856ba
+    session_environment.update(pam_handle.getenvlist())
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    user_info = pwd.getpwnam(user)
Jonas Ådahl 44856ba
+    uid = user_info.pw_uid
Jonas Ådahl 44856ba
+    gid = user_info.pw_gid
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    old_tty_output = os.fdopen(os.dup(2), 'w')
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    pid = os.fork()
Jonas Ådahl 44856ba
+    if pid == 0:
Jonas Ådahl 44856ba
+        try:
Jonas Ådahl 44856ba
+            os.setsid()
Jonas Ådahl 44856ba
+        except OSError as e:
Jonas Ådahl 44856ba
+            print(f"Could not create new pid session: {e}", file=old_tty_output)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+        try:
Jonas Ådahl 44856ba
+            os.dup2(tty_input.fileno(), 0)
Jonas Ådahl 44856ba
+            os.dup2(tty_output.fileno(), 1)
Jonas Ådahl 44856ba
+            os.dup2(tty_output.fileno(), 2)
Jonas Ådahl 44856ba
+        except OSError as e:
Jonas Ådahl 44856ba
+            print(f"Could not set up standard i/o: {e}", file=old_tty_output)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+        try:
Jonas Ådahl 44856ba
+            os.initgroups(user, gid)
Jonas Ådahl 44856ba
+            os.setgid(gid)
Jonas Ådahl 44856ba
+            os.setuid(uid);
Jonas Ådahl 44856ba
+        except OSError as e:
Jonas Ådahl 44856ba
+            print(f"Could not become user {user} (uid={uid}): {e}", file=old_tty_output)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+        try:
Jonas Ådahl 44856ba
+            os.execvpe(args[0], args, session_environment)
Jonas Ådahl 44856ba
+        except OSError as e:
Jonas Ådahl 44856ba
+            print(f"Could not run program \"{' '.join(arguments)}\": {e}", file=old_tty_output)
Jonas Ådahl 44856ba
+        os._exit(1)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    def signal_handler(sig, frame):
Jonas Ådahl 44856ba
+        os.kill(pid, sig)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    signal.signal(signal.SIGTERM, signal_handler)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    try:
Jonas Ådahl 44856ba
+        (_, exit_code) = os.waitpid(pid, 0);
Jonas Ådahl 44856ba
+    except KeyboardInterrupt:
Jonas Ådahl 44856ba
+        os.kill(pid, signal.SIGTERM)
Jonas Ådahl 44856ba
+    except OSError as e:
Jonas Ådahl 44856ba
+        print(f"Could not wait for program to finish: {e}", file=old_tty_output)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    if os.WIFEXITED(exit_code):
Jonas Ådahl 44856ba
+        exit_code = os.WEXITSTATUS(exit_code)
Jonas Ådahl 44856ba
+    else:
Jonas Ådahl 44856ba
+        os.kill(os.getpid(), os.WTERMSIG(exit_code))
Jonas Ådahl 44856ba
+    old_tty_output.close()
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    if pam_handle.close_session() != pam.PAM_SUCCESS:
Jonas Ådahl 44856ba
+        raise Exception("Failed to close PAM session")
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    pam_handle.end()
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    return exit_code
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+def wait_for_user_data(user):
Jonas Ådahl 44856ba
+    main_context = GLib.MainContext.default()
Jonas Ådahl 44856ba
+    while not user.is_loaded():
Jonas Ådahl 44856ba
+        main_context.iteration(True)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+def main():
Jonas Ådahl 44856ba
+    parser = argparse.ArgumentParser(description='Run a desktop session in a PAM session as a specified user.')
Jonas Ådahl 44856ba
+    parser.add_argument('--user', help='Username for which to run the session')
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    args = parser.parse_args()
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    if args.user is None:
Jonas Ådahl 44856ba
+        parser.print_usage()
Jonas Ådahl 44856ba
+        sys.exit(1)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    try:
Jonas Ådahl 44856ba
+        tty_path = '/dev/null'
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+        tty_input = open(tty_path, 'r')
Jonas Ådahl 44856ba
+        tty_output = open(tty_path, 'w')
Jonas Ådahl 44856ba
+    except OSError as e:
Jonas Ådahl 44856ba
+        raise Exception(f"Error opening /dev/null as tty associated with VT {vt}: {e}")
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    user_manager = AccountsService.UserManager().get_default()
Jonas Ådahl 44856ba
+    user = user_manager.get_user(args.user)
Jonas Ådahl 44856ba
+    wait_for_user_data(user)
Jonas Ådahl 44856ba
+    session_desktop = user.get_session()
Jonas Ådahl 44856ba
+    if not session_desktop:
Jonas Ådahl 44856ba
+        session_desktop = 'gnome'
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    pam_environment = {}
Jonas Ådahl 44856ba
+    pam_environment['XDG_SESSION_TYPE'] = 'wayland'
Jonas Ådahl 44856ba
+    pam_environment['XDG_SESSION_CLASS'] = 'user'
Jonas Ådahl 44856ba
+    pam_environment['XDG_SESSION_DESKTOP'] = session_desktop
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+    try:
Jonas Ådahl 44856ba
+        result = run_desktop_in_new_session(pam_environment, args.user, session_desktop, tty_input, tty_output)
Jonas Ådahl 44856ba
+    except Exception as e:
Jonas Ådahl 44856ba
+        raise Exception(f"Error running desktop session \"{session_desktop}\": {e}")
Jonas Ådahl 44856ba
+    tty_input.close()
Jonas Ådahl 44856ba
+    tty_output.close()
Jonas Ådahl 44856ba
+    sys.exit(result)
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+if __name__ == '__main__':
Jonas Ådahl 44856ba
+    main()
Jonas Ådahl 44856ba
diff --git a/utils/meson.build b/utils/meson.build
Jonas Ådahl 44856ba
index e4141fb13..57dd6519f 100644
Jonas Ådahl 44856ba
--- a/utils/meson.build
Jonas Ådahl 44856ba
+++ b/utils/meson.build
Jonas Ådahl 44856ba
@@ -65,3 +65,8 @@ if distro != 'none'
Jonas Ådahl 44856ba
     install_dir: get_option('libexecdir'),
Jonas Ådahl 44856ba
   )
Jonas Ådahl 44856ba
 endif
Jonas Ådahl 44856ba
+
Jonas Ådahl 44856ba
+gdm_headless_login_session = install_data('gdm-headless-login-session',
Jonas Ådahl 44856ba
+    install_mode: 'rwxr-xr-x',
Jonas Ådahl 44856ba
+    install_dir: get_option('libexecdir'),
Jonas Ådahl 44856ba
+  )
Jonas Ådahl 44856ba
-- 
Jonas Ådahl 44856ba
2.44.0
Jonas Ådahl 44856ba