#!/bin/sh
# Не выпускать браузер, пока показывать некуда и нечего.
#
# Два ожидания, и оба нужны:
#
#  1. Подключённый экран. Композитору нужен CRTC; если телевизор ещё не воткнут
#     или не ответил по HDMI, ядро пишет «Cannot find any crtc or sizes», и
#     запуск проваливается. Плату включают раньше телевизора чаще, чем кажется.
#
#  2. Агент. Иначе первое, что нарисует браузер, — ошибка соединения, а не
#     страница, и человек перед телевизором ничего с этим сделать не может.
#
# Выходим с ошибкой, а не ждём вечно: юнит перезапустится и попробует снова.
# Лимит перезапусков у него снят, так что попытки не кончатся.
set -eu

PORT="${SCREEN_PORT:-3000}"
TIMEOUT="${DASHDASH_WAIT_TIMEOUT:-120}"
BOOT_PAGE="${DASHDASH_BOOT_PAGE:-/usr/local/share/dashdash/boot.html}"
BOOT_PID=""

display_connected() {
	for status in /sys/class/drm/card*-*/status; do
		[ -f "$status" ] || continue
		if [ "$(cat "$status" 2>/dev/null)" = "connected" ]; then
			return 0
		fi
	done
	return 1
}

i=0
while [ "$i" -lt "$TIMEOUT" ]; do
	if display_connected; then
		break
	fi
	i=$((i + 1))
	sleep 1
done
if ! display_connected; then
	echo "dashdash: за ${TIMEOUT}с не появилось ни одного подключённого экрана" >&2
	exit 1
fi

# Экран есть, агента ещё нет — и это те полминуты, за которые человек перед
# телевизором успевает решить, что устройство сломано. Показываем заставку тем
# же киоском, только со страницы с диска: сети на этом этапе может не быть.
#
# Запускаем через setsid, отдельной группой процессов. Это не мелочь: гасить
# надо всю группу. Раньше здесь убивали один процесс, его дети переживали
# родителя, держали открытыми дескрипторы /dev/dri — и настоящий киоск не мог
# стать DRM master, получая «Device or resource busy». systemd при этом писал
# «Found left-over process … Ignoring» и спокойно запускал композитор, которому
# экрана уже не досталось. Отказ тихий: юнит «работает», телевизор пуст.
show_boot_screen() {
	[ -f "$BOOT_PAGE" ] || return 0
	command -v cage >/dev/null 2>&1 || return 0
	command -v cog >/dev/null 2>&1 || return 0
	setsid cage -- cog --platform=wl "file://${BOOT_PAGE}" >/dev/null 2>&1 &
	BOOT_PID=$!
}

boot_screen_alive() {
	[ -n "$BOOT_PID" ] || return 1
	kill -0 "$BOOT_PID" 2>/dev/null
}

# Никто больше не держит видеокарту?
#
# `if`, а не `ls | grep -q … && return 1`. При set -e связка `a && b` считается
# неудачной целиком, когда неудачна `b`, — а grep не находит совпадения почти у
# каждого процесса. Прежняя запись выбрасывала скрипт наружу на первом же
# постороннем процессе, то есть в обработчик EXIT, а тот заходил сам в себя.
# Снаружи это выглядело как намертво зависший ExecStartPre.
drm_is_free() {
	for proc in /proc/[0-9]*; do
		if ls -l "$proc/fd" 2>/dev/null | grep -q 'dri/card'; then
			return 1
		fi
	done
	return 0
}

hide_boot_screen() {
	# Номер забираем себе и обнуляем сразу: функция стоит обработчиком EXIT, и
	# повторный заход должен выйти на первой строке, а не начать всё заново.
	pid="$BOOT_PID"
	BOOT_PID=""
	[ -n "$pid" ] || return 0

	# Минус перед номером — вся группа: оболочка, композитор, браузер.
	kill -TERM "-$pid" 2>/dev/null || true

	# Ждать без срока нельзя. Здесь стоял `wait "$pid"`, и на этой строке
	# скрипт вис навсегда: cage от SIGTERM не уходит. ExecStartPre, который не
	# возвращается, снаружи неотличим от зависшего запуска киоска.
	waited=0
	while [ "$waited" -lt 5 ]; do
		kill -0 "$pid" 2>/dev/null || break
		waited=$((waited + 1))
		sleep 1
	done
	kill -KILL "-$pid" 2>/dev/null || true

	# И только теперь — пока ядро не отпустит видеокарту. Оно отпускает мастера
	# не по сигналу, а когда закрыт последний дескриптор, так что «подождать
	# секунду» здесь было бы гаданием.
	waited=0
	while [ "$waited" -lt 10 ]; do
		if drm_is_free; then
			break
		fi
		waited=$((waited + 1))
		sleep 1
	done
	return 0
}

trap hide_boot_screen EXIT INT TERM

show_boot_screen

i=0
while [ "$i" -lt "$TIMEOUT" ]; do
	if curl -fsS -o /dev/null "http://127.0.0.1:${PORT}/health"; then
		hide_boot_screen
		exit 0
	fi

	# При холодном старте заставка часто не переживает первую попытку: ядро
	# только что подняло DRM, разъём ещё «дребезжит», и процесс умирает молча.
	# Пробуем снова, не чаще раза в пять секунд, чтобы не устроить круг
	# перезапусков.
	if ! boot_screen_alive && [ $((i % 5)) -eq 0 ]; then
		BOOT_PID=""
		show_boot_screen
	fi

	i=$((i + 1))
	sleep 1
done

echo "dashdash: агент не ответил на :${PORT} за ${TIMEOUT}с" >&2
exit 1
