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.
This commit is contained in:
Yaping Li
2026-05-22 02:56:24 +00:00
parent 355f73a564
commit b4de3e5759
5 changed files with 233 additions and 1 deletions
+56
View 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) ||
+2
View 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);
+105
View 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,
+36
View 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"),
+34 -1
View File
@@ -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