logind: add ListUsers Varlink method

The Varlink ListUsers method accepts optional UID and PID filters,
folding in the D-Bus GetUser(u) and GetUserByPID(u) lookups.

Passing a unique-key filter (UID and/or PID) yields a single reply on
match, or NoSuchUser on miss. Passing no filter with the 'more' flag
streams the full list; passing no filter without 'more' resolves to
the caller's user (preserving the ergonomic default of GetUser). If
both UID and PID are specified they must reference the same user,
otherwise NoSuchUser is returned.

The UserInfo type in the io.systemd.Login Varlink IDL carries all
user properties matching the D-Bus org.freedesktop.login1.User
interface.
This commit is contained in:
Yaping Li
2026-05-22 02:56:24 +00:00
parent 1f9fb6300b
commit 355f73a564
5 changed files with 300 additions and 1 deletions
+72
View File
@@ -17,6 +17,7 @@
#include "format-util.h"
#include "fs-util.h"
#include "hashmap.h"
#include "json-util.h"
#include "limits-util.h"
#include "logind-session.h"
#include "logind.h"
@@ -961,6 +962,77 @@ void user_update_last_session_timer(User *u) {
FORMAT_TIMESPAN(user_stop_delay, USEC_PER_MSEC));
}
static int user_sessions_build_json(sd_json_variant **ret, const char *name, void *userdata) {
User *u = ASSERT_PTR(userdata);
int r;
assert(ret);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
LIST_FOREACH(sessions_by_user, session, u->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 user_context_build_json(sd_json_variant **ret, const char *name, void *userdata) {
User *u = ASSERT_PTR(userdata);
assert(ret);
assert(u->user_record);
int linger = user_check_linger_file(u);
if (linger == -ENOMEM)
return linger;
if (linger < 0)
log_warning_errno(linger,
"Failed to check linger file for user '%s', assuming disabled: %m",
u->user_record->user_name);
return sd_json_buildo(
ret,
SD_JSON_BUILD_PAIR_UNSIGNED("UID", u->user_record->uid),
SD_JSON_BUILD_PAIR_UNSIGNED("GID", u->user_record->gid),
SD_JSON_BUILD_PAIR_STRING("Name", u->user_record->user_name),
SD_JSON_BUILD_PAIR_BOOLEAN("Linger", linger > 0),
JSON_BUILD_PAIR_STRING_NON_EMPTY("Service", u->service_manager_unit),
JSON_BUILD_PAIR_STRING_NON_EMPTY("Slice", u->slice),
JSON_BUILD_PAIR_STRING_NON_EMPTY("RuntimePath", u->runtime_path));
}
static int user_runtime_build_json(sd_json_variant **ret, const char *name, void *userdata) {
User *u = ASSERT_PTR(userdata);
assert(ret);
dual_timestamp idle_ts;
bool idle = user_get_idle_hint(u, &idle_ts);
return sd_json_buildo(
ret,
JSON_BUILD_PAIR_STRING_NON_EMPTY("Display", u->display ? u->display->id : NULL),
JSON_BUILD_PAIR_ENUM("State", user_state_to_string(user_get_state(u))),
JSON_BUILD_PAIR_CALLBACK_NON_NULL("Sessions", user_sessions_build_json, u),
JSON_BUILD_PAIR_DUAL_TIMESTAMP_NON_NULL("Timestamp", &u->timestamp),
SD_JSON_BUILD_PAIR_BOOLEAN("IdleHint", idle),
SD_JSON_BUILD_PAIR_CONDITION(idle, "IdleSinceHint", JSON_BUILD_DUAL_TIMESTAMP(&idle_ts)));
}
int user_build_json(User *u, sd_json_variant **ret) {
assert(u);
assert(u->user_record);
assert(ret);
return sd_json_buildo(
ret,
SD_JSON_BUILD_PAIR_CALLBACK("context", user_context_build_json, u),
SD_JSON_BUILD_PAIR_CALLBACK("runtime", user_runtime_build_json, u));
}
static const char* const user_state_table[_USER_STATE_MAX] = {
[USER_OFFLINE] = "offline",
[USER_OPENING] = "opening",
+2
View File
@@ -83,6 +83,8 @@ int user_check_linger_file(const User *u);
void user_elect_display(User *u);
void user_update_last_session_timer(User *u);
int user_build_json(User *u, sd_json_variant **ret);
DECLARE_STRING_TABLE_LOOKUP(user_state, UserState);
DECLARE_STRING_TABLE_LOOKUP(user_gc_mode, UserGCMode);
+128
View File
@@ -459,6 +459,133 @@ static int vl_method_list_sessions(sd_varlink *link, sd_json_variant *parameters
return 0;
}
static int manager_varlink_get_user_by_uid_or_pidref(
Manager *m,
uid_t uid,
const PidRef *pidref,
User **ret) {
int r;
assert(m);
assert(ret);
/* Resolves a user by UID and/or PID. At least one filter must be set. If both are set they must
* reference the same user, otherwise -ESRCH is returned. Returns -ESRCH on "not found". Returns
* negative errno on other failures. */
User *by_uid = NULL;
if (uid_is_valid(uid)) {
by_uid = hashmap_get(m->users, UID_TO_PTR(uid));
if (!by_uid)
return -ESRCH;
}
User *by_pid = NULL;
if (pidref && pidref_is_set(pidref)) {
uid_t pid_uid;
r = cg_pidref_get_owner_uid(pidref, &pid_uid);
if (r < 0) {
if (!IN_SET(r, -ENXIO, -ENOENT))
return log_debug_errno(r, "Failed to acquire owning UID of PID " PID_FMT ": %m", pidref->pid);
/* -ENXIO: PID not in a user-N.slice cgroup (e.g. PID 1).
* -ENOENT: cgroup gone (process terminated).
* Translate to -ESRCH so the caller maps to the NoSuchUser sentinel. */
return -ESRCH;
}
by_pid = hashmap_get(m->users, UID_TO_PTR(pid_uid));
if (!by_pid)
return -ESRCH;
}
if (by_uid && by_pid && by_uid != by_pid)
return log_debug_errno(SYNTHETIC_ERRNO(ESRCH),
"Search by UID " UID_FMT " and PID " PID_FMT " resulted in two different users",
uid, pidref->pid);
assert(by_uid || by_pid);
*ret = by_uid ?: by_pid;
return 0;
}
static int emit_user_reply(sd_varlink *link, User *user) {
assert(link);
assert(user);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
int r = user_build_json(user, &v);
if (r < 0)
return r;
return sd_varlink_reply(link, v);
}
typedef struct ListUsersParameters {
uid_t uid;
PidRef pidref;
} ListUsersParameters;
static void list_users_parameters_done(ListUsersParameters *p) {
assert(p);
pidref_done(&p->pidref);
}
static int vl_method_list_users(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
Manager *m = ASSERT_PTR(userdata);
int r;
static const sd_json_dispatch_field dispatch_table[] = {
{ "UID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(ListUsersParameters, uid), 0 },
{ "PID", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_pidref, offsetof(ListUsersParameters, pidref), 0 },
{}
};
_cleanup_(list_users_parameters_done) ListUsersParameters p = {
.uid = UID_INVALID,
.pidref = PIDREF_NULL,
};
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
if (r != 0)
return r;
/* Unique-key path: UID and/or PID provided. Single reply or NoSuchUser. */
if (uid_is_valid(p.uid) || pidref_is_set(&p.pidref)) {
r = sd_varlink_set_sentinel(link, "io.systemd.Login.NoSuchUser");
if (r < 0)
return r;
User *user;
r = manager_varlink_get_user_by_uid_or_pidref(m, p.uid, &p.pidref, &user);
if (r == -ESRCH)
return 0; /* triggers NoSuchUser sentinel */
if (r < 0)
return r;
return emit_user_reply(link, user);
}
/* 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;
User *user;
HASHMAP_FOREACH(user, m->users) {
r = emit_user_reply(link, user);
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;
@@ -627,6 +754,7 @@ int manager_varlink_init(Manager *m, int fd) {
"io.systemd.Login.CreateSession", vl_method_create_session,
"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.Shutdown.PowerOff", vl_method_power_off,
"io.systemd.Shutdown.Reboot", vl_method_reboot,
"io.systemd.Shutdown.Halt", vl_method_halt,
+53
View File
@@ -156,7 +156,52 @@ static SD_VARLINK_DEFINE_METHOD(
SD_VARLINK_FIELD_COMMENT("The identifier string of the session to release. If unspecified or 'self', will return the callers session."),
SD_VARLINK_DEFINE_INPUT(Id, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_STRUCT_TYPE(
UserContext,
SD_VARLINK_FIELD_COMMENT("Numeric UNIX UID"),
SD_VARLINK_DEFINE_FIELD(UID, SD_VARLINK_INT, 0),
SD_VARLINK_FIELD_COMMENT("Numeric UNIX GID"),
SD_VARLINK_DEFINE_FIELD(GID, SD_VARLINK_INT, 0),
SD_VARLINK_FIELD_COMMENT("User name"),
SD_VARLINK_DEFINE_FIELD(Name, SD_VARLINK_STRING, 0),
SD_VARLINK_FIELD_COMMENT("Whether lingering is enabled for this user"),
SD_VARLINK_DEFINE_FIELD(Linger, SD_VARLINK_BOOL, 0),
SD_VARLINK_FIELD_COMMENT("Service manager unit name"),
SD_VARLINK_DEFINE_FIELD(Service, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("User slice unit name"),
SD_VARLINK_DEFINE_FIELD(Slice, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Path to the user's runtime directory"),
SD_VARLINK_DEFINE_FIELD(RuntimePath, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_STRUCT_TYPE(
UserRuntime,
SD_VARLINK_FIELD_COMMENT("Display session identifier"),
SD_VARLINK_DEFINE_FIELD(Display, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Current state of the user"),
SD_VARLINK_DEFINE_FIELD(State, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Identifiers of sessions belonging to this user"),
SD_VARLINK_DEFINE_FIELD(Sessions, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
SD_VARLINK_FIELD_COMMENT("Timestamp when the user was 'started' for the first time"),
SD_VARLINK_DEFINE_FIELD_BY_TYPE(Timestamp, Timestamp, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Whether the user is idle"),
SD_VARLINK_DEFINE_FIELD(IdleHint, SD_VARLINK_BOOL, 0),
SD_VARLINK_FIELD_COMMENT("Timestamp when the user 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(
ListUsers,
SD_VARLINK_SUPPORTS_MORE,
SD_VARLINK_FIELD_COMMENT("If non-null, the UNIX UID of a user to look up."),
SD_VARLINK_DEFINE_INPUT(UID, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("If non-null, return the user owning the cgroup containing the process with this PID. If both UID and PID are specified they must reference the same user, otherwise NoSuchUser is returned."),
SD_VARLINK_DEFINE_INPUT_BY_TYPE(PID, ProcessId, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Configuration of the user"),
SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(context, UserContext, 0),
SD_VARLINK_FIELD_COMMENT("Runtime information of the user"),
SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, UserRuntime, 0));
static SD_VARLINK_DEFINE_ERROR(NoSuchSession);
static SD_VARLINK_DEFINE_ERROR(NoSuchUser);
static SD_VARLINK_DEFINE_ERROR(NoSuchSeat);
static SD_VARLINK_DEFINE_ERROR(AlreadySessionMember);
static SD_VARLINK_DEFINE_ERROR(VirtualTerminalAlreadyTaken);
@@ -186,10 +231,18 @@ SD_VARLINK_DEFINE_INTERFACE(
&vl_method_ReleaseSession,
SD_VARLINK_SYMBOL_COMMENT("Lists current sessions. If an ID or PID filter is provided, returns the single matching session; otherwise streams all current sessions (requires the 'more' flag)."),
&vl_method_ListSessions,
SD_VARLINK_SYMBOL_COMMENT("Configuration aspects of a user"),
&vl_type_UserContext,
SD_VARLINK_SYMBOL_COMMENT("Runtime state and dynamic information of a user"),
&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("No session by this name found"),
&vl_error_NoSuchSession,
SD_VARLINK_SYMBOL_COMMENT("No seat by this name found"),
&vl_error_NoSuchSeat,
SD_VARLINK_SYMBOL_COMMENT("No user by this UID found"),
&vl_error_NoSuchUser,
SD_VARLINK_SYMBOL_COMMENT("Process already member of a session"),
&vl_error_AlreadySessionMember,
SD_VARLINK_SYMBOL_COMMENT("The specified virtual terminal (VT) is already taken by another session"),
+45 -1
View File
@@ -806,7 +806,7 @@ teardown_varlink() (
)
testcase_varlink() {
local session uid session_out
local session uid session_out user_out
if [[ ! -c /dev/tty2 ]]; then
echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}."
@@ -820,6 +820,7 @@ testcase_varlink() {
: "--- Introspect ---"
varlinkctl introspect "$VARLINK_SOCKET"
varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListSessions" >/dev/null
varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListUsers" >/dev/null
: "--- Setup test session ---"
create_session
@@ -879,6 +880,49 @@ testcase_varlink() {
echo "$list_err" | grep "'more' flag" >/dev/null
systemctl is-active systemd-logind.service >/dev/null
: "--- ListUsers: UID filter (single reply) ---"
user_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers "{\"UID\":$uid}")
echo "$user_out" | jq -e ".context.UID == $uid" >/dev/null
echo "$user_out" | jq -e '.context.Name == "logind-test-user"' >/dev/null
echo "$user_out" | jq -e '.runtime.State' >/dev/null
echo "$user_out" | jq -e '.context.Linger == false' >/dev/null
# Sessions is now a flat array of session id strings (no wrapper object).
echo "$user_out" | jq -e ".runtime.Sessions[] | select(. == \"$session\")" >/dev/null
: "--- ListUsers: PID filter (single reply) ---"
# PID of a process in the test user's session should map back to the user.
local pid_user_out
pid_user_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers \
"{\"PID\":{\"pid\":$leader_pid}}")
echo "$pid_user_out" | jq -e --argjson u "$uid" '.context.UID == $u' >/dev/null
: "--- ListUsers: UID+PID consistency check ---"
# Same user referenced two ways: must succeed.
local ok_user_out mismatch_user_err
ok_user_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers \
"{\"UID\":$uid,\"PID\":{\"pid\":$leader_pid}}")
echo "$ok_user_out" | jq -e --argjson u "$uid" '.context.UID == $u' >/dev/null
# Mismatched UID and PID (PID 1 is systemd init, UID 0): must fail with NoSuchUser.
mismatch_user_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers \
"{\"UID\":$uid,\"PID\":{\"pid\":1}}" 2>&1 || true)
echo "$mismatch_user_err" | grep NoSuchUser >/dev/null
: "--- ListUsers: empty input without --more must require --more ---"
# The DescribeUser-style caller-UID fallback was removed; a no-filter call without
# --more must fail with the EXPECTED_MORE error.
local empty_users_err
empty_users_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{}' 2>&1 || true)
echo "$empty_users_err" | grep "'more' flag" >/dev/null
systemctl is-active systemd-logind.service >/dev/null
# nonexistent UID
(! varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{"UID":4294967294}')
: "--- ListUsers: streaming path ---"
varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{}' \
| 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
: "--- 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