From b4de3e5759963eaabeb96b7d17770214bc8700b9 Mon Sep 17 00:00:00 2001 From: Yaping Li <202858510+YapingLi04@users.noreply.github.com> Date: Sun, 10 May 2026 14:50:13 +0000 Subject: [PATCH] logind: add ListSeats Varlink method 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 | 56 ++++++++++++++ src/login/logind-seat.h | 2 + src/login/logind-varlink.c | 105 ++++++++++++++++++++++++++ src/shared/varlink-io.systemd.Login.c | 36 +++++++++ test/units/TEST-35-LOGIN.sh | 35 ++++++++- 5 files changed, 233 insertions(+), 1 deletion(-) diff --git a/src/login/logind-seat.c b/src/login/logind-seat.c index c20c6bb3c7..1fe1eb41f4 100644 --- a/src/login/logind-seat.c +++ b/src/login/logind-seat.c @@ -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) || diff --git a/src/login/logind-seat.h b/src/login/logind-seat.h index 5ca657cafb..3f6d2aaf3c 100644 --- a/src/login/logind-seat.h +++ b/src/login/logind-seat.h @@ -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); diff --git a/src/login/logind-varlink.c b/src/login/logind-varlink.c index e7fd4febd0..89dcbf8778 100644 --- a/src/login/logind-varlink.c +++ b/src/login/logind-varlink.c @@ -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, diff --git a/src/shared/varlink-io.systemd.Login.c b/src/shared/varlink-io.systemd.Login.c index 1060552b21..5771c52c4c 100644 --- a/src/shared/varlink-io.systemd.Login.c +++ b/src/shared/varlink-io.systemd.Login.c @@ -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"), diff --git a/test/units/TEST-35-LOGIN.sh b/test/units/TEST-35-LOGIN.sh index f9d6e853ff..491b754b29 100755 --- a/test/units/TEST-35-LOGIN.sh +++ b/test/units/TEST-35-LOGIN.sh @@ -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