]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
logind: add ListSeats Varlink method
authorYaping Li <202858510+YapingLi04@users.noreply.github.com>
Sun, 10 May 2026 14:50:13 +0000 (14:50 +0000)
committerYaping Li <202858510+YapingLi04@users.noreply.github.com>
Fri, 22 May 2026 02:56:24 +0000 (02:56 +0000)
The Varlink ListSeats method accepts an optional Id filter, folding in
the D-Bus GetSeat(s) lookup.

Passing Id yields a single reply on match, or NoSuchSeat on miss.
Passing no Id with the 'more' flag streams the full list; passing no
Id without 'more' resolves to the caller's seat (preserving the
ergonomic default of GetSeat). The Id filter supports the special
names "self" and "auto" which resolve to the caller's seat.

The SeatInfo type in the io.systemd.Login Varlink IDL carries all seat
properties matching the D-Bus org.freedesktop.login1.Seat interface.

src/login/logind-seat.c
src/login/logind-seat.h
src/login/logind-varlink.c
src/shared/varlink-io.systemd.Login.c
test/units/TEST-35-LOGIN.sh

index c20c6bb3c7e8fb326a8897e14045fa315a8f3dc7..1fe1eb41f45f32f58d82cb496c00fc99379d1aec 100644 (file)
@@ -16,6 +16,7 @@
 #include "fs-util.h"
 #include "hashmap.h"
 #include "id128-util.h"
+#include "json-util.h"
 #include "log.h"
 #include "logind.h"
 #include "logind-device.h"
@@ -826,6 +827,61 @@ void seat_add_to_gc_queue(Seat *s) {
         s->in_gc_queue = true;
 }
 
+static int seat_sessions_build_json(sd_json_variant **ret, const char *name, void *userdata) {
+        Seat *s = ASSERT_PTR(userdata);
+        int r;
+
+        assert(ret);
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+        LIST_FOREACH(sessions_by_seat, session, s->sessions) {
+                r = sd_json_variant_append_arrayb(&v, SD_JSON_BUILD_STRING(session->id));
+                if (r < 0)
+                        return r;
+        }
+
+        *ret = TAKE_PTR(v);
+        return 0;
+}
+
+static int seat_context_build_json(sd_json_variant **ret, const char *name, void *userdata) {
+        Seat *s = ASSERT_PTR(userdata);
+
+        assert(ret);
+
+        return sd_json_buildo(
+                        ret,
+                        SD_JSON_BUILD_PAIR_STRING("Id", s->id));
+}
+
+static int seat_runtime_build_json(sd_json_variant **ret, const char *name, void *userdata) {
+        Seat *s = ASSERT_PTR(userdata);
+
+        assert(ret);
+
+        dual_timestamp idle_ts;
+        bool idle = seat_get_idle_hint(s, &idle_ts);
+
+        return sd_json_buildo(
+                        ret,
+                        JSON_BUILD_PAIR_STRING_NON_EMPTY("ActiveSession", s->active ? s->active->id : NULL),
+                        JSON_BUILD_PAIR_CALLBACK_NON_NULL("Sessions", seat_sessions_build_json, s),
+                        SD_JSON_BUILD_PAIR_BOOLEAN("CanTTY", seat_can_tty(s)),
+                        SD_JSON_BUILD_PAIR_BOOLEAN("CanGraphical", seat_can_graphical(s)),
+                        SD_JSON_BUILD_PAIR_BOOLEAN("IdleHint", idle),
+                        SD_JSON_BUILD_PAIR_CONDITION(idle, "IdleSinceHint", JSON_BUILD_DUAL_TIMESTAMP(&idle_ts)));
+}
+
+int seat_build_json(Seat *s, sd_json_variant **ret) {
+        assert(s);
+        assert(ret);
+
+        return sd_json_buildo(
+                        ret,
+                        SD_JSON_BUILD_PAIR_CALLBACK("context", seat_context_build_json, s),
+                        SD_JSON_BUILD_PAIR_CALLBACK("runtime", seat_runtime_build_json, s));
+}
+
 static bool seat_name_valid_char(char c) {
         return
                 ascii_isalpha(c) ||
index 5ca657cafbea144725c68b4564031815f196b1a7..3f6d2aaf3c93069cbdac2a1bcdded81856c1574e 100644 (file)
@@ -64,6 +64,8 @@ int seat_stop_sessions(Seat *s, bool force);
 bool seat_may_gc(Seat *s, bool drop_not_started);
 void seat_add_to_gc_queue(Seat *s);
 
+int seat_build_json(Seat *s, sd_json_variant **ret);
+
 bool seat_name_is_valid(const char *name);
 bool seat_is_self(const char *name);
 bool seat_is_auto(const char *name);
index e7fd4febd0a03f26efa58beed711d020d914a6cd..89dcbf87789936d6bb0d805137b2deba50535a2c 100644 (file)
@@ -586,6 +586,110 @@ static int vl_method_list_users(sd_varlink *link, sd_json_variant *parameters, s
         return 0;
 }
 
+static int manager_varlink_get_seat_by_name(
+                Manager *m,
+                sd_varlink *link,
+                const char *name,
+                Seat **ret) {
+
+        int r;
+
+        assert(m);
+        assert(link);
+        assert(name);
+        assert(ret);
+
+        /* Resolves a seat name to a seat object. Supports the special names "self" and "auto" — these
+         * resolve to the seat of the caller's session. Returns -EINVAL on invalid seat name. Returns
+         * -ESRCH on "not found". Caller is expected to turn that into a varlink error. */
+
+        if (seat_is_self(name) || seat_is_auto(name)) {
+                Session *session;
+                r = manager_varlink_get_session_by_peer(m, link, /* consult_display= */ seat_is_auto(name), &session);
+                if (r < 0)
+                        return r;
+                if (!session || !session->seat)
+                        return -ESRCH;
+
+                *ret = session->seat;
+                return 0;
+        }
+
+        if (!seat_name_is_valid(name))
+                return -EINVAL;
+
+        Seat *seat = hashmap_get(m->seats, name);
+        if (!seat)
+                return -ESRCH;
+
+        *ret = seat;
+        return 0;
+}
+
+static int emit_seat_reply(sd_varlink *link, Seat *seat) {
+        assert(link);
+        assert(seat);
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+        int r = seat_build_json(seat, &v);
+        if (r < 0)
+                return r;
+
+        return sd_varlink_reply(link, v);
+}
+
+static int vl_method_list_seats(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+        Manager *m = ASSERT_PTR(userdata);
+        int r;
+
+        const char *id = NULL;
+
+        static const sd_json_dispatch_field dispatch_table[] = {
+                { "Id", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, 0, 0 },
+                {}
+        };
+
+        r = sd_varlink_dispatch(link, parameters, dispatch_table, &id);
+        if (r != 0)
+                return r;
+
+        /* Unique-key path: Id provided. Single reply or NoSuchSeat. */
+        if (id) {
+                r = sd_varlink_set_sentinel(link, "io.systemd.Login.NoSuchSeat");
+                if (r < 0)
+                        return r;
+
+                Seat *seat;
+                r = manager_varlink_get_seat_by_name(m, link, id, &seat);
+                if (r == -EINVAL)
+                        return sd_varlink_error_invalid_parameter_name(link, "Id");
+                if (r == -ESRCH)
+                        return 0; /* triggers NoSuchSeat sentinel */
+                if (r < 0)
+                        return r;
+
+                return emit_seat_reply(link, seat);
+        }
+
+        /* Streaming path: no filter. Full list, requires 'more' flag. */
+        if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE))
+                return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, /* parameters= */ NULL);
+
+        /* Empty hashmap is a valid empty stream, not "not found" — see vl_method_list_inhibitors. */
+        r = sd_varlink_set_sentinel(link, /* error_id= */ NULL);
+        if (r < 0)
+                return r;
+
+        Seat *seat;
+        HASHMAP_FOREACH(seat, m->seats) {
+                r = emit_seat_reply(link, seat);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
 static int vl_method_release_session(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
         Manager *m = ASSERT_PTR(userdata);
         int r;
@@ -755,6 +859,7 @@ int manager_varlink_init(Manager *m, int fd) {
                         "io.systemd.Login.ReleaseSession",   vl_method_release_session,
                         "io.systemd.Login.ListSessions",     vl_method_list_sessions,
                         "io.systemd.Login.ListUsers",        vl_method_list_users,
+                        "io.systemd.Login.ListSeats",        vl_method_list_seats,
                         "io.systemd.Shutdown.PowerOff",      vl_method_power_off,
                         "io.systemd.Shutdown.Reboot",        vl_method_reboot,
                         "io.systemd.Shutdown.Halt",          vl_method_halt,
index 1060552b21600fe128d27bff30f319a49dd8b52e..5771c52c4c69b02aff4bb4388e7622f55fa9623f 100644 (file)
@@ -200,6 +200,36 @@ static SD_VARLINK_DEFINE_METHOD_FULL(
                 SD_VARLINK_FIELD_COMMENT("Runtime information of the user"),
                 SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, UserRuntime, 0));
 
+static SD_VARLINK_DEFINE_STRUCT_TYPE(
+                SeatContext,
+                SD_VARLINK_FIELD_COMMENT("The seat identifier"),
+                SD_VARLINK_DEFINE_FIELD(Id, SD_VARLINK_STRING, 0));
+
+static SD_VARLINK_DEFINE_STRUCT_TYPE(
+                SeatRuntime,
+                SD_VARLINK_FIELD_COMMENT("The currently active session on this seat, if any"),
+                SD_VARLINK_DEFINE_FIELD(ActiveSession, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+                SD_VARLINK_FIELD_COMMENT("Identifiers of sessions assigned to this seat"),
+                SD_VARLINK_DEFINE_FIELD(Sessions, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
+                SD_VARLINK_FIELD_COMMENT("Whether this seat supports text terminal sessions"),
+                SD_VARLINK_DEFINE_FIELD(CanTTY, SD_VARLINK_BOOL, 0),
+                SD_VARLINK_FIELD_COMMENT("Whether this seat supports graphical sessions"),
+                SD_VARLINK_DEFINE_FIELD(CanGraphical, SD_VARLINK_BOOL, 0),
+                SD_VARLINK_FIELD_COMMENT("Whether the seat is idle"),
+                SD_VARLINK_DEFINE_FIELD(IdleHint, SD_VARLINK_BOOL, 0),
+                SD_VARLINK_FIELD_COMMENT("Timestamp when the seat went idle, only present when IdleHint is true"),
+                SD_VARLINK_DEFINE_FIELD_BY_TYPE(IdleSinceHint, Timestamp, SD_VARLINK_NULLABLE));
+
+static SD_VARLINK_DEFINE_METHOD_FULL(
+                ListSeats,
+                SD_VARLINK_SUPPORTS_MORE,
+                SD_VARLINK_FIELD_COMMENT("If non-null, the identifier string of a seat to look up."),
+                SD_VARLINK_DEFINE_INPUT(Id, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+                SD_VARLINK_FIELD_COMMENT("Configuration of the seat"),
+                SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(context, SeatContext, 0),
+                SD_VARLINK_FIELD_COMMENT("Runtime information of the seat"),
+                SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, SeatRuntime, 0));
+
 static SD_VARLINK_DEFINE_ERROR(NoSuchSession);
 static SD_VARLINK_DEFINE_ERROR(NoSuchUser);
 static SD_VARLINK_DEFINE_ERROR(NoSuchSeat);
@@ -237,6 +267,12 @@ SD_VARLINK_DEFINE_INTERFACE(
                 &vl_type_UserRuntime,
                 SD_VARLINK_SYMBOL_COMMENT("Lists current users. If a UID or PID filter is provided, returns the single matching user; otherwise streams all current users (requires the 'more' flag). If called with no parameters and no 'more' flag, resolves to the caller's user."),
                 &vl_method_ListUsers,
+                SD_VARLINK_SYMBOL_COMMENT("Configuration aspects of a seat"),
+                &vl_type_SeatContext,
+                SD_VARLINK_SYMBOL_COMMENT("Runtime state and dynamic information of a seat"),
+                &vl_type_SeatRuntime,
+                SD_VARLINK_SYMBOL_COMMENT("Lists current seats. If an Id filter is provided (or the caller has a session), returns the single matching seat; otherwise streams all current seats (requires the 'more' flag). If called with no parameters and no 'more' flag, resolves to the caller's seat."),
+                &vl_method_ListSeats,
                 SD_VARLINK_SYMBOL_COMMENT("No session by this name found"),
                 &vl_error_NoSuchSession,
                 SD_VARLINK_SYMBOL_COMMENT("No seat by this name found"),
index f9d6e853ff162c3fabe30d915eb150b0287a9625..491b754b295ea469d308bd2818d4d46e558a1459 100755 (executable)
@@ -806,7 +806,7 @@ teardown_varlink() (
 )
 
 testcase_varlink() {
-    local session uid session_out user_out
+    local session uid session_out user_out seat_out self_err
 
     if [[ ! -c /dev/tty2 ]]; then
         echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}."
@@ -821,6 +821,7 @@ testcase_varlink() {
     varlinkctl introspect "$VARLINK_SOCKET"
     varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListSessions" >/dev/null
     varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListUsers" >/dev/null
+    varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListSeats" >/dev/null
 
     : "--- Setup test session ---"
     create_session
@@ -923,6 +924,38 @@ testcase_varlink() {
         | jq --seq -e 'select(.context.Name == "logind-test-user")' >/dev/null
     test "$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{}' | wc -l)" -ge 2
 
+    : "--- ListSeats: Id filter (single reply) ---"
+    seat_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{"Id":"seat0"}')
+    echo "$seat_out" | jq -e '.context.Id == "seat0"' >/dev/null
+    echo "$seat_out" | jq -e '.runtime.CanTTY == true' >/dev/null
+    # Sessions is a flat array of session id strings.
+    echo "$seat_out" | jq -e ".runtime.Sessions[] | select(. == \"$session\")" >/dev/null
+
+    # nonexistent seat
+    (! varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{"Id":"seat-nonexistent"}')
+
+    : "--- ListSeats: empty input without --more must require --more ---"
+    # The caller-seat fallback was removed; no filter + no --more = EXPECTED_MORE.
+    local empty_seats_err
+    empty_seats_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{}' 2>&1 || true)
+    echo "$empty_seats_err" | grep "'more' flag" >/dev/null
+    systemctl is-active systemd-logind.service >/dev/null
+
+    : "--- ListSeats: self/auto from session-less context yields NoSuchSeat ---"
+    # self/auto still resolves via peer session; running as root outside any session
+    # has no peer session, so we must get NoSuchSeat (not NoSuchSession leaked from
+    # the lookup helper).
+    local self_err
+    for id_arg in '{"Id":"self"}' '{"Id":"auto"}'; do
+        self_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSeats "$id_arg" 2>&1 || true)
+        echo "$self_err" | grep NoSuchSeat >/dev/null
+        (! echo "$self_err" | grep NoSuchSession >/dev/null)
+    done
+
+    : "--- ListSeats: streaming path ---"
+    varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{}' | grep "seat0" >/dev/null
+    test "$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListSeats '{}' | wc -l)" -ge 1
+
     : "--- ReleaseSession: NULL ID resolves to caller's session ---"
     # A caller with a logind session calling ReleaseSession '{}' (no ID) must
     # release its own session. We spawn the call inside a transient unit with