]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
Merge pull request #30884 from poettering/logind-background-light
authorLennart Poettering <lennart@poettering.net>
Thu, 11 Jan 2024 20:20:01 +0000 (21:20 +0100)
committerGitHub <noreply@github.com>
Thu, 11 Jan 2024 20:20:01 +0000 (21:20 +0100)
logind: add "background-light" + "manager" session classes

man/pam_systemd.xml
man/sd_session_is_active.xml
src/login/logind-core.c
src/login/logind-dbus.c
src/login/logind-session-dbus.c
src/login/logind-session.c
src/login/logind-session.h
src/login/logind-user.c
src/login/logind-user.h
src/login/pam_systemd.c
test/units/testsuite-35.sh

index c9a3ccbb5f29fecd65a9e37cfb1a827a493c75cb..2c3bbec5d8147531ed39e5c1bde2abb930802ac8 100644 (file)
       <varlistentry>
         <term><varname>class=</varname></term>
 
-        <listitem><para>Takes a string argument which sets the session class. The <varname>XDG_SESSION_CLASS</varname>
-        environment variable (see below) takes precedence. One of <literal>user</literal>, <literal>greeter</literal>,
-        <literal>lock-screen</literal> or <literal>background</literal>. See
-        <citerefentry><refentrytitle>sd_session_get_class</refentrytitle><manvolnum>3</manvolnum></citerefentry> for
-        details about the session class.</para>
+        <listitem><para>Takes a string argument which sets the session class. The
+        <varname>XDG_SESSION_CLASS</varname> environment variable (see below) takes precedence. See
+        <citerefentry><refentrytitle>sd_session_get_class</refentrytitle><manvolnum>3</manvolnum></citerefentry>
+        for a way to query the class of a session. The following session classes are defined:</para>
+
+        <table>
+          <title>Session Classes</title>
+          <tgroup cols='2' align='left' colsep='1' rowsep='1'>
+            <colspec colname="name" />
+            <colspec colname="explanation" />
+            <thead>
+              <row>
+                <entry>Name</entry>
+                <entry>Explanation</entry>
+              </row>
+            </thead>
+            <tbody>
+              <row>
+                <entry><constant>user</constant></entry>
+                <entry>A regular interactive user session. This is the default class for sessions for which a TTY or X display is known at session registration time.</entry>
+              </row>
+              <row>
+                <entry><constant>user-early</constant></entry>
+                <entry>Similar to <literal>user</literal> but sessions of this class are not ordered after <filename>systemd-user-sessions.service</filename>, i.e. may be started before regular sessions are allowed to be established. This session class is the default for sessions of the root user that would otherwise qualify for the <constant>user</constant> class, see above. (Added in v256.)</entry>
+              </row>
+              <row>
+                <entry><constant>greeter</constant></entry>
+                <entry>Similar to <literal>user</literal> but for sessions that are spawned by a display manager ephemerally and which prompt the user for login credentials.</entry>
+              </row>
+              <row>
+                <entry><constant>lock-screen</constant></entry>
+                <entry>Similar to <literal>user</literal> but for sessions that are spawned by a display manager ephemerally and which show a lock screen that can be used to unlock locked user accounts or sessions.</entry>
+              </row>
+              <row>
+                <entry><constant>background</constant></entry>
+                <entry>Used for background sessions, such as those invoked by <command>cron</command> and similar tools. This is the default class for sessions for which no TTY or X display is known at session registration time.</entry>
+              </row>
+              <row>
+                <entry><constant>background-light</constant></entry>
+                <entry>Similar to <constant>background</constant>, but sessions of this class will not pull in the <filename>user@.service</filename> of the user, and thus possibly have no services of the user running. (Added in v256.)</entry>
+              </row>
+              <row>
+                <entry><constant>manager</constant></entry>
+                <entry>The <filename>user@.service</filename> service of the user is registered under this session class. (Added in v256.)</entry>
+              </row>
+              <row>
+                <entry><constant>manager-early</constant></entry>
+                <entry>Similar to <constant>manager</constant>, but for the root user. Compare with the <constant>user</constant> vs. <constant>user-early</constant> situation. (Added in v256.)</entry>
+              </row>
+            </tbody>
+          </tgroup>
+        </table>
 
         <xi:include href="version-info.xml" xpointer="v197"/></listitem>
       </varlistentry>
index e69ef59fd0eda0886be75beb913b26506c1b8393..747fab4c68ff2ceb7e99b8614a490e1d14409923 100644 (file)
     <citerefentry project='man-pages'><refentrytitle>free</refentrytitle><manvolnum>3</manvolnum></citerefentry>
     call after use.</para>
 
-    <para><function>sd_session_get_class()</function> may be used to
-    determine the class of the session identified by the specified
-    session identifier. The returned string is one of
-    <literal>user</literal>, <literal>greeter</literal>,
-    <literal>lock-screen</literal>, or <literal>background</literal>
-    and needs to be freed with the libc
-    <citerefentry project='man-pages'><refentrytitle>free</refentrytitle><manvolnum>3</manvolnum></citerefentry>
-    call after use.</para>
+    <para><function>sd_session_get_class()</function> may be used to determine the class of the session
+    identified by the specified session identifier. The returned string is one of <literal>user</literal>,
+    <literal>user-early</literal>, <literal>greeter</literal>, <literal>lock-screen</literal>,
+    <literal>background</literal>, <literal>background-light</literal>, <literal>manager</literal> or
+    <literal>manager-early</literal> and needs to be freed with the libc <citerefentry
+    project='man-pages'><refentrytitle>free</refentrytitle><manvolnum>3</manvolnum></citerefentry> call after
+    use.</para>
 
     <para><function>sd_session_get_desktop()</function> may be used to
     determine the brand of the desktop running on the session
index 26133ee6efceece88d6b716ff8b09ebb5aa814bf..582cbcbea84709873e5572fd60a22225cb6c4256 100644 (file)
@@ -413,6 +413,9 @@ int manager_get_idle_hint(Manager *m, dual_timestamp *t) {
                 dual_timestamp k;
                 int ih;
 
+                if (!SESSION_CLASS_CAN_IDLE(s->class))
+                        continue;
+
                 ih = session_get_idle_hint(s, &k);
                 if (ih < 0)
                         return ih;
index 4f8525b46a16788ada0ae925dc1ca781ca6e1f49..99b9da6ba10802795c7118e347ef0e7a627094a1 100644 (file)
@@ -865,25 +865,19 @@ static int create_session(
                         c = SESSION_USER;
         }
 
-        /* Check if we are already in a logind session. Or if we are in user@.service
-         * which is a special PAM session that avoids creating a logind session. */
-        r = manager_get_user_by_pid(m, leader.pid, NULL);
+        /* Check if we are already in a logind session, and if so refuse. */
+        r = manager_get_session_by_pidref(m, &leader, /* ret_session= */ NULL);
         if (r < 0)
                 return r;
         if (r > 0)
                 return sd_bus_error_setf(error, BUS_ERROR_SESSION_BUSY,
                                          "Already running in a session or user slice");
 
-        /*
-         * Old gdm and lightdm start the user-session on the same VT as
-         * the greeter session. But they destroy the greeter session
-         * after the user-session and want the user-session to take
-         * over the VT. We need to support this for
-         * backwards-compatibility, so make sure we allow new sessions
-         * on a VT that a greeter is running on. Furthermore, to allow
-         * re-logins, we have to allow a greeter to take over a used VT for
-         * the exact same reasons.
-         */
+        /* Old gdm and lightdm start the user-session on the same VT as the greeter session. But they destroy
+         * the greeter session after the user-session and want the user-session to take over the VT. We need
+         * to support this for backwards-compatibility, so make sure we allow new sessions on a VT that a
+         * greeter is running on. Furthermore, to allow re-logins, we have to allow a greeter to take over a
+         * used VT for the exact same reasons. */
         if (c != SESSION_GREETER &&
             vtnr > 0 &&
             vtnr < MALLOC_ELEMENTSOF(m->seat0->positions) &&
@@ -943,9 +937,17 @@ static int create_session(
                 goto fail;
 
         session->original_type = session->type = t;
-        session->class = c;
         session->remote = remote;
         session->vtnr = vtnr;
+        session->class = c;
+
+        /* Once the first session that is of a pinning class shows up we'll change the GC mode for the user
+         * from USER_GC_BY_ANY to USER_GC_BY_PIN, so that the user goes away once the last pinning session
+         * goes away. Background: we want that user@.service – when started manually – remains around (which
+         * itself is a non-pinning session), but gets stopped when the last pinning session goes away. */
+
+        if (SESSION_CLASS_PIN_USER(c))
+                user->gc_mode = USER_GC_BY_PIN;
 
         if (!isempty(tty)) {
                 session->tty = strdup(tty);
@@ -1017,8 +1019,14 @@ static int create_session(
 
         session->create_message = sd_bus_message_ref(message);
 
-        /* Now, let's wait until the slice unit and stuff got created. We send the reply back from
-         * session_send_create_reply(). */
+        /* Now call into session_send_create_reply(), which will reply to this method call for us. Or it
+         * won't – in case we just spawned a session scope and/or user service manager, and they aren't ready
+         * yet. We'll call session_create_reply() again once the session scope or the user service manager is
+         * ready, where the function will check again if a reply is then ready to be sent, and then do so if
+         * all is complete - or wait again. */
+        r = session_send_create_reply(session, /* error= */ NULL);
+        if (r < 0)
+                return r;
 
         return 1;
 
index 15f04c4a66aaeedd08511cc9bd732e9f5579d0a9..7217b8147642c188d959c77d826bff94cf3f22e5 100644 (file)
@@ -216,7 +216,9 @@ int bus_session_method_lock(sd_bus_message *message, void *userdata, sd_bus_erro
         if (r == 0)
                 return 1; /* Will call us back */
 
-        r = session_send_lock(s, strstr(sd_bus_message_get_member(message), "Lock"));
+        r = session_send_lock(s, /* lock= */ strstr(sd_bus_message_get_member(message), "Lock"));
+        if (r == -ENOTTY)
+                return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Session does not support lock screen.");
         if (r < 0)
                 return r;
 
@@ -248,7 +250,7 @@ static int method_set_idle_hint(sd_bus_message *message, void *userdata, sd_bus_
 
         r = session_set_idle_hint(s, b);
         if (r == -ENOTTY)
-                return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Idle hint control is not supported on non-graphical sessions.");
+                return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Idle hint control is not supported on non-graphical and non-user sessions.");
         if (r < 0)
                 return r;
 
@@ -278,7 +280,11 @@ static int method_set_locked_hint(sd_bus_message *message, void *userdata, sd_bu
         if (uid != 0 && uid != s->user->user_record->uid)
                 return sd_bus_error_set(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may set locked hint");
 
-        session_set_locked_hint(s, b);
+        r = session_set_locked_hint(s, b);
+        if (r == -ENOTTY)
+                return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Session does not support lock screen.");
+        if (r < 0)
+                return r;
 
         return sd_bus_reply_method_return(message, NULL);
 }
@@ -387,6 +393,9 @@ static int method_set_type(sd_bus_message *message, void *userdata, sd_bus_error
                 return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS,
                                          "Invalid session type '%s'", t);
 
+        if (!SESSION_CLASS_CAN_CHANGE_TYPE(s->class))
+                return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Session class doesn't support changing type.");
+
         if (!session_is_controller(s, sd_bus_message_get_sender(message)))
                 return sd_bus_error_set(error, BUS_ERROR_NOT_IN_CONTROL, "You must be in control of this session to set type");
 
@@ -470,6 +479,9 @@ static int method_take_device(sd_bus_message *message, void *userdata, sd_bus_er
         if (!DEVICE_MAJOR_VALID(major) || !DEVICE_MINOR_VALID(minor))
                 return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Device major/minor is not valid.");
 
+        if (!SESSION_CLASS_CAN_TAKE_DEVICE(s->class))
+                return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Session class doesn't support taking device control.");
+
         if (!session_is_controller(s, sd_bus_message_get_sender(message)))
                 return sd_bus_error_set(error, BUS_ERROR_NOT_IN_CONTROL, "You are not in control of this session");
 
@@ -765,6 +777,9 @@ int session_send_lock(Session *s, bool lock) {
 
         assert(s);
 
+        if (!SESSION_CLASS_CAN_LOCK(s->class))
+                return -ENOTTY;
+
         p = session_bus_path(s);
         if (!p)
                 return -ENOMEM;
@@ -786,6 +801,9 @@ int session_send_lock_all(Manager *m, bool lock) {
         HASHMAP_FOREACH(session, m->sessions) {
                 int k;
 
+                if (!SESSION_CLASS_CAN_LOCK(session->class))
+                        continue;
+
                 k = session_send_lock(session, lock);
                 if (k < 0)
                         r = k;
@@ -800,7 +818,7 @@ static bool session_ready(Session *s) {
         /* Returns true when the session is ready, i.e. all jobs we enqueued for it are done (regardless if successful or not) */
 
         return !s->scope_job &&
-                !s->user->service_job;
+                (!SESSION_CLASS_WANTS_SERVICE_MANAGER(s->class) || !s->user->service_job);
 }
 
 int session_send_create_reply(Session *s, sd_bus_error *error) {
index c60811f72efb0a1b8d76082e93aaf283287f7341..69dc52ad66ce0d025c081cd5270d38abc1bc265e 100644 (file)
@@ -722,8 +722,11 @@ static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_er
         assert(s);
         assert(s->user);
 
+        if (!SESSION_CLASS_WANTS_SCOPE(s->class))
+                return 0;
+
         if (!s->scope) {
-                _cleanup_strv_free_ char **after = NULL;
+                _cleanup_strv_free_ char **wants = NULL, **after = NULL;
                 _cleanup_free_ char *scope = NULL;
                 const char *description;
 
@@ -735,6 +738,12 @@ static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_er
 
                 description = strjoina("Session ", s->id, " of User ", s->user->user_record->user_name);
 
+                /* These two have StopWhenUnneeded= set, hence add a dep towards them */
+                wants = strv_new(s->user->runtime_dir_service,
+                                 SESSION_CLASS_WANTS_SERVICE_MANAGER(s->class) ? s->user->service : STRV_IGNORE);
+                if (!wants)
+                        return log_oom();
+
                 /* We usually want to order session scopes after systemd-user-sessions.service since the
                  * latter unit is used as login session barrier for unprivileged users. However the barrier
                  * doesn't apply for root as sysadmin should always be able to log in (and without waiting
@@ -754,9 +763,7 @@ static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_er
                                 &s->leader,
                                 s->user->slice,
                                 description,
-                                /* These two have StopWhenUnneeded= set, hence add a dep towards them */
-                                STRV_MAKE(s->user->runtime_dir_service,
-                                          s->user->service),
+                                wants,
                                 after,
                                 user_record_home_directory(s->user->user_record),
                                 properties,
@@ -810,7 +817,7 @@ static int session_setup_stop_on_idle_timer(Session *s) {
 
         assert(s);
 
-        if (s->manager->stop_idle_session_usec == USEC_INFINITY || IN_SET(s->class, SESSION_GREETER, SESSION_LOCK_SCREEN))
+        if (s->manager->stop_idle_session_usec == USEC_INFINITY || !SESSION_CLASS_CAN_STOP_ON_IDLE(s->class))
                 return 0;
 
         r = sd_event_add_time_relative(
@@ -1148,7 +1155,9 @@ found_atime:
 int session_set_idle_hint(Session *s, bool b) {
         assert(s);
 
-        if (!SESSION_TYPE_IS_GRAPHICAL(s->type))
+        if (!SESSION_CLASS_CAN_IDLE(s->class)) /* Only some session classes know the idle concept at all */
+                return -ENOTTY;
+        if (!SESSION_TYPE_IS_GRAPHICAL(s->type)) /* And only graphical session types can set the field explicitly */
                 return -ENOTTY;
 
         if (s->idle_hint == b)
@@ -1174,15 +1183,20 @@ int session_get_locked_hint(Session *s) {
         return s->locked_hint;
 }
 
-void session_set_locked_hint(Session *s, bool b) {
+int session_set_locked_hint(Session *s, bool b) {
         assert(s);
 
+        if (!SESSION_CLASS_CAN_LOCK(s->class))
+                return -ENOTTY;
+
         if (s->locked_hint == b)
-                return;
+                return 0;
 
         s->locked_hint = b;
+        (void) session_save(s);
+        (void) session_send_changed(s, "LockedHint", NULL);
 
-        session_send_changed(s, "LockedHint", NULL);
+        return 1;
 }
 
 void session_set_type(Session *s, SessionType t) {
@@ -1632,11 +1646,14 @@ static const char* const session_type_table[_SESSION_TYPE_MAX] = {
 DEFINE_STRING_TABLE_LOOKUP(session_type, SessionType);
 
 static const char* const session_class_table[_SESSION_CLASS_MAX] = {
-        [SESSION_USER]        = "user",
-        [SESSION_USER_EARLY]  = "user-early",
-        [SESSION_GREETER]     = "greeter",
-        [SESSION_LOCK_SCREEN] = "lock-screen",
-        [SESSION_BACKGROUND]  = "background",
+        [SESSION_USER]              = "user",
+        [SESSION_USER_EARLY]        = "user-early",
+        [SESSION_GREETER]           = "greeter",
+        [SESSION_LOCK_SCREEN]       = "lock-screen",
+        [SESSION_BACKGROUND]        = "background",
+        [SESSION_BACKGROUND_LIGHT]  = "background-light",
+        [SESSION_MANAGER]           = "manager",
+        [SESSION_MANAGER_EARLY]     = "manager-early",
 };
 
 DEFINE_STRING_TABLE_LOOKUP(session_class, SessionClass);
index a9c44b465f73fe7cf772ccd673ddea088cba2305..6a0fb5430359f4fbb8895c66f7f786a4cf84d3d2 100644 (file)
@@ -25,13 +25,43 @@ typedef enum SessionClass {
         SESSION_GREETER,            /* A login greeter pseudo-session */
         SESSION_LOCK_SCREEN,        /* A lock screen */
         SESSION_BACKGROUND,         /* Things like cron jobs, which are non-interactive */
+        SESSION_BACKGROUND_LIGHT,   /* Like SESSION_BACKGROUND, but without the service manager */
+        SESSION_MANAGER,            /* The service manager */
+        SESSION_MANAGER_EARLY,      /* The service manager for root (which is allowed to run before systemd-user-sessions.service) */
         _SESSION_CLASS_MAX,
         _SESSION_CLASS_INVALID = -EINVAL,
 } SessionClass;
 
-/* Whether we shall allow sessions of this class to run before 'systemd-user-sessions.service'. For now,
- * there's only one class we allow this for. It's generally set for root sessions, but no one else. */
-#define SESSION_CLASS_IS_EARLY(class) ((class) == SESSION_USER_EARLY)
+/* Whether we shall allow sessions of this class to run before 'systemd-user-sessions.service'. It's
+ * generally set for root sessions, but no one else. */
+#define SESSION_CLASS_IS_EARLY(class) IN_SET((class), SESSION_USER_EARLY, SESSION_MANAGER_EARLY)
+
+/* Which session classes want their own scope units? (all of them, except the manager, which comes in its own service unit already */
+#define SESSION_CLASS_WANTS_SCOPE(class) IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_GREETER, SESSION_LOCK_SCREEN, SESSION_BACKGROUND, SESSION_BACKGROUND_LIGHT)
+
+/* Which session classes want their own per-user service manager? */
+#define SESSION_CLASS_WANTS_SERVICE_MANAGER(class) IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_GREETER, SESSION_LOCK_SCREEN, SESSION_BACKGROUND)
+
+/* Which session classes can pin our user tracking? */
+#define SESSION_CLASS_PIN_USER(class) (!IN_SET((class), SESSION_MANAGER, SESSION_MANAGER_EARLY))
+
+/* Which session classes decide whether system is idle? (should only cover sessions that have input, and are not idle screens themselves)*/
+#define SESSION_CLASS_CAN_IDLE(class) (IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_GREETER))
+
+/* Which session classes have a lock screen concept? */
+#define SESSION_CLASS_CAN_LOCK(class) (IN_SET((class), SESSION_USER, SESSION_USER_EARLY))
+
+/* Which sessions are candidates to become "display" sessions */
+#define SESSION_CLASS_CAN_DISPLAY(class) (IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_GREETER))
+
+/* Which sessions classes should be subject to stop-in-idle */
+#define SESSION_CLASS_CAN_STOP_ON_IDLE(class) (IN_SET((class), SESSION_USER, SESSION_USER_EARLY))
+
+/* Which session classes can take control of devices */
+#define SESSION_CLASS_CAN_TAKE_DEVICE(class) (IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_GREETER, SESSION_LOCK_SCREEN))
+
+/* Which session classes allow changing session types */
+#define SESSION_CLASS_CAN_CHANGE_TYPE(class) (IN_SET((class), SESSION_USER, SESSION_USER_EARLY, SESSION_GREETER, SESSION_LOCK_SCREEN))
 
 typedef enum SessionType {
         SESSION_UNSPECIFIED,
@@ -146,7 +176,7 @@ bool session_is_active(Session *s);
 int session_get_idle_hint(Session *s, dual_timestamp *t);
 int session_set_idle_hint(Session *s, bool b);
 int session_get_locked_hint(Session *s);
-void session_set_locked_hint(Session *s, bool b);
+int session_set_locked_hint(Session *s, bool b);
 void session_set_type(Session *s, SessionType t);
 int session_set_display(Session *s, const char *display);
 int session_set_tty(Session *s, const char *tty);
index 4868eb48fb9f3dc33aa737b4a4f91facb09ee2df..e6e57ad79ee41ac5c529706b1c5e04c176ad0061 100644 (file)
@@ -63,6 +63,7 @@ int user_new(User **ret,
                 .manager = m,
                 .user_record = user_record_ref(ur),
                 .last_session_timestamp = USEC_INFINITY,
+                .gc_mode = USER_GC_BY_ANY,
         };
 
         if (asprintf(&u->state_file, "/run/systemd/users/" UID_FMT, ur->uid) < 0)
@@ -162,10 +163,12 @@ static int user_save_internal(User *u) {
                 "# This is private data. Do not parse.\n"
                 "NAME=%s\n"
                 "STATE=%s\n"         /* friendly user-facing state */
-                "STOPPING=%s\n",     /* low-level state */
+                "STOPPING=%s\n"      /* low-level state */
+                "GC_MODE=%s\n",
                 u->user_record->user_name,
                 user_state_to_string(user_get_state(u)),
-                yes_no(u->stopping));
+                yes_no(u->stopping),
+                user_gc_mode_to_string(u->gc_mode));
 
         /* LEGACY: no-one reads RUNTIME= anymore, drop it at some point */
         if (u->runtime_path)
@@ -302,7 +305,7 @@ int user_save(User *u) {
 }
 
 int user_load(User *u) {
-        _cleanup_free_ char *realtime = NULL, *monotonic = NULL, *stopping = NULL, *last_session_timestamp = NULL;
+        _cleanup_free_ char *realtime = NULL, *monotonic = NULL, *stopping = NULL, *last_session_timestamp = NULL, *gc_mode = NULL;
         int r;
 
         assert(u);
@@ -312,7 +315,8 @@ int user_load(User *u) {
                            "STOPPING",               &stopping,
                            "REALTIME",               &realtime,
                            "MONOTONIC",              &monotonic,
-                           "LAST_SESSION_TIMESTAMP", &last_session_timestamp);
+                           "LAST_SESSION_TIMESTAMP", &last_session_timestamp,
+                           "GC_MODE",                &gc_mode);
         if (r == -ENOENT)
                 return 0;
         if (r < 0)
@@ -333,10 +337,24 @@ int user_load(User *u) {
         if (last_session_timestamp)
                 (void) deserialize_usec(last_session_timestamp, &u->last_session_timestamp);
 
+        u->gc_mode = user_gc_mode_from_string(gc_mode);
+        if (u->gc_mode < 0)
+                u->gc_mode = USER_GC_BY_PIN;
+
         return 0;
 }
 
-static void user_start_service(User *u) {
+static bool user_wants_service_manager(User *u) {
+        assert(u);
+
+        LIST_FOREACH(sessions_by_user, s, u->sessions)
+                if (SESSION_CLASS_WANTS_SERVICE_MANAGER(s->class))
+                        return true;
+
+        return false;
+}
+
+void user_start_service_manager(User *u) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         int r;
 
@@ -346,6 +364,12 @@ static void user_start_service(User *u) {
          * start the per-user slice or the systemd-runtime-dir@.service instance, as those are pulled in both by
          * user@.service and the session scopes as dependencies. */
 
+        if (u->stopping) /* Don't try to start this if the user is going down */
+                return;
+
+        if (!user_wants_service_manager(u)) /* Only start user service manager if there's at least one session which wants it */
+                return;
+
         u->service_job = mfree(u->service_job);
 
         r = manager_start_unit(u->manager, u->service, &error, &u->service_job);
@@ -448,7 +472,7 @@ int user_start(User *u) {
         u->stopping = false;
 
         if (!u->started)
-                log_debug("Starting services for new user %s.", u->user_record->user_name);
+                log_debug("Tracking new user %s.", u->user_record->user_name);
 
         /* Save the user data so far, because pam_systemd will read the XDG_RUNTIME_DIR out of it while starting up
          * systemd --user.  We need to do user_save_internal() because we have not "officially" started yet. */
@@ -458,7 +482,7 @@ int user_start(User *u) {
         (void) user_update_slice(u);
 
         /* Start user@UID.service */
-        user_start_service(u);
+        user_start_service_manager(u);
 
         if (!u->started) {
                 if (!dual_timestamp_is_set(&u->timestamp))
@@ -575,6 +599,9 @@ int user_get_idle_hint(User *u, dual_timestamp *t) {
                 dual_timestamp k;
                 int ih;
 
+                if (!SESSION_CLASS_CAN_IDLE(s->class))
+                        continue;
+
                 ih = session_get_idle_hint(s, &k);
                 if (ih < 0)
                         return ih;
@@ -651,6 +678,29 @@ static usec_t user_get_stop_delay(User *u) {
         return u->manager->user_stop_delay;
 }
 
+static bool user_pinned_by_sessions(User *u) {
+        assert(u);
+
+        /* Returns true if at least one session exists that shall keep the user tracking alive. That
+         * generally means one session that isn't the service manager still exists. */
+
+        switch (u->gc_mode) {
+
+        case USER_GC_BY_ANY:
+                return u->sessions;
+
+        case USER_GC_BY_PIN:
+                LIST_FOREACH(sessions_by_user, i, u->sessions)
+                        if (SESSION_CLASS_PIN_USER(i->class))
+                                return true;
+
+                return false;
+
+        default:
+                assert_not_reached();
+        }
+}
+
 bool user_may_gc(User *u, bool drop_not_started) {
         int r;
 
@@ -659,7 +709,7 @@ bool user_may_gc(User *u, bool drop_not_started) {
         if (drop_not_started && !u->started)
                 return true;
 
-        if (u->sessions)
+        if (user_pinned_by_sessions(u))
                 return false;
 
         if (u->last_session_timestamp != USEC_INFINITY) {
@@ -718,22 +768,26 @@ UserState user_get_state(User *u) {
         if (!u->started || u->service_job)
                 return USER_OPENING;
 
-        if (u->sessions) {
-                bool all_closing = true;
+        bool any = false, all_closing = true;
+        LIST_FOREACH(sessions_by_user, i, u->sessions) {
+                SessionState state;
 
-                LIST_FOREACH(sessions_by_user, i, u->sessions) {
-                        SessionState state;
+                /* Ignore sessions that don't pin the user, i.e. are not supposed to have an effect on user state */
+                if (!SESSION_CLASS_PIN_USER(i->class))
+                        continue;
 
-                        state = session_get_state(i);
-                        if (state == SESSION_ACTIVE)
-                                return USER_ACTIVE;
-                        if (state != SESSION_CLOSING)
-                                all_closing = false;
-                }
+                state = session_get_state(i);
+                if (state == SESSION_ACTIVE)
+                        return USER_ACTIVE;
+                if (state != SESSION_CLOSING)
+                        all_closing = false;
 
-                return all_closing ? USER_CLOSING : USER_ONLINE;
+                any = true;
         }
 
+        if (any)
+                return all_closing ? USER_CLOSING : USER_ONLINE;
+
         if (user_check_linger_file(u) > 0 && user_unit_active(u))
                 return USER_LINGERING;
 
@@ -750,7 +804,7 @@ static bool elect_display_filter(Session *s) {
         /* Return true if the session is a candidate for the user’s ‘primary session’ or ‘display’. */
         assert(s);
 
-        return IN_SET(s->class, SESSION_USER, SESSION_GREETER) && s->started && !s->stopping;
+        return SESSION_CLASS_CAN_DISPLAY(s->class) && s->started && !s->stopping;
 }
 
 static int elect_display_compare(Session *s1, Session *s2) {
@@ -828,7 +882,7 @@ void user_update_last_session_timer(User *u) {
 
         assert(u);
 
-        if (u->sessions) {
+        if (user_pinned_by_sessions(u)) {
                 /* There are sessions, turn off the timer */
                 u->last_session_timestamp = USEC_INFINITY;
                 u->timer_event_source = sd_event_source_unref(u->timer_event_source);
@@ -876,6 +930,13 @@ static const char* const user_state_table[_USER_STATE_MAX] = {
 
 DEFINE_STRING_TABLE_LOOKUP(user_state, UserState);
 
+static const char* const user_gc_mode_table[_USER_GC_MODE_MAX] = {
+        [USER_GC_BY_PIN] = "pin",
+        [USER_GC_BY_ANY] = "any",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(user_gc_mode, UserGCMode);
+
 int config_parse_tmpfs_size(
                 const char* unit,
                 const char *filename,
index 21b9f8f348ec5d73b1eecbfd534ef78e783e1a70..9bda5dde4218139daf25bbbb690426392f380a76 100644 (file)
@@ -19,6 +19,13 @@ typedef enum UserState {
         _USER_STATE_INVALID = -EINVAL,
 } UserState;
 
+typedef enum UserGCMode {
+        USER_GC_BY_ANY,         /* any session pins this user */
+        USER_GC_BY_PIN,         /* only sessions with an explicitly pinning class pin this user */
+        _USER_GC_MODE_MAX,
+        _USER_GC_MODE_INVALID = -EINVAL,
+} UserGCMode;
+
 struct User {
         Manager *manager;
 
@@ -41,6 +48,7 @@ struct User {
         /* Set up when the last session of the user logs out */
         sd_event_source *timer_event_source;
 
+        UserGCMode gc_mode;
         bool in_gc_queue:1;
 
         bool started:1;       /* Whenever the user being started, has been started or is being stopped again. */
@@ -57,6 +65,7 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(User *, user_free);
 
 bool user_may_gc(User *u, bool drop_not_started);
 void user_add_to_gc_queue(User *u);
+void user_start_service_manager(User *u);
 int user_start(User *u);
 int user_stop(User *u, bool force);
 int user_finalize(User *u);
@@ -72,4 +81,7 @@ void user_update_last_session_timer(User *u);
 const char* user_state_to_string(UserState s) _const_;
 UserState user_state_from_string(const char *s) _pure_;
 
+const char* user_gc_mode_to_string(UserGCMode m) _const_;
+UserGCMode user_gc_mode_from_string(const char *s) _pure_;
+
 CONFIG_PARSER_PROTOTYPE(config_parse_compat_user_tasks_max);
index 297689203b0a5d6a5318cc60278f2bb7f46f5070..d7814a7275dfb4f4ee96325036eb30ce04dc440b 100644 (file)
@@ -936,30 +936,21 @@ _public_ PAM_EXTERN int pam_sm_open_session(
         if (r != PAM_SUCCESS)
                 return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get PAM items: @PAMERR@");
 
-        /* Make sure we don't enter a loop by talking to systemd-logind when it is actually waiting for the
-         * background to finish start-up. If the service is "systemd-user" we simply set XDG_RUNTIME_DIR and
-         * leave. */
-
-        if (streq_ptr(service, "systemd-user")) {
-                char rt[STRLEN("/run/user/") + DECIMAL_STR_MAX(uid_t)];
-
-                xsprintf(rt, "/run/user/"UID_FMT, ur->uid);
-                r = configure_runtime_directory(handle, ur, rt);
-                if (r != PAM_SUCCESS)
-                        return r;
-
-                goto success;
-        }
-
-        /* Otherwise, we ask logind to create a session for us */
-
         seat = getenv_harder(handle, "XDG_SEAT", NULL);
         cvtnr = getenv_harder(handle, "XDG_VTNR", NULL);
         type = getenv_harder(handle, "XDG_SESSION_TYPE", type_pam);
         class = getenv_harder(handle, "XDG_SESSION_CLASS", class_pam);
         desktop = getenv_harder(handle, "XDG_SESSION_DESKTOP", desktop_pam);
 
-        if (tty && strchr(tty, ':')) {
+        if (streq_ptr(service, "systemd-user")) {
+                /* If we detect that we are running in the "systemd-user" PAM stack, then let's patch the class to
+                 * 'manager' if not set, simply for robustness reasons. */
+                type = "unspecified";
+                class = IN_SET(user_record_disposition(ur), USER_INTRINSIC, USER_SYSTEM, USER_DYNAMIC) ?
+                        "manager-early" : "manager";
+                tty = NULL;
+
+        } else if (tty && strchr(tty, ':')) {
                 /* A tty with a colon is usually an X11 display, placed there to show up in utmp. We rearrange things
                  * and don't pretend that an X display was a tty. */
                 if (isempty(display))
index 8ea801ee96ee44069b0eb3e4219f3065b59fa947..5c230c0a45b02da4f928d83ce6c8d00d8c245a32 100755 (executable)
@@ -258,7 +258,7 @@ cleanup_session() (
 
     systemctl stop getty@tty2.service
 
-    for s in $(loginctl --no-legend list-sessions | awk '$3 == "logind-test-user" { print $1 }'); do
+    for s in $(loginctl --no-legend list-sessions | grep tty | awk '$3 == "logind-test-user" { print $1 }'); do
         echo "INFO: stopping session $s"
         loginctl terminate-session "$s"
     done
@@ -308,18 +308,18 @@ check_session() (
 
     local seat session leader_pid
 
-    if [[ $(loginctl --no-legend | grep -c "logind-test-user") != 1 ]]; then
+    if [[ $(loginctl --no-legend | grep tty | grep -c "logind-test-user") != 1 ]]; then
         echo "no session or multiple sessions for logind-test-user." >&2
         return 1
     fi
 
-    seat=$(loginctl --no-legend | grep 'logind-test-user *seat' | awk '{ print $4 }')
+    seat=$(loginctl --no-legend | grep tty | grep 'logind-test-user *seat' | awk '{ print $4 }')
     if [[ -z "$seat" ]]; then
         echo "no seat found for user logind-test-user" >&2
         return 1
     fi
 
-    session=$(loginctl --no-legend | awk '$3 == "logind-test-user" { print $1 }')
+    session=$(loginctl --no-legend | grep tty | awk '$3 == "logind-test-user" { print $1 }')
     if [[ -z "$session" ]]; then
         echo "no session found for user logind-test-user" >&2
         return 1
@@ -364,7 +364,7 @@ EOF
         check_session && break
     done
     check_session
-    assert_eq "$(loginctl --no-legend | awk '$3=="logind-test-user" { print $5 }')" "tty2"
+    assert_eq "$(loginctl --no-legend | grep tty | awk '$3=="logind-test-user" { print $5 }')" "tty2"
 }
 
 testcase_sanity_check() {
@@ -455,7 +455,7 @@ EOF
     udevadm info "$dev"
 
     # trigger logind and activate session
-    loginctl activate "$(loginctl --no-legend | awk '$3 == "logind-test-user" { print $1 }')"
+    loginctl activate "$(loginctl --no-legend | grep tty | awk '$3 == "logind-test-user" { print $1 }')"
 
     # check ACL
     sleep 1
@@ -496,7 +496,7 @@ testcase_lock_idle_action() {
         return
     fi
 
-    if loginctl --no-legend | grep -q logind-test-user; then
+    if loginctl --no-legend | grep tty | grep -q logind-test-user; then
         echo >&2 "Session of the 'logind-test-user' is already present."
         exit 1
     fi
@@ -545,7 +545,7 @@ testcase_session_properties() {
     trap cleanup_session RETURN
     create_session
 
-    s=$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $1 }')
+    s=$(loginctl list-sessions --no-legend | grep tty | awk '$3 == "logind-test-user" { print $1 }')
     /usr/lib/systemd/tests/unit-tests/manual/test-session-properties "/org/freedesktop/login1/session/_3${s?}" /dev/tty2
 }
 
@@ -561,17 +561,17 @@ testcase_list_users_sessions_seats() {
     create_session
 
     # Activate the session
-    loginctl activate "$(loginctl --no-legend | awk '$3 == "logind-test-user" { print $1 }')"
+    loginctl activate "$(loginctl --no-legend | grep tty | awk '$3 == "logind-test-user" { print $1 }')"
 
-    session=$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $1 }')
+    session=$(loginctl list-sessions --no-legend | grep tty | awk '$3 == "logind-test-user" { print $1 }')
     : check that we got a valid session id
     busctl get-property org.freedesktop.login1 "/org/freedesktop/login1/session/_3${session?}" org.freedesktop.login1.Session Id
-    assert_eq "$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $2 }')" "$(id -ru logind-test-user)"
-    seat=$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $4 }')
-    assert_eq "$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $5 }')" tty2
-    assert_eq "$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $6 }')" active
-    assert_eq "$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $7 }')" no
-    assert_eq "$(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $8 }')" '-'
+    assert_eq "$(loginctl list-sessions --no-legend | grep tty | awk '$3 == "logind-test-user" { print $2 }')" "$(id -ru logind-test-user)"
+    seat=$(loginctl list-sessions --no-legend | grep tty | awk '$3 == "logind-test-user" { print $4 }')
+    assert_eq "$(loginctl list-sessions --no-legend | grep tty | awk '$3 == "logind-test-user" { print $5 }')" tty2
+    assert_eq "$(loginctl list-sessions --no-legend | grep tty | awk '$3 == "logind-test-user" { print $6 }')" active
+    assert_eq "$(loginctl list-sessions --no-legend | grep tty | awk '$3 == "logind-test-user" { print $7 }')" no
+    assert_eq "$(loginctl list-sessions --no-legend | grep tty | awk '$3 == "logind-test-user" { print $8 }')" '-'
 
     loginctl list-seats --no-legend | grep -Fwq "${seat?}"
 
@@ -582,10 +582,10 @@ testcase_list_users_sessions_seats() {
     loginctl enable-linger logind-test-user
     assert_eq "$(loginctl list-users --no-legend | awk '$2 == "logind-test-user" { print $3 }')" yes
 
-    for s in $(loginctl list-sessions --no-legend | awk '$3 == "logind-test-user" { print $1 }'); do
+    for s in $(loginctl list-sessions --no-legend | grep tty | awk '$3 == "logind-test-user" { print $1 }'); do
         loginctl terminate-session "$s"
     done
-    if ! timeout 30 bash -c "while loginctl --no-legend | grep -q logind-test-user; do sleep 1; done"; then
+    if ! timeout 30 bash -c "while loginctl --no-legend | grep tty | grep -q logind-test-user; do sleep 1; done"; then
         echo "WARNING: session for logind-test-user still active, ignoring."
         return
     fi
@@ -613,7 +613,7 @@ testcase_stop_idle_session() {
     create_session
     trap teardown_stop_idle_session RETURN
 
-    id="$(loginctl --no-legend | awk '$3 == "logind-test-user" { print $1; }')"
+    id="$(loginctl --no-legend | grep tty | awk '$3 == "logind-test-user" { print $1; }')"
     ts="$(date '+%H:%M:%S')"
 
     mkdir -p /run/systemd/logind.conf.d
@@ -625,7 +625,7 @@ EOF
     sleep 5
 
     assert_eq "$(journalctl -b -u systemd-logind.service --since="$ts" --grep "Session \"$id\" of user \"logind-test-user\" is idle, stopping." | wc -l)" 1
-    assert_eq "$(loginctl --no-legend | grep -c "logind-test-user")" 0
+    assert_eq "$(loginctl --no-legend | grep tty | grep -c "logind-test-user")" 0
 }
 
 testcase_ambient_caps() {
@@ -680,6 +680,51 @@ EOF
     rm -f "$SCRIPT" "$PAMSERVICE"
 }
 
+background_at_return() {
+    rm -f /etc/pam.d/"$PAMSERVICE"
+    unset PAMSERVICE
+}
+
+testcase_background() {
+
+    local uid TRANSIENTUNIT1 TRANSIENTUNIT2
+
+    uid=$(id -u logind-test-user)
+
+    systemctl stop user@"$uid".service
+
+    PAMSERVICE="pamserv$RANDOM"
+    TRANSIENTUNIT1="bg$RANDOM.service"
+    TRANSIENTUNIT2="bgg$RANDOM.service"
+
+    trap background_at_return RETURN
+
+    cat > /etc/pam.d/"$PAMSERVICE" <<EOF
+auth sufficient    pam_unix.so
+auth required      pam_deny.so
+account sufficient pam_unix.so
+account required   pam_permit.so
+session optional   pam_systemd.so debug
+session required   pam_unix.so
+EOF
+
+    systemd-run -u "$TRANSIENTUNIT1" -p PAMName="$PAMSERVICE" -p "Environment=XDG_SESSION_CLASS=background-light" -p Type=exec -p User=logind-test-user sleep infinity
+
+    # This was a 'light' background service, hence the service manager should not be running
+    (! systemctl is-active user@"$uid".service )
+
+    systemctl stop "$TRANSIENTUNIT1"
+
+    systemd-run -u "$TRANSIENTUNIT2" -p PAMName="$PAMSERVICE" -p "Environment=XDG_SESSION_CLASS=background" -p Type=exec -p User=logind-test-user sleep infinity
+
+    # This was a regular background service, hence the service manager should be running
+    systemctl is-active user@"$uid".service
+
+    systemctl stop "$TRANSIENTUNIT2"
+
+    systemctl stop user@"$uid".service
+}
+
 setup_test_user
 test_write_dropin
 run_testcases