#!/usr/bin/bash
# What busybox init starts on tty1 when this medium has something graphical on
# it, and what it says when it does not.
#
#   graphical-session                 start it, or hold tty1
#   graphical-session --would-start   say nothing; exit 0 if it would start
#                                     something. console-session asks this
#                                     before taking tty1 for a shell.
#
# There is no systemd here and no display-manager.service, so a greeter is
# started the way a shell is: an inittab entry running a script that decides.
# This is console-session's sibling and follows its rule -- EVERY branch
# announces itself, so a boot log can be asserted on which one was taken, and
# "this never ran" cannot be read as "this ran and did nothing".
#
# Order of preference, and it is a preference rather than a fallback chain:
#
#   gdm      the real answer. It runs its own PAM stack, creates its own
#            session through logind, and starts the shell.
#   weston   NOT a desktop, and not a substitute for one. It is the smallest
#            thing in this tree that opens a DRM device, brings up EGL and
#            enumerates input through libinput -- which is the entire boot path
#            a GNOME session needs underneath it. So it is what proves that
#            path works before gdm exists. It is built here with the kiosk
#            shell and NO clients, so what appears on the screen is a blank
#            compositor: the thing being demonstrated is a live Wayland
#            display, and this script waits for the socket and says so.
#
# duct.live.session= on the kernel command line overrides the choice: `gdm`,
# `weston`, or `none` to leave tty1 alone.

PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
export PATH

query=
[ "${1:-}" = "--would-start" ] && query=1

# EVERYTHING THIS SCRIPT SAYS GOES TO /dev/console, NOT TO ITS OWN TERMINAL.
#
# This entry runs on tty1, so stdout is tty1 -- and on a machine with a serial
# console that is a screen nobody is watching, while on a machine without one a
# compositor takes the VT into graphics mode and text written to it disappears
# entirely. Either way the session's own account of itself lands where it
# cannot be read, which is how the first weston boot test failed: udev, the
# bus, seatd and a client taking seat0 were all visible on the serial console,
# and not one line from this script or from the compositor was, so a run that
# may well have worked was indistinguishable from one that never started.
#
# A marker that the observer cannot see is not a marker. /dev/console is
# whatever the kernel command line says it is, which is exactly the place a
# boot is watched from.
# Tested by OPENING it, not by asking -w. access(2) answers a question about
# permission bits and /dev/console is not an ordinary file: measured in a
# container, `[ -w /dev/console ]` is FALSE while a write to it succeeds -- so
# the obvious test silences the session in exactly the environments where it
# would have worked. Appending nothing is the same operation the writes below
# perform, which is the only test that cannot disagree with them.
console=/dev/console
if ! : >>"$console" 2>/dev/null; then
	console=/dev/null
fi

say() {
	[ -n "$query" ] && return 0
	echo "duct-live: $*" >>"$console"
}

# The kernel command line, read the way the initramfs reads it.
want=
for arg in $(cat /proc/cmdline 2>/dev/null); do
	case $arg in
		duct.live.session=*) want=${arg#duct.live.session=} ;;
	esac
done

# What is installed. gdm has been in /usr/sbin on every distribution that ships
# it, but this tree has moved a binary between bin and sbin before, so both are
# looked at rather than assumed.
gdm=
weston=
for c in /usr/sbin/gdm /usr/bin/gdm; do
	if [ -x "$c" ]; then gdm=$c; break; fi
done
for c in /usr/bin/weston /usr/local/bin/weston; do
	if [ -x "$c" ]; then weston=$c; break; fi
done

session=
case $want in
	none)   session= ;;
	gdm)    session=$gdm ;;
	weston) session=$weston ;;
	"")     session=${gdm:-$weston} ;;
	*)
		say "unknown duct.live.session=$want; choosing by what is installed"
		session=${gdm:-$weston}
		;;
esac

# The query console-session asks. Nothing is printed and nothing is started:
# the answer is the exit status.
if [ -n "$query" ]; then
	[ -n "$session" ] && exit 0
	exit 1
fi

if [ -z "$session" ]; then
	case $want in
		none)
			say "duct.live.session=none -- tty1 left to the console entry" ;;
		gdm|weston)
			say "duct.live.session=$want, but $want is not installed on this medium" ;;
		*)
			say "no display manager and no compositor on this medium; tty1 idle."
			say "a graphical medium is built with: make -C images iso DESKTOP=1" ;;
	esac
	# EXIT rather than starting a shell here. When /dev/console is a virtual
	# terminal it is tty1, and the inittab's console entry is already running a
	# session on it -- a second one would be two shells on one terminal fighting
	# over every keystroke. Exiting is free because the inittab entry is `once`:
	# nothing respawns this, and the line above stays on the screen.
	exit 0
fi

# XDG_RUNTIME_DIR.
#
# gdm does not need this from us: it authenticates through PAM, and
# pam_elogind creates /run/user/<uid> for the session it opens. weston started
# from an inittab entry has no PAM anywhere in its ancestry, so nothing has
# created one -- and a compositor with no XDG_RUNTIME_DIR has nowhere to put
# the socket that is the entire point of starting it.
if [ -n "$weston" ] && [ "$session" = "$weston" ]; then
	uid=$(id -u)
	XDG_RUNTIME_DIR=/run/user/$uid
	export XDG_RUNTIME_DIR
	if [ ! -d "$XDG_RUNTIME_DIR" ]; then
		mkdir -p "$XDG_RUNTIME_DIR"
		chmod 0700 "$XDG_RUNTIME_DIR"
		chown "$uid" "$XDG_RUNTIME_DIR" 2>/dev/null || true
	fi
fi

say "starting $session on tty1"

# Watch for the display coming up, and say so on the console.
#
# THE SOCKET IS THE ASSERTION. A compositor's log says a great many
# encouraging things before it has a usable display, and a boot test that greps
# for one of them is testing prose. A Wayland socket in a runtime directory is
# the artefact: it exists only once the compositor is listening, and it is what
# a client would connect to. Backgrounded, because the compositor has to be in
# the foreground.
(
	for _ in $(seq 1 60); do
		for sock in /run/user/*/wayland-*; do
			if [ -S "$sock" ]; then
				echo "duct-live: wayland display up: $sock" >>"$console"
				exit 0
			fi
		done
		sleep 1
	done
	echo "duct-live: no wayland socket after 60s -- the display did not come up" >>"$console"
) &

# In the foreground, so busybox init sees the session end and restarts it.
case $session in
	*gdm)
		# NO FLAGS, AND --nodaemon IN PARTICULAR IS FATAL.
		#
		# This used to pass --nodaemon, for a real reason: a gdm that forks
		# and returns looks to init like a session that exited immediately,
		# and init would respawn it against a daemon already running. The
		# reason is sound and the flag is not -- gdm 48 does not have it.
		#
		# daemon/main.c:308-314 is the entire option table:
		#
		#     { "fatal-warnings", ... }
		#     { "timed-exit", ... }
		#     { "version", ... }
		#
		# and nothing in daemon/main.c calls fork(), daemon() or setsid(), so
		# gdm 48 never detaches and the behaviour --nodaemon asked for is the
		# only behaviour there is. GOption rejects an unknown option before
		# anything else runs:
		#
		#     Failed to parse options: Unknown option --nodaemon
		#
		# and gdm exits. This inittab entry is `once`, so nothing retries: the
		# whole desktop is one unknown flag away from a tty1 that simply goes
		# quiet, which is what the first gdm boot test did.
		#
		# GREPPING THE TARBALL FOR THE FLAG FINDS IT AND IS WRONG. --nodaemon
		# occurs seven times in gdm 48.0 -- every one of them in a translated
		# documentation string under docs/, some already marked obsolete. The
		# option was removed from the code and its documentation outlived it,
		# so the name is present in the source, present in the manual, and
		# absent from the parser. Check an option against the OPTION TABLE or
		# against the binary, never against the tarball as a whole.
		exec "$session" >>"$console" 2>&1
		;;
	*weston)
		# --shell=kiosk IS NOT OPTIONAL, and the reason is not visible from
		# the command line. weston's default shell is "desktop", and this
		# tree builds weston -Dshell-desktop=false -Dshell-kiosk=true -- so
		# the default names a desktop-shell.so that was never built and weston
		# exits at startup having initialised everything else first. Read from
		# frontend/main.c, not from the manual page.
		#
		# --continue-without-input because the drm backend otherwise refuses
		# to start on a machine with no input device, which is exactly what an
		# emulated boot test is.
		# Output to the console for the same reason as say(): weston takes
		# tty1 into graphics mode, so its log would be written to a screen
		# that is showing a framebuffer. When it fails, this line is the
		# entire diagnosis.
		exec "$session" --shell=kiosk --continue-without-input >>"$console" 2>&1
		;;
esac

# Only reachable if the case above grows a branch that does not exec. Said out
# loud rather than exiting silently, because a tty1 that is simply blank is the
# one outcome nobody can diagnose from the screen.
say "$session was chosen and not started -- no branch above knows how to run it"
exit 1
