logind: migrate to Varlink (phase1) (#41561)

This implements phase 1 of #41560.

Add List Varlink methods for all logind object types (sessions, users,
seats, inhibitors).
This commit is contained in:
Yu Watanabe
2026-05-26 03:15:55 +09:00
committed by GitHub
24 changed files with 1236 additions and 82 deletions
+212 -1
View File
@@ -797,8 +797,219 @@ EOF
systemctl stop "$RUN0UNIT3"
}
teardown_varlink() (
set +ex
systemctl stop test-varlink-inhibit.service 2>/dev/null
cleanup_session
return 0
)
testcase_varlink() {
varlinkctl introspect /run/systemd/io.systemd.Login
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]}."
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
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
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
: "--- 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
: "--- 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
: "--- 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
# PAM so pam_systemd creates a real session for the caller.
local PAMSERVICE_REL="pamserv-rel$RANDOM"
cat >/etc/pam.d/"$PAMSERVICE_REL" <<EOF
auth sufficient pam_unix.so
auth required pam_deny.so
account sufficient pam_unix.so
account required pam_permit.so
session optional pam_systemd.so debug
session required pam_unix.so
EOF
systemd-run --wait --pipe \
-p PAMName="$PAMSERVICE_REL" \
-p Type=exec \
-p User=logind-test-user \
varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ReleaseSession '{}'
rm -f /etc/pam.d/"$PAMSERVICE_REL"
# With no caller session (running as root outside any session), the helper
# must return NoSuchSession, not PermissionDenied.
local no_sess_rel
no_sess_rel=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ReleaseSession '{}' 2>&1 || true)
echo "$no_sess_rel" | grep NoSuchSession >/dev/null
}
testcase_restart() {