From 1f9fb6300b3294904e430ba78dd6c9a0fbf301fe Mon Sep 17 00:00:00 2001 From: Yaping Li <202858510+YapingLi04@users.noreply.github.com> Date: Sun, 10 May 2026 14:50:12 +0000 Subject: [PATCH] logind: add ListSessions Varlink method The Varlink ListSessions method accepts optional Id and PID filters, folding in the D-Bus GetSession(s) and GetSessionByPID(u) lookups. Passing a unique-key filter (Id and/or PID) yields a single reply on match, or NoSuchSession on miss. Passing no filter streams the full list (requires the 'more' flag). Specifying both Id and PID acts as a consistency check: both must refer to the same session, otherwise NoSuchSession is returned. The Id filter supports the special names "self" and "auto" which resolve to the caller's session. The SessionInfo type in the io.systemd.Login Varlink IDL carries all session properties matching the D-Bus org.freedesktop.login1.Session interface. --- src/login/logind-session.c | 60 ++++++++ src/login/logind-session.h | 2 + src/login/logind-varlink.c | 188 ++++++++++++++++++++++++-- src/login/pam_systemd.c | 2 +- src/shared/varlink-io.systemd.Login.c | 82 ++++++++++- src/shared/varlink-io.systemd.Login.h | 3 + src/test/meson.build | 4 + src/test/test-varlink-idl-login.c | 15 ++ test/units/TEST-35-LOGIN.sh | 107 ++++++++++++++- 9 files changed, 444 insertions(+), 19 deletions(-) create mode 100644 src/test/test-varlink-idl-login.c diff --git a/src/login/logind-session.c b/src/login/logind-session.c index 1b8731e2c6..e62a5768da 100644 --- a/src/login/logind-session.c +++ b/src/login/logind-session.c @@ -28,6 +28,7 @@ #include "format-util.h" #include "fs-util.h" #include "hashmap.h" +#include "json-util.h" #include "login-util.h" #include "logind.h" #include "logind-dbus.h" @@ -1684,6 +1685,65 @@ bool session_is_auto(const char *name) { return streq_ptr(name, "auto"); } +static int session_context_build_json(sd_json_variant **ret, const char *name, void *userdata) { + Session *s = ASSERT_PTR(userdata); + + assert(ret); + assert(s->user); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_UNSIGNED("UID", s->user->user_record->uid), + JSON_BUILD_PAIR_PIDREF_NON_NULL("PID", &s->leader), + JSON_BUILD_PAIR_ENUM("Type", session_type_to_string(s->type)), + JSON_BUILD_PAIR_ENUM("Class", session_class_to_string(s->class)), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Service", s->service), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Desktop", s->desktop), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Seat", s->seat ? s->seat->id : NULL), + JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("VTNr", s->vtnr), + JSON_BUILD_PAIR_STRING_NON_EMPTY("TTY", s->tty), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Display", s->display), + SD_JSON_BUILD_PAIR_BOOLEAN("Remote", s->remote), + JSON_BUILD_PAIR_STRING_NON_EMPTY("RemoteHost", s->remote_host), + JSON_BUILD_PAIR_STRING_NON_EMPTY("RemoteUser", s->remote_user), + JSON_BUILD_PAIR_STRV_NON_EMPTY("ExtraDeviceAccess", s->extra_device_access)); +} + +static int session_runtime_build_json(sd_json_variant **ret, const char *name, void *userdata) { + Session *s = ASSERT_PTR(userdata); + + assert(ret); + + dual_timestamp idle_ts; + bool idle = session_get_idle_hint(s, &idle_ts); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_STRING("Id", s->id), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Scope", s->scope), + SD_JSON_BUILD_PAIR_CONDITION(audit_session_is_valid(s->audit_id), + "Audit", SD_JSON_BUILD_UNSIGNED(s->audit_id)), + JSON_BUILD_PAIR_DUAL_TIMESTAMP_NON_NULL("Timestamp", &s->timestamp), + JSON_BUILD_PAIR_ENUM("State", session_state_to_string(session_get_state(s))), + SD_JSON_BUILD_PAIR_BOOLEAN("Active", session_is_active(s)), + SD_JSON_BUILD_PAIR_BOOLEAN("IdleHint", idle), + SD_JSON_BUILD_PAIR_CONDITION(idle, "IdleSinceHint", JSON_BUILD_DUAL_TIMESTAMP(&idle_ts)), + SD_JSON_BUILD_PAIR_BOOLEAN("LockedHint", s->locked_hint), + SD_JSON_BUILD_PAIR_BOOLEAN("CanIdle", SESSION_CLASS_CAN_IDLE(s->class)), + SD_JSON_BUILD_PAIR_BOOLEAN("CanLock", SESSION_CLASS_CAN_LOCK(s->class))); +} + +int session_build_json(Session *s, sd_json_variant **ret) { + assert(s); + assert(s->user); + assert(ret); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_CALLBACK("context", session_context_build_json, s), + SD_JSON_BUILD_PAIR_CALLBACK("runtime", session_runtime_build_json, s)); +} + static const char* const session_state_table[_SESSION_STATE_MAX] = { [SESSION_OPENING] = "opening", [SESSION_ONLINE] = "online", diff --git a/src/login/logind-session.h b/src/login/logind-session.h index da8c52510a..b0749134af 100644 --- a/src/login/logind-session.h +++ b/src/login/logind-session.h @@ -219,3 +219,5 @@ int session_send_create_reply(Session *s, const sd_bus_error *error); bool session_is_self(const char *name); bool session_is_auto(const char *name); + +int session_build_json(Session *s, sd_json_variant **ret); diff --git a/src/login/logind-varlink.c b/src/login/logind-varlink.c index 71a445e900..9b58791991 100644 --- a/src/login/logind-varlink.c +++ b/src/login/logind-varlink.c @@ -41,7 +41,8 @@ static int manager_varlink_get_session_by_peer( assert(ret); /* Determines the session of the peer. If the peer is not part of a session, but consult_display is - * true, then will return the display session of the peer's owning user */ + * true, then will return the display session of the peer's owning user. Returns 0 with *ret set to + * NULL if no session could be determined; the caller decides which error to report to the client. */ _cleanup_(pidref_done) PidRef pidref = PIDREF_NULL; r = varlink_get_peer_pidref(link, &pidref); @@ -70,35 +71,114 @@ static int manager_varlink_get_session_by_peer( } else session = hashmap_get(m->sessions, name); + *ret = session; + return 0; +} + +static int lookup_session_by_name(Manager *m, sd_varlink *link, const char *name, Session **ret) { + int r; + + assert(m); + assert(link); + assert(name); + assert(ret); + + if (session_is_self(name) || session_is_auto(name)) { + Session *peer; + r = manager_varlink_get_session_by_peer(m, link, /* consult_display= */ session_is_auto(name), &peer); + if (r < 0) + return r; + if (!peer) + return -ESRCH; + + *ret = peer; + return 0; + } + + if (!session_id_valid(name)) + return -EINVAL; + + Session *session = hashmap_get(m->sessions, name); if (!session) - return sd_varlink_error(link, "io.systemd.Login.NoSuchSession", /* parameters= */ NULL); + return -ESRCH; *ret = session; return 0; } -static int manager_varlink_get_session_by_name( +static int lookup_session_by_pidref(Manager *m, const PidRef *pidref, Session **ret) { + int r; + + assert(m); + assert(pidref); + assert(ret); + + Session *session; + r = manager_get_session_by_pidref(m, pidref, &session); + if (r < 0) + return log_debug_errno(r, "Failed to look up session by PID " PID_FMT ": %m", pidref->pid); + if (!session) + return -ESRCH; + + *ret = session; + return 0; +} + +static int manager_varlink_get_session_by_name_or_pidref( Manager *m, sd_varlink *link, const char *name, + const PidRef *pidref, Session **ret) { + Session *by_name = NULL, *by_pid = NULL; + int r; + assert(m); assert(link); assert(ret); - /* Resolves a session name to a session object. Supports resolving the special names "self" and "auto". */ + /* Resolves a session by name and/or PID. Supports the special names "self" and "auto" for the name + * argument. If both name and pidref are unset, resolves to the caller's session. If both name and + * pidref are set they must refer to the same session, otherwise -ESRCH is returned. Returns -ESRCH + * on "not found". Caller is expected to turn that into a varlink error, typically via + * sd_varlink_set_sentinel(). Returns negative errno on other failures. */ - if (session_is_self(name)) - return manager_varlink_get_session_by_peer(m, link, /* consult_display= */ false, ret); - if (session_is_auto(name)) - return manager_varlink_get_session_by_peer(m, link, /* consult_display= */ true, ret); + if (name) { + r = lookup_session_by_name(m, link, name, &by_name); + if (r == -EINVAL) + return sd_varlink_error_invalid_parameter_name(link, "Id"); + if (r < 0) + return r; + } - Session *session = hashmap_get(m->sessions, name); - if (!session) - return sd_varlink_error(link, "io.systemd.Login.NoSuchSession", /* parameters= */ NULL); + if (pidref && pidref_is_set(pidref)) { + r = lookup_session_by_pidref(m, pidref, &by_pid); + if (r == -EINVAL) + return sd_varlink_error_invalid_parameter_name(link, "PID"); + if (r < 0) + return r; + } - *ret = session; + if (by_name && by_pid && by_name != by_pid) + return log_debug_errno(SYNTHETIC_ERRNO(ESRCH), + "Search by session id '%s' and PID " PID_FMT " resulted in two different sessions", + name, pidref->pid); + + if (by_name || by_pid) { + *ret = by_name ?: by_pid; + return 0; + } + + /* Neither filter set — fall back to caller's session. */ + Session *by_peer; + r = manager_varlink_get_session_by_peer(m, link, /* consult_display= */ true, &by_peer); + if (r < 0) + return r; + if (!by_peer) + return -ESRCH; + + *ret = by_peer; return 0; } @@ -129,7 +209,7 @@ int session_send_create_reply_varlink(Session *s, const sd_bus_error *error) { SD_JSON_BUILD_PAIR_STRING("RuntimePath", s->user->runtime_path), SD_JSON_BUILD_PAIR_UNSIGNED("UID", s->user->user_record->uid), SD_JSON_BUILD_PAIR_CONDITION(!!s->seat, "Seat", SD_JSON_BUILD_STRING(s->seat ? s->seat->id : NULL)), - SD_JSON_BUILD_PAIR_CONDITION(s->vtnr > 0, "VTNr", SD_JSON_BUILD_UNSIGNED(s->vtnr)), + JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("VTNr", s->vtnr), JSON_BUILD_PAIR_ENUM("Class", session_class_to_string(s->class)), JSON_BUILD_PAIR_ENUM("Type", session_type_to_string(s->type))); } @@ -304,6 +384,81 @@ fail: return r; } +static int emit_session_reply(sd_varlink *link, Session *session) { + assert(link); + assert(session); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + int r = session_build_json(session, &v); + if (r < 0) + return r; + + return sd_varlink_reply(link, v); +} + +typedef struct ListSessionsParameters { + const char *id; + PidRef pidref; +} ListSessionsParameters; + +static void list_sessions_parameters_done(ListSessionsParameters *p) { + assert(p); + pidref_done(&p->pidref); +} + +static int vl_method_list_sessions(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[] = { + { "Id", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(ListSessionsParameters, id), 0 }, + { "PID", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_pidref, offsetof(ListSessionsParameters, pidref), 0 }, + {} + }; + + _cleanup_(list_sessions_parameters_done) ListSessionsParameters p = { + .pidref = PIDREF_NULL, + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + /* Unique-key path: Id and/or PID provided. Single reply or NoSuchSession. */ + if (p.id || pidref_is_set(&p.pidref)) { + r = sd_varlink_set_sentinel(link, "io.systemd.Login.NoSuchSession"); + if (r < 0) + return r; + + Session *session; + r = manager_varlink_get_session_by_name_or_pidref(m, link, p.id, &p.pidref, &session); + if (r == -ESRCH) + return 0; /* triggers NoSuchSession sentinel */ + if (r < 0) + return r; + + return emit_session_reply(link, session); + } + + /* 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; + + Session *session; + HASHMAP_FOREACH(session, m->sessions) { + r = emit_session_reply(link, session); + 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; @@ -322,7 +477,9 @@ static int vl_method_release_session(sd_varlink *link, sd_json_variant *paramete return r; Session *session; - r = manager_varlink_get_session_by_name(m, link, p.id, &session); + r = manager_varlink_get_session_by_name_or_pidref(m, link, p.id, /* pidref= */ NULL, &session); + if (r == -ESRCH) + return sd_varlink_error(link, "io.systemd.Login.NoSuchSession", /* parameters= */ NULL); if (r < 0) return r; @@ -330,6 +487,8 @@ static int vl_method_release_session(sd_varlink *link, sd_json_variant *paramete r = manager_varlink_get_session_by_peer(m, link, /* consult_display= */ false, &peer_session); if (r < 0) return r; + if (!peer_session) + return sd_varlink_error(link, "io.systemd.Login.NoSuchSession", /* parameters= */ NULL); if (session != peer_session) return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, /* parameters= */ NULL); @@ -467,6 +626,7 @@ int manager_varlink_init(Manager *m, int fd) { s, "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.Shutdown.PowerOff", vl_method_power_off, "io.systemd.Shutdown.Reboot", vl_method_reboot, "io.systemd.Shutdown.Halt", vl_method_halt, diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 1b0adfae9e..b6f7ddd38c 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -1147,7 +1147,7 @@ static int register_session( JSON_BUILD_PAIR_ENUM("Class", c->class), JSON_BUILD_PAIR_STRING_NON_EMPTY("Desktop", c->desktop), JSON_BUILD_PAIR_STRING_NON_EMPTY("Seat", c->seat), - SD_JSON_BUILD_PAIR_CONDITION(c->vtnr > 0, "VTNr", SD_JSON_BUILD_UNSIGNED(c->vtnr)), + JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("VTNr", c->vtnr), JSON_BUILD_PAIR_STRING_NON_EMPTY("TTY", c->tty), JSON_BUILD_PAIR_STRING_NON_EMPTY("Display", c->display), SD_JSON_BUILD_PAIR_BOOLEAN("Remote", c->remote), diff --git a/src/shared/varlink-io.systemd.Login.c b/src/shared/varlink-io.systemd.Login.c index 9a07155ef2..42cd0aa95a 100644 --- a/src/shared/varlink-io.systemd.Login.c +++ b/src/shared/varlink-io.systemd.Login.c @@ -3,7 +3,7 @@ #include "varlink-idl-common.h" #include "varlink-io.systemd.Login.h" -static SD_VARLINK_DEFINE_ENUM_TYPE( +SD_VARLINK_DEFINE_ENUM_TYPE( SessionType, SD_VARLINK_DEFINE_ENUM_VALUE(unspecified), SD_VARLINK_DEFINE_ENUM_VALUE(tty), @@ -12,7 +12,7 @@ static SD_VARLINK_DEFINE_ENUM_TYPE( SD_VARLINK_DEFINE_ENUM_VALUE(mir), SD_VARLINK_DEFINE_ENUM_VALUE(web)); -static SD_VARLINK_DEFINE_ENUM_TYPE( +SD_VARLINK_DEFINE_ENUM_TYPE( SessionClass, SD_VARLINK_FIELD_COMMENT("Regular user sessions"), SD_VARLINK_DEFINE_ENUM_VALUE(user), @@ -37,6 +37,62 @@ static SD_VARLINK_DEFINE_ENUM_TYPE( SD_VARLINK_FIELD_COMMENT("The special session of the service manager for the root user"), SD_VARLINK_DEFINE_ENUM_VALUE(manager_early)); +static SD_VARLINK_DEFINE_STRUCT_TYPE( + SessionContext, + SD_VARLINK_FIELD_COMMENT("Numeric UNIX UID of the user owning this session"), + SD_VARLINK_DEFINE_FIELD(UID, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("PID of the session leader"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(PID, ProcessId, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The type of the session"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(Type, SessionType, 0), + SD_VARLINK_FIELD_COMMENT("The class of the session"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(Class, SessionClass, 0), + SD_VARLINK_FIELD_COMMENT("PAM service name"), + SD_VARLINK_DEFINE_FIELD(Service, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Desktop identifier"), + SD_VARLINK_DEFINE_FIELD(Desktop, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Seat this session is assigned to"), + SD_VARLINK_DEFINE_FIELD(Seat, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Virtual terminal number of the session, if applicable"), + SD_VARLINK_DEFINE_FIELD(VTNr, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("TTY device of the session"), + SD_VARLINK_DEFINE_FIELD(TTY, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("X11 display of the session"), + SD_VARLINK_DEFINE_FIELD(Display, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether this is a remote session"), + SD_VARLINK_DEFINE_FIELD(Remote, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Remote host, if this is a remote session"), + SD_VARLINK_DEFINE_FIELD(RemoteHost, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Remote user, if this is a remote session"), + SD_VARLINK_DEFINE_FIELD(RemoteUser, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Device paths this session has special access to"), + SD_VARLINK_DEFINE_FIELD(ExtraDeviceAccess, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + SessionRuntime, + SD_VARLINK_FIELD_COMMENT("The session identifier"), + SD_VARLINK_DEFINE_FIELD(Id, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("systemd scope unit name"), + SD_VARLINK_DEFINE_FIELD(Scope, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Audit session ID"), + SD_VARLINK_DEFINE_FIELD(Audit, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The session timestamps"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(Timestamp, Timestamp, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Current state of the session"), + SD_VARLINK_DEFINE_FIELD(State, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("Whether the session is active (i.e. in the foreground)"), + SD_VARLINK_DEFINE_FIELD(Active, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Whether the session is idle"), + SD_VARLINK_DEFINE_FIELD(IdleHint, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Timestamp when the session went idle, only present when IdleHint is true"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(IdleSinceHint, Timestamp, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether the session is currently locked"), + SD_VARLINK_DEFINE_FIELD(LockedHint, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Whether this session class supports idle tracking"), + SD_VARLINK_DEFINE_FIELD(CanIdle, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Whether this session class supports locking"), + SD_VARLINK_DEFINE_FIELD(CanLock, SD_VARLINK_BOOL, 0)); + static SD_VARLINK_DEFINE_METHOD( CreateSession, SD_VARLINK_FIELD_COMMENT("Numeric UNIX UID of the user this session shall be owned by"), @@ -83,6 +139,18 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_FIELD_COMMENT("The assigned session class"), SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(Class, SessionClass, 0)); +static SD_VARLINK_DEFINE_METHOD_FULL( + ListSessions, + SD_VARLINK_SUPPORTS_MORE, + SD_VARLINK_FIELD_COMMENT("If non-null, the identifier string of a session to look up. Supports the special names 'self' and 'auto' which resolve to the caller's session."), + SD_VARLINK_DEFINE_INPUT(Id, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If non-null, return the session containing the process with this PID. If both Id and PID are specified they must reference the same session, otherwise NoSuchSession is returned."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(PID, ProcessId, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Configuration of the session"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(context, SessionContext, 0), + SD_VARLINK_FIELD_COMMENT("Runtime information of the session"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, SessionRuntime, 0)); + static SD_VARLINK_DEFINE_METHOD( ReleaseSession, SD_VARLINK_FIELD_COMMENT("The identifier string of the session to release. If unspecified or 'self', will return the callers session."), @@ -102,14 +170,22 @@ SD_VARLINK_DEFINE_INTERFACE( SD_VARLINK_INTERFACE_COMMENT("APIs for managing login sessions."), SD_VARLINK_SYMBOL_COMMENT("Process identifier"), &vl_type_ProcessId, + SD_VARLINK_SYMBOL_COMMENT("Dual timestamp"), + &vl_type_Timestamp, SD_VARLINK_SYMBOL_COMMENT("Various types of sessions"), &vl_type_SessionType, SD_VARLINK_SYMBOL_COMMENT("Various classes of sessions"), &vl_type_SessionClass, + SD_VARLINK_SYMBOL_COMMENT("Configuration aspects of a session (suitable for reuse as creation input)"), + &vl_type_SessionContext, + SD_VARLINK_SYMBOL_COMMENT("Runtime state and dynamic information of a session"), + &vl_type_SessionRuntime, SD_VARLINK_SYMBOL_COMMENT("Allocates a new session."), &vl_method_CreateSession, - SD_VARLINK_SYMBOL_COMMENT("Releases an existing session. Currently, will be refuses unless originating from the session to release itself."), + SD_VARLINK_SYMBOL_COMMENT("Releases an existing session. Currently, will be refused unless originating from the session to release itself."), &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("No session by this name found"), &vl_error_NoSuchSession, SD_VARLINK_SYMBOL_COMMENT("No seat by this name found"), diff --git a/src/shared/varlink-io.systemd.Login.h b/src/shared/varlink-io.systemd.Login.h index 5453cdfb25..52a2ddd974 100644 --- a/src/shared/varlink-io.systemd.Login.h +++ b/src/shared/varlink-io.systemd.Login.h @@ -3,4 +3,7 @@ #include "sd-varlink-idl.h" +extern const sd_varlink_symbol vl_type_SessionType; +extern const sd_varlink_symbol vl_type_SessionClass; + extern const sd_varlink_interface vl_interface_io_systemd_Login; diff --git a/src/test/meson.build b/src/test/meson.build index 0c8dba64d5..8a25cc03dc 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -494,6 +494,10 @@ executables += [ 'sources' : files('test-varlink-idl-machine.c'), 'objects' : ['systemd-machined'], }, + test_template + { + 'sources' : files('test-varlink-idl-login.c'), + 'objects' : ['systemd-logind'], + }, test_template + { 'sources' : files('test-watchdog.c'), 'type' : 'unsafe', diff --git a/src/test/test-varlink-idl-login.c b/src/test/test-varlink-idl-login.c new file mode 100644 index 0000000000..7dff80178f --- /dev/null +++ b/src/test/test-varlink-idl-login.c @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "logind-session.h" +#include "tests.h" +#include "test-varlink-idl-util.h" +#include "varlink-io.systemd.Login.h" + +TEST(login_enums_idl) { + TEST_IDL_ENUM(SessionType, session_type, vl_type_SessionType); + /* SessionClass has SESSION_NONE ("none") as an internal sentinel that is intentionally not exposed + * via Varlink. Only validate the IDL→C direction. */ + TEST_IDL_ENUM_FROM_STRING(SessionClass, session_class, vl_type_SessionClass); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/test/units/TEST-35-LOGIN.sh b/test/units/TEST-35-LOGIN.sh index 58b3bcca07..74e83c79d4 100755 --- a/test/units/TEST-35-LOGIN.sh +++ b/test/units/TEST-35-LOGIN.sh @@ -797,8 +797,113 @@ EOF systemctl stop "$RUN0UNIT3" } +teardown_varlink() ( + set +ex + + cleanup_session + + return 0 +) + testcase_varlink() { - varlinkctl introspect /run/systemd/io.systemd.Login + local session uid session_out + + if [[ ! -c /dev/tty2 ]]; then + echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." + return + fi + + trap teardown_varlink RETURN + + local VARLINK_SOCKET="/run/systemd/io.systemd.Login" + + : "--- Introspect ---" + varlinkctl introspect "$VARLINK_SOCKET" + varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListSessions" >/dev/null + + : "--- Setup test session ---" + create_session + session=$(loginctl --no-legend | grep -v manager | awk '$3 == "logind-test-user" { print $1 }') + uid=$(id -ru logind-test-user) + local leader_pid + leader_pid=$(loginctl show-session "$session" -p Leader --value) + loginctl activate "$session" + + : "--- ListSessions: Id filter (single reply) ---" + session_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions "{\"Id\":\"$session\"}") + echo "$session_out" | jq -e ".runtime.Id == \"$session\"" >/dev/null + echo "$session_out" | jq -e ".context.UID == $uid" >/dev/null + echo "$session_out" | jq -e ".context.PID.pid == $leader_pid" >/dev/null + echo "$session_out" | jq -e '.context.TTY == "tty2"' >/dev/null + echo "$session_out" | jq -e '.context.Remote == false' >/dev/null + echo "$session_out" | jq -e '.context.Type == "tty"' >/dev/null + echo "$session_out" | jq -e '.context.Class == "user"' >/dev/null + echo "$session_out" | jq -e '.runtime.CanIdle == true' >/dev/null + echo "$session_out" | jq -e '.runtime.CanLock == true' >/dev/null + echo "$session_out" | jq -e '.runtime.State == "active"' >/dev/null + echo "$session_out" | jq -e '.runtime.Active == true' >/dev/null + # ExtraDeviceAccess may be absent (empty) but must be an array if present. + echo "$session_out" | jq -e '(.context | has("ExtraDeviceAccess") | not) or (.context.ExtraDeviceAccess | type == "array")' >/dev/null + + # nonexistent session id + (! varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions '{"Id":"nonexistent"}') + + : "--- ListSessions: PID filter (single reply) ---" + # Look up the session containing the session leader's PID. + local pid_out + pid_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions "{\"PID\":{\"pid\":$leader_pid}}") + echo "$pid_out" | jq -e ".runtime.Id == \"$session\"" >/dev/null + + : "--- ListSessions: Id+PID consistency check ---" + # Same session referenced two ways: must succeed. + local ok_out mismatch_err + ok_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions \ + "{\"Id\":\"$session\",\"PID\":{\"pid\":$leader_pid}}") + echo "$ok_out" | jq -e ".runtime.Id == \"$session\"" >/dev/null + # Mismatched Id and PID (PID 1 is not in $session): must fail with NoSuchSession. + mismatch_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions \ + "{\"Id\":\"$session\",\"PID\":{\"pid\":1}}" 2>&1 || true) + echo "$mismatch_err" | grep NoSuchSession >/dev/null + + : "--- ListSessions: streaming path ---" + # varlinkctl --more emits RFC 7464 JSON-seq (each record prefixed with RS/0x1e), so jq --seq is required. + varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListSessions '{}' \ + | jq --seq -e --arg s "$session" 'select(.runtime.Id == $s)' >/dev/null + test "$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListSessions '{}' | wc -l)" -ge 2 + # without --more and no filter: must fail with ExpectedMore (not assert()) so logind stays running. + local list_err + list_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListSessions '{}' 2>&1 || true) + # varlinkctl rewrites SD_VARLINK_ERROR_EXPECTED_MORE to the friendly description + # below (see src/varlinkctl/varlinkctl.c), so match on that substring rather than + # the raw error id. + echo "$list_err" | grep "'more' flag" >/dev/null + systemctl is-active systemd-logind.service >/dev/null + + : "--- 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 + # PAM so pam_systemd creates a real session for the caller. + local PAMSERVICE_REL="pamserv-rel$RANDOM" + cat >/etc/pam.d/"$PAMSERVICE_REL" <&1 || true) + echo "$no_sess_rel" | grep NoSuchSession >/dev/null } testcase_restart() {