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