#!/bin/bash -e
if [ $# -eq 0 ]; then
	echo "usage: session-tool [-u USER] [-p PID_FILE] [--] <CMD>"
	echo "usage: session-tool --prepare | --restore [-u USER]"
	echo "usage: session-tool --kill-leaked"
	echo "usage: session-tool --dump"
	exit 1
fi
if [ "$(id -u)" -ne 0 ]; then
    echo "session-tool needs to be invoked as root" >&2
    exit 1
fi
if [ -z "$(command -v busctl)" ]; then
    echo "session-tool requires busctl" >&2
    exit 1
fi
user=root
pid_file=
action=
while [ $# -gt 0 ]; do
	case "$1" in
		--)
			shift
			break
			;;
		-p)
			if [ $# -eq 1 ]; then
				echo "session-tool: option -p requires an argument" >&2
				exit 1
			fi
			pid_file="$2"
			shift 2
			;;
		--kill-leaked)
			action=kill-leaked
			shift
			;;
		--prepare)
			action=prepare
			shift
			;;
		--restore)
			action=restore
			shift
			;;
		--dump)
			action=dump
			shift
			;;
		-u)
			if [ $# -eq 1 ]; then
				echo "session-tool: option -u requires an argument" >&2
				exit 1
			fi
			user="$2"
			shift 2
			;;
		-*)
			echo "session-tool: unsupported argument $1" >&2
			exit 1
			;;
		*)
			break
			;;
	esac
done

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)
		# Make /var/lib/systemd writable so that we can get linger enabled.
		# This only applies to Ubuntu Core 16 where individual directories were
		# writable. In Core 18 and beyod all of /var/lib/systemd is writable.
		case "$SPREAD_SYSTEM" in
			ubuntu-core-16-*)
				mount tmpfs -t tmpfs /var/lib/systemd/
				mkdir /var/lib/systemd/{catalog,coredump,deb-systemd-helper-enabled,rfkill}
				touch /var/lib/systemd/random-seed
				mount --bind /writable/system-data/var/lib/systemd/rfkill /var/lib/systemd/rfkill
				mount --bind /writable/system-data/var/lib/systemd/random-seed /var/lib/systemd/random-seed

				touch /run/session-tool-core16.workaround
				;;
		esac

		# Work around https://github.com/systemd/systemd/issues/12401 fixed
		# in systemd v243.
		if systemctl cat systemd-logind.service | not grep -q StateDirectory; then
			mkdir -p /etc/systemd/system/systemd-logind.service.d
			(
				echo "[Service]"
				echo "StateDirectory=systemd/linger"
			) > /etc/systemd/system/systemd-logind.service.d/linger.conf
			mkdir -p /var/lib/systemd/linger
			test "$(command -v restorecon)" != "" && restorecon /var/lib/systemd/linger

			systemctl daemon-reload
			systemctl restart systemd-logind.service

			touch /run/session-tool-systemd-issue-12401.workaround
		fi

		# Enable linger for the selected user.
		loginctl enable-linger "$user"

		exit 0
		;;
	restore)
		# Disable linger for the selected user.
		loginctl disable-linger "$user"

		if [ -e /run/session-tool-core16.workaround ]; then
			rm  /run/session-tool-core16.workaround
			# Undo changes to make /var/lib/systemd/ writable.
			umount -l /var/lib/systemd
		fi

		if [ -e /run/session-tool-systemd-issue-12401.workaround ]; then
			rm  /run/session-tool-systemd-issue-12401.workaround

			# Remove the logind customizations we (may have) done above.
			rm -rf /etc/systemd/system/systemd-logind.service.d

			# Reload systemd and restart logind.
			systemctl daemon-reload
			systemctl restart systemd-logind.service
		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
		;;
esac

# 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="session-tool-$uuid.service"
tmp_dir="/tmp/session-tool-$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" &
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
# session-tool-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 session-tool. 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.
cat >"$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 "session-tool 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 "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.
kill $dbus_monitor_pid || 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
