Files
snapd/tests/lib/tools/tests.session
Sergio Cazzolato 10588ad16f Normalize test tools
This is the first part of the normalization done for test tools.

The idea is to keep as similar as possible all the test tools regarding
imlpementation and usage.
2021-01-13 10:38:55 -03:00

392 lines
13 KiB
Bash
Executable File

#!/bin/bash -e
show_help() {
echo "usage: tests.session exec [-u USER] [-p PID_FILE] [--] <CMD>"
echo " tests.session prepare | restore [-u USER | -u USER1,USER2,...]"
echo " tests.session kill-leaked"
echo " tests.session dump"
echo " tests.session has-system-systemd-and-dbus"
echo " tests.session has-session-systemd-and-dbus"
}
main() {
if [ $# -eq 0 ]; then
show_help
exit 1
fi
user=root
pid_file=
action=
while [ $# -gt 0 ]; do
case "$1" in
-h)
shift
show_help
exit 0
;;
-p)
if [ $# -eq 1 ]; then
echo "tests.session: option -p requires an argument" >&2
exit 1
fi
pid_file="$2"
shift 2
;;
--)
shift
break
;;
kill-leaked)
action=kill-leaked
shift
;;
prepare)
action=prepare
shift
;;
restore)
action=restore
shift
;;
dump)
action=dump
shift
;;
has-system-systemd-and-dbus)
action=has-system-systemd-and-dbus
shift
;;
has-session-systemd-and-dbus)
action=has-session-systemd-and-dbus
shift
;;
exec)
action="exec"
shift
# remaining arguments are the command to execute
break
;;
-u)
if [ $# -eq 1 ]; then
echo "tests.session: option -u requires an argument" >&2
exit 1
fi
user="$2"
shift 2
;;
-*)
echo "tests.session: unsupported argument $1" >&2
exit 1
;;
*)
echo "tests.session: unsupported action $1" >&2
exit 1
;;
esac
done
if [ "$(id -u)" -ne 0 ]; then
echo "tests.session needs to be invoked as root" >&2
exit 1
fi
case "$action" in
kill-leaked)
# Work around a bug in older versions of logind that leak closing session
# without a process inhabiting it anymore. In the case we've observed there's a
# session that is being closed with a missing process.
for session_id in $(loginctl --no-legend | awk '{ print $1 }'); do
# It would be easier to use "loginctl show-session --value" but we cannot rely on it.
if [ "$(loginctl show-session "$session_id" --property=State | cut -d = -f 2-)" = closing ]; then
leader=$(loginctl show-session "$session_id" --property=Leader | cut -d = -f 2-)
if [ ! -e "/proc/$leader" ]; then
loginctl kill-session "$session_id"
fi
fi
done
exit 0
;;
prepare)
# Try to enable linger for the selected user(s).
for u in $(echo "$user" | tr ',' ' '); do
if ! loginctl enable-linger "$u"; then
echo "tests.session requires external fix for //github.com/systemd/systemd/issues/12401" >&2
exit 1
fi
done
for u in $(echo "$user" | tr ',' ' '); do
# We've enabled linger, now let's explicitly start the
# default.target. This ensures that test code does not need to pay
# extra attention to synchronization.
#
# Because some systems do not support systemd --user (see
# tests/main/tests.session-support for details), check if this is
# expected to work ahead of trying.
if tests.session -u "$u" exec systemctl --user is-enabled default.target >/dev/null; then
tests.session -u "$u" exec systemctl --user start default.target
fi
done
exit 0
;;
restore)
# Disable linger for the selected user(s).
for u in $(echo "$user" | tr ',' ' '); do
loginctl disable-linger "$u"
# If the user is not root, also stop their user slice and ensure
# their XDG_RUNTIME_DIR goes away. Currently doing this for root is
# impossible, because spread logs into a test system over ssh and
# gets a root user slice, at minimum.
#
# If spread ever changes, so that the tests are executed as system
# services, then there this condition could be removed.
if [ "$u" != root ]; then
uid="$(id -u "$u")"
# Unmount gvfs-fuse and the document portal explicitly. FUSE
# file-systems can stay mounted in a broken state even if their
# corresponding process goes away. This can happen if we stop
# the slice and the cleanup is not perfectly graceful.
umount "/run/user/$uid/gvfs" || true
umount "/run/user/$uid/doc" || true
# See user@.service(5) for discussion about user-UID.slice and
# user@UID.service and why both are used. Here we stop the
# slice as that encompasses both user@UID.service and any
# existing sessions of that user.
systemctl stop "user-$uid.slice"
# On Ubuntu 16.04 and Debian 9 stopping the slice above is
# insufficient to stop the service responsible for
# /run/user/UID *immediately*. This seems to be related to
# *absence* of user-session-dir@.service (template service)
# which is responsible for this operation later on (and where
# it works more reliably).
#
# Give the system some time to clean things up.
retry -n 3 --wait 3 test ! -e "/run/user/$uid"
if [ -e "/run/user/$uid" ]; then
echo "tests.session: /run/user/$uid still exists" >&2
exit 1
fi
fi
done
if [ -e /run/tests.session-core16.workaround ]; then
rm /run/tests.session-core16.workaround
# Undo changes to make /var/lib/systemd/ writable.
umount -l /var/lib/systemd
fi
exit 0
;;
dump)
echo "Active sessions:"
for session_id in $(loginctl list-sessions --no-legend | awk '{ print($1) }'); do
echo "Details of session $session_id"
loginctl show-session "$session_id"
done
exit 0
;;
has-system-systemd-and-dbus)
# Ubuntu 14.04 with deputy systemd does not connect to DBus
# and systemd-shim is really responding to "systemd" requests.
test -n "$(command -v busctl)" || ( echo "no busctl"; exit 1 )
# /lib /usr/lib (if present) dbus-broker.service variant
test -f /lib/systemd/system/default.target || test -f /usr/lib/systemd/system/default.target || ( echo "no system default.target"; exit 1 )
test -f /lib/systemd/system/dbus.socket || test -f /usr/lib/systemd/system/dbus.socket || ( echo "no system dbus.socket"; exit 1 )
test -f /lib/systemd/system/dbus.service || test -f /usr/lib/systemd/system/dbus.service || test -f /lib/systemd/system/dbus-broker.service || ( echo "no system dbus{,-broker}.service"; exit 1 )
echo "ok"
exit 0
;;
has-session-systemd-and-dbus)
# CentOS 7 and derivatives disabled systemd --user
# https://bugs.centos.org/view.php?id=8767
test -n "$(command -v busctl)" || ( echo "no busctl"; exit 1 )
# /lib /usr/lib (if present) dbus-broker.service variant
test -f /lib/systemd/user/default.target || test -f /usr/lib/systemd/user/default.target || ( echo "no user default.target"; exit 1)
test -f /lib/systemd/user/dbus.socket || test -f /usr/lib/systemd/user/dbus.socket || ( echo "no user dbus.socket"; exit 1 )
test -f /lib/systemd/user/dbus.service || test -f /usr/lib/systemd/user/dbus.service || test -f /lib/systemd/user/dbus-broker.service || ( echo "no system dbus{,-broker}.service"; exit 1 )
echo "ok"
exit 0
;;
esac
if [ -z "$(command -v busctl)" ]; then
echo "tests.session requires busctl" >&2
exit 1
fi
# This fixes a bug in some older Debian systems where /root/.profile contains
# unconditional invocation of mesg, which on non-tty shells prints "mesg
# ttyname failed inappropriate ioctl for device" which pollutes output from
# invoked programs.
# TODO: move this to spread wide project setup.
test -f /root/.profile && sed -i -e 's/mesg n .*true/tty -s \&\& mesg n/g' /root/.profile
read -r uuid < /proc/sys/kernel/random/uuid
unit_name="tests.session-$uuid.service"
tmp_dir="/tmp/tests.session-$uuid"
mkdir "$tmp_dir"
mkfifo -m 0666 "$tmp_dir/dbus-monitor.pipe" "$tmp_dir/ready.pipe" "$tmp_dir/result.pipe" "$tmp_dir/stdin.pipe" "$tmp_dir/stdout.pipe" "$tmp_dir/stderr.pipe"
trap 'rm -rf $tmp_dir' EXIT
# Run dbus-monitor waiting for systemd JobRemoved signal. Process the output
# with awk, forwarding the result of the service to a fifo.
monitor_expr="type='signal', sender='org.freedesktop.systemd1', interface='org.freedesktop.systemd1.Manager', path='/org/freedesktop/systemd1', member='JobRemoved'"
stdbuf -oL dbus-monitor --system --monitor "$monitor_expr" > "$tmp_dir/dbus-monitor.pipe" 2>"$tmp_dir/dbus-monitor.stderr" &
dbus_monitor_pid=$!
awk_expr="
BEGIN {
found=0;
ready=0;
}
# Once we get the NameAcquired message we are sure dbus-monitor is connected
# and will receive JobRemoved once it is sent. The reason we are getting this
# message despite the filter we establish above is that it is sent directly to
# us, which bypasses DBus filter expressions.
/member=NameAcquired/ {
if (!ready) {
ready=1;
print ready > \"$tmp_dir/ready.pipe\";
close(\"$tmp_dir/ready.pipe\");
}
}
# This part matches an argument to JobRemoved that contains the name of the
# tests.session-xxx.service name we picked earlier. Once we see this we are sure
# the job is gone.
/ string \"$unit_name\"/ {
found=1;
print \"found service file\";
fflush();
next;
}
# This matches any string argument but only takes effect once we found our
# JobRemoved message. The order of arguments to JobRemoved is such that the
# immediate successor of the service name is the result word. This is
# translated to a pass / fail exit code from tests.session. Scanning this part
# terminates awk.
/string \".*\"/ {
if (found==1) {
print \"found result\";
print \$2 > \"$tmp_dir/result.pipe\";
fflush(\"\");
close(\"$tmp_dir/result.pipe\");
exit;
}
}
"
awk -W interactive "$awk_expr" <"$tmp_dir/dbus-monitor.pipe" >"$tmp_dir/awk.log" 2>&1 &
awk_pid=$!
# Wait for dbus-monitor to start.
cat "$tmp_dir/ready.pipe" >/dev/null
# Use busctl to spawn a command. The command is wrapped in shell, runuser -l
# and redirects to capture output. Sadly busctl doesn't support passing file
# descriptors https://github.com/systemd/systemd/issues/14954 As a workaround
# we pass a set of pipes. This is good for non-interactive work.
# NOTE: /dev/stdin is provided explicitly to trigger special behavior in bash,
# as otherwise background tasks are started with /dev/null for stdin.
cat </dev/stdin >"$tmp_dir/stdin.pipe" &
cat_stdin_pid=$!
cat <"$tmp_dir/stdout.pipe" >&1 &
cat_stdout_pid=$!
cat <"$tmp_dir/stderr.pipe" >&2 &
cat_stderr_pid=$!
(
echo "#!/bin/sh"
test -n "$pid_file" && echo "echo \$$ >\"$pid_file\""
printf "exec "
for arg in "$@"; do
printf '%q ' "$arg"
done
echo
)>"$tmp_dir/exec"
chmod +x "$tmp_dir/exec"
# We want to provide a SELinuxContext but systemd in older SELinux-using distributions
# does not recognize this attribute. See https://github.com/systemd/systemd/blob/master/NEWS#L6402
# The typical user context as reported by id -Z is: unconfined_u:unconfined_r:unconfined_t:s0
selinux_context_arg=
case $SPREAD_SYSTEM in
fedora-*|centos-*)
if [ "$(systemctl --version | head -n 1 | awk '{ print $2 }')" -gt 219 ]; then
selinux_context_arg="SELinuxContext s unconfined_u:unconfined_r:unconfined_t:s0"
fi
;;
esac
# NOTE: This shellcheck directive is for the $selinux_context_arg expansion below.
# shellcheck disable=SC2086
busctl \
--allow-interactive-authorization=no --quiet \
--system \
-- \
call \
org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager \
StartTransientUnit "ssa(sv)a(sa(sv))" \
"$unit_name" fail $(( 4 + $(test -n "$selinux_context_arg" && echo 1 || echo 0))) \
Description s "tests.session running $* as $user" \
Type s oneshot \
Environment as 1 TERM=xterm-256color \
$selinux_context_arg \
ExecStart "a(sasb)" 1 \
"$(command -v runuser)" 6 "$(command -v runuser)" -l "$user" - -c "SNAPD_DEBUG=$SNAPD_DEBUG SNAP_CONFINE_DEBUG=$SNAP_CONFINE_DEBUG exec $tmp_dir/exec <$tmp_dir/stdin.pipe >$tmp_dir/stdout.pipe 2>$tmp_dir/stderr.pipe" false \
0
# This is done so that we can configure file redirects. Once Ubuntu 16.04 is no
# longer supported we can use Standard{Input,Output,Error}=file:path property
# of systemd, or perhaps even StandardInputFileDescriptor and pass a file
# descriptor to the FIFOs we open in the script above.
# Wait for the service to terminate. The trigger is a successful read
# from the result FIFO. In case we are signaled in one of the familiar
# ways, kill the started service with the same signal. The reading occurs
# in a loop as the signal will first interrupt the read process, which will
# fail and return nothing.
trap 'systemctl kill --signal=INT $unit_name' INT
trap 'systemctl kill --signal=TERM $unit_name' TERM
trap 'systemctl kill --signal=QUIT $unit_name' QUIT
result=""
for sig in INT TERM QUIT; do
result=$(cat "$tmp_dir/result.pipe" 2>/dev/null) && break
trap - "$sig"
done
trap - INT TERM QUIT
# Kill dbus-monitor that otherwise runs until it notices the pipe is no longer
# connected, which happens after a longer while. Redirect stderr to /dev/null
# to avoid upsetting tests which are sensitive to stderr, e.g. tests/main/document-portal-activation
kill $dbus_monitor_pid 2>/dev/null || true
wait $dbus_monitor_pid 2>/dev/null || true
wait $awk_pid
wait "$cat_stdin_pid"
wait "$cat_stdout_pid"
wait "$cat_stderr_pid"
# Prevent accumulation of failed sessions.
if [ "$result" != '"done"' ]; then
systemctl reset-failed "$unit_name"
fi
case "$result" in
'"done"') exit 0; ;;
'"failed"') exit 1; ;;
'"canceled"') exit 1; ;;
esac
}
main "$@"