#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"
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) ||
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);
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;
"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,
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);
&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"),
)
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]}."
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
| 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