From: Yaping Li <202858510+YapingLi04@users.noreply.github.com> Date: Sun, 10 May 2026 14:50:13 +0000 (+0000) Subject: logind: add ListInhibitors Varlink method X-Git-Tag: v261-rc2~25^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d2bdaab55e8c875bae8efc6ca6328b968521ce7a;p=thirdparty%2Fsystemd.git logind: add ListInhibitors Varlink method The Varlink ListInhibitors method is the counterpart of D-Bus ListInhibitors. Like its D-Bus counterpart it is zero-filter and streams the full list of currently registered inhibitors using the SD_VARLINK_METHOD_MORE pattern, returning InhibitorInfo objects with Id, What, Who, Why, Mode, UID, PID, and Since fields. There is no D-Bus GetInhibitor getter to fold in, so no unique-key filter is introduced here. --- diff --git a/src/login/logind-inhibit.c b/src/login/logind-inhibit.c index 30b43404b19..99ec0a3c99e 100644 --- a/src/login/logind-inhibit.c +++ b/src/login/logind-inhibit.c @@ -17,6 +17,7 @@ #include "fs-util.h" #include "hashmap.h" #include "io-util.h" +#include "json-util.h" #include "log.h" #include "logind-session.h" #include "logind.h" @@ -528,6 +529,67 @@ int inhibit_what_from_string(const char *s) { } } +static int inhibit_what_build_json(sd_json_variant **ret, const char *name, void *userdata) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + InhibitWhat *what = ASSERT_PTR(userdata); + int r; + + assert(ret); + assert(inhibit_what_is_valid(*what)); + + for (unsigned bit = 0; (1U << bit) < (unsigned) _INHIBIT_WHAT_MAX; bit++) { + InhibitWhat w = 1U << bit; + + if (!FLAGS_SET(*what, w)) + continue; + + r = sd_json_variant_append_arrayb(&v, JSON_BUILD_STRING_UNDERSCORIFY(inhibit_what_to_string(w))); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(v); + return 0; +} + +static int inhibitor_context_build_json(sd_json_variant **ret, const char *name, void *userdata) { + Inhibitor *i = ASSERT_PTR(userdata); + + assert(ret); + assert(i->mode >= 0 && i->mode < _INHIBIT_MODE_MAX); + assert(inhibit_what_is_valid(i->what)); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_STRING("Id", i->id), + SD_JSON_BUILD_PAIR_CALLBACK("What", inhibit_what_build_json, &i->what), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Who", i->who), + JSON_BUILD_PAIR_STRING_NON_EMPTY("Why", i->why), + JSON_BUILD_PAIR_ENUM("Mode", inhibit_mode_to_string(i->mode)), + SD_JSON_BUILD_PAIR_UNSIGNED("UID", i->uid)); +} + +static int inhibitor_runtime_build_json(sd_json_variant **ret, const char *name, void *userdata) { + Inhibitor *i = ASSERT_PTR(userdata); + + assert(ret); + + return sd_json_buildo( + ret, + JSON_BUILD_PAIR_PIDREF_NON_NULL("PID", &i->pid), + JSON_BUILD_PAIR_DUAL_TIMESTAMP_NON_NULL("Since", &i->since)); +} + +int inhibitor_build_json(Inhibitor *i, sd_json_variant **ret) { + assert(i); + assert(ret); + + return sd_json_buildo( + ret, + SD_JSON_BUILD_PAIR_CALLBACK("context", inhibitor_context_build_json, i), + SD_JSON_BUILD_PAIR_CALLBACK("runtime", inhibitor_runtime_build_json, i)); +} + static const char* const inhibit_mode_table[_INHIBIT_MODE_MAX] = { [INHIBIT_BLOCK] = "block", [INHIBIT_BLOCK_WEAK] = "block-weak", diff --git a/src/login/logind-inhibit.h b/src/login/logind-inhibit.h index 1cb4ed46054..c606582df21 100644 --- a/src/login/logind-inhibit.h +++ b/src/login/logind-inhibit.h @@ -80,6 +80,8 @@ bool manager_is_inhibited( uid_t uid_to_ignore, Inhibitor **ret_offending); +int inhibitor_build_json(Inhibitor *i, sd_json_variant **ret); + static inline bool inhibit_what_is_valid(InhibitWhat w) { return w > 0 && w < _INHIBIT_WHAT_MAX; } diff --git a/src/login/logind-varlink.c b/src/login/logind-varlink.c index 89dcbf87789..382f8fea221 100644 --- a/src/login/logind-varlink.c +++ b/src/login/logind-varlink.c @@ -14,6 +14,7 @@ #include "login-util.h" #include "logind.h" #include "logind-dbus.h" +#include "logind-inhibit.h" #include "logind-seat.h" #include "logind-session.h" #include "logind-shutdown.h" @@ -690,6 +691,40 @@ static int vl_method_list_seats(sd_varlink *link, sd_json_variant *parameters, s return 0; } +static int vl_method_list_inhibitors(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + int r; + + /* IDL uses SD_VARLINK_REQUIRES_MORE, so the framework rejects non-more calls before this handler. */ + + r = sd_varlink_dispatch(link, parameters, /* dispatch_table= */ NULL, /* userdata= */ NULL); + if (r != 0) + return r; + + /* Required for multi-reply streaming: sd_varlink_reply() only stashes the previous reply (for + * the 'continues' flag machinery) when v->sentinel is set. Pass NULL to send an empty reply + * (not an error) when the inhibitor hashmap is empty — this method has no point-lookup, so an + * empty list is a valid result, not a "not found" failure. */ + r = sd_varlink_set_sentinel(link, /* error_id= */ NULL); + if (r < 0) + return r; + + Inhibitor *inhibitor; + HASHMAP_FOREACH(inhibitor, m->inhibitors) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + + r = inhibitor_build_json(inhibitor, &v); + if (r < 0) + return r; + + r = sd_varlink_reply(link, v); + 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; @@ -860,6 +895,7 @@ int manager_varlink_init(Manager *m, int fd) { "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.Login.ListInhibitors", vl_method_list_inhibitors, "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 5771c52c4c6..7d7f5f2a4a8 100644 --- a/src/shared/varlink-io.systemd.Login.c +++ b/src/shared/varlink-io.systemd.Login.c @@ -230,9 +230,60 @@ static SD_VARLINK_DEFINE_METHOD_FULL( SD_VARLINK_FIELD_COMMENT("Runtime information of the seat"), SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, SeatRuntime, 0)); +SD_VARLINK_DEFINE_ENUM_TYPE( + InhibitMode, + SD_VARLINK_FIELD_COMMENT("Hard inhibition; blocks the operation entirely"), + SD_VARLINK_DEFINE_ENUM_VALUE(block), + SD_VARLINK_FIELD_COMMENT("Weak hard inhibition; like block, but the user who set it can override it without polkit authorization"), + SD_VARLINK_DEFINE_ENUM_VALUE(block_weak), + SD_VARLINK_FIELD_COMMENT("Soft inhibition; delays the operation but ultimately permits it"), + SD_VARLINK_DEFINE_ENUM_VALUE(delay)); + +SD_VARLINK_DEFINE_ENUM_TYPE( + InhibitWhat, + SD_VARLINK_DEFINE_ENUM_VALUE(shutdown), + SD_VARLINK_DEFINE_ENUM_VALUE(sleep), + SD_VARLINK_DEFINE_ENUM_VALUE(idle), + SD_VARLINK_DEFINE_ENUM_VALUE(handle_power_key), + SD_VARLINK_DEFINE_ENUM_VALUE(handle_suspend_key), + SD_VARLINK_DEFINE_ENUM_VALUE(handle_hibernate_key), + SD_VARLINK_DEFINE_ENUM_VALUE(handle_lid_switch), + SD_VARLINK_DEFINE_ENUM_VALUE(handle_reboot_key)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + InhibitorContext, + SD_VARLINK_FIELD_COMMENT("The inhibitor identifier"), + SD_VARLINK_DEFINE_FIELD(Id, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("What is being inhibited"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(What, InhibitWhat, SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("A human-readable descriptive string of who is taking the inhibition"), + SD_VARLINK_DEFINE_FIELD(Who, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("A human-readable descriptive string of why the inhibition is taken"), + SD_VARLINK_DEFINE_FIELD(Why, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The inhibition mode"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(Mode, InhibitMode, 0), + SD_VARLINK_FIELD_COMMENT("The UID of the user taking the inhibition"), + SD_VARLINK_DEFINE_FIELD(UID, SD_VARLINK_INT, 0)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + InhibitorRuntime, + SD_VARLINK_FIELD_COMMENT("The PID of the process taking the inhibition"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(PID, ProcessId, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The inhibitor timestamps"), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(Since, Timestamp, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_METHOD_FULL( + ListInhibitors, + SD_VARLINK_REQUIRES_MORE, + SD_VARLINK_FIELD_COMMENT("Configuration of the inhibitor"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(context, InhibitorContext, 0), + SD_VARLINK_FIELD_COMMENT("Runtime information of the inhibitor"), + SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, InhibitorRuntime, 0)); + static SD_VARLINK_DEFINE_ERROR(NoSuchSession); static SD_VARLINK_DEFINE_ERROR(NoSuchUser); static SD_VARLINK_DEFINE_ERROR(NoSuchSeat); +static SD_VARLINK_DEFINE_ERROR(NoSuchInhibitor); static SD_VARLINK_DEFINE_ERROR(AlreadySessionMember); static SD_VARLINK_DEFINE_ERROR(VirtualTerminalAlreadyTaken); static SD_VARLINK_DEFINE_ERROR(TooManySessions); @@ -259,26 +310,38 @@ SD_VARLINK_DEFINE_INTERFACE( &vl_method_CreateSession, 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)."), + 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."), + 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)."), &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."), + SD_VARLINK_SYMBOL_COMMENT("Lists current seats. If an Id filter is provided, returns the single matching seat; otherwise streams all current seats (requires the 'more' flag)."), &vl_method_ListSeats, + SD_VARLINK_SYMBOL_COMMENT("Inhibition mode"), + &vl_type_InhibitMode, + SD_VARLINK_SYMBOL_COMMENT("Operations that can be inhibited"), + &vl_type_InhibitWhat, + SD_VARLINK_SYMBOL_COMMENT("Configuration aspects of an inhibitor"), + &vl_type_InhibitorContext, + SD_VARLINK_SYMBOL_COMMENT("Runtime state and dynamic information of an inhibitor"), + &vl_type_InhibitorRuntime, + SD_VARLINK_SYMBOL_COMMENT("Lists all current inhibitors."), + &vl_method_ListInhibitors, 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("No inhibitor found"), + &vl_error_NoSuchInhibitor, 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"), diff --git a/src/shared/varlink-io.systemd.Login.h b/src/shared/varlink-io.systemd.Login.h index 52a2ddd9749..2d751eebdf9 100644 --- a/src/shared/varlink-io.systemd.Login.h +++ b/src/shared/varlink-io.systemd.Login.h @@ -5,5 +5,7 @@ extern const sd_varlink_symbol vl_type_SessionType; extern const sd_varlink_symbol vl_type_SessionClass; +extern const sd_varlink_symbol vl_type_InhibitMode; +extern const sd_varlink_symbol vl_type_InhibitWhat; extern const sd_varlink_interface vl_interface_io_systemd_Login; diff --git a/src/test/test-varlink-idl-login.c b/src/test/test-varlink-idl-login.c index 7dff80178f3..fb8ef813637 100644 --- a/src/test/test-varlink-idl-login.c +++ b/src/test/test-varlink-idl-login.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "logind-inhibit.h" #include "logind-session.h" #include "tests.h" #include "test-varlink-idl-util.h" @@ -10,6 +11,11 @@ TEST(login_enums_idl) { /* 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); + TEST_IDL_ENUM(InhibitMode, inhibit_mode, vl_type_InhibitMode); + /* InhibitWhat is a bit-flag enum: inhibit_what_to_string() accepts a bitmask and + * the index does not correspond to a sequential integer, so the TEST_IDL_ENUM + * round-trip macro doesn't apply. The flag→string mapping is exercised through + * ListInhibitors integration tests in TEST-35-LOGIN.sh. */ } DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/test/units/TEST-35-LOGIN.sh b/test/units/TEST-35-LOGIN.sh index 491b754b295..e4f9621c8d9 100755 --- a/test/units/TEST-35-LOGIN.sh +++ b/test/units/TEST-35-LOGIN.sh @@ -800,13 +800,14 @@ EOF teardown_varlink() ( set +ex + systemctl stop test-varlink-inhibit.service 2>/dev/null cleanup_session return 0 ) testcase_varlink() { - local session uid session_out user_out seat_out self_err + local session uid session_out user_out seat_out self_err inhibitor_out if [[ ! -c /dev/tty2 ]]; then echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}." @@ -822,6 +823,7 @@ testcase_varlink() { 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 + varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListInhibitors" >/dev/null : "--- Setup test session ---" create_session @@ -956,6 +958,33 @@ testcase_varlink() { 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 + : "--- ListInhibitors ---" + systemd-run --unit=test-varlink-inhibit.service --service-type=exec \ + systemd-inhibit --what=shutdown --who="varlink-test" --why="testing varlink" --mode=block \ + sleep infinity + timeout 10 bash -c "until varlinkctl call --more '$VARLINK_SOCKET' io.systemd.Login.ListInhibitors '{}' 2>/dev/null | grep varlink-test >/dev/null; do sleep 0.5; done" + + inhibitor_out=$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListInhibitors '{}') + # What is now an array of strings; pick our test record and validate fields with jq. + echo "$inhibitor_out" | jq --seq -e 'select(.context.Who == "varlink-test") | .context.What | type == "array" and contains(["shutdown"])' >/dev/null + echo "$inhibitor_out" | jq --seq -e 'select(.context.Who == "varlink-test") | .context.Mode == "block"' >/dev/null + echo "$inhibitor_out" | jq --seq -e 'select(.context.Who == "varlink-test") | .context.Why == "testing varlink"' >/dev/null + + # without --more should fail (ListInhibitors has no filter, --more required) + (! varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListInhibitors '{}') + + systemctl stop test-varlink-inhibit.service + + # Best-effort empty-list check: stopping the test inhibitor doesn't guarantee + # zero entries (other system inhibitors may be registered), but the contract + # we verify holds regardless — ListInhibitors has no single-lookup path, so + # it must never emit a NoSuchInhibitor sentinel. An empty result is just an + # empty parameters reply as the stream terminator; the IDL-validation skip + # in the sentinel handler ensures this passes validation. + local empty_inhibitor_err + empty_inhibitor_err=$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListInhibitors '{}' 2>&1 || true) + (! echo "$empty_inhibitor_err" | grep NoSuchInhibitor >/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