#!/bin/sh -e
# Copyright (c) 2026 Anton Farygin <rider@altlinux.org>
# SPDX-License-Identifier: GPL-2.0-or-later

# Bring up hasher-privd inside a container. Root-only; idempotent; called by the
# hsh wrappers on demand, so nothing runs until someone actually builds.
#
# Every step here is destructive or system-wide, and the only reason any of it
# is acceptable is that a container has no init to do it properly. So the script
# checks that first and refuses everywhere else, and it does nothing at all
# until it knows it is the one that has to act: the checks come first, the
# changes after.

libexec=/usr/lib/hasher-kayfabe
socket_dir=/run/hasher-priv
socket="$socket_dir/daemon"
lock=/run/kayfabe-privd.lock

[ "$(id -u)" = 0 ] || {
	echo "${0##*/}: must run as root" >&2
	exit 1
}

# On a host this script would kill a running daemon, replace it with a preloaded
# one and remount cgroupfs — as an unprivileged caller's errand, since the setuid
# helper can reach it. There is nothing here worth that: a host has an init that
# starts hasher-privd properly.
if ! "$libexec/kayfabe-in-container"; then
	echo "${0##*/}: this is not a container — hasher-privd belongs to the init" >&2
	echo "${0##*/}: here (systemctl start hasher-privd); refusing to interfere" >&2
	exit 1
fi

# /usr/lib64 on 64-bit ALT, /usr/lib on i586 — look in both rather than bake in
# one at build time.
for d in /usr/lib64 /usr/lib; do
	[ -e "$d/hasher-kayfabe/libkayfabe.so" ] || continue
	shim="$d/hasher-kayfabe/libkayfabe.so"
	break
done

[ -n "$shim" ] || {
	echo "${0##*/}: libkayfabe.so found in neither /usr/lib64 nor /usr/lib" >&2
	exit 1
}

# Everything below changes shared state, and several callers arrive at once as
# soon as a build runs anything in parallel. Without this, the second one
# unlinks the socket the first has just bound and kills the daemon behind it.
if command -v flock >/dev/null; then
	exec 9>"$lock"
	flock 9
fi

# Serving means both halves: a socket file clients can reach, and something
# actually bound to that path. Either alone lies — a file outlives the daemon
# that made it, and the kernel keeps the path in /proc/net/unix even after the
# file is unlinked, so a daemon nobody can reach still looks alive there.
if [ -S "$socket" ] && grep -q " $socket\$" /proc/net/unix 2>/dev/null; then
	exit 0
fi

# Not serving, so whatever is left of a previous daemon has to go: it holds the
# pidfile lock, and a new hasher-privd would refuse to start behind it. The pid
# comes from that same pidfile rather than from a process name, which a zombie
# answers too.
pidfile=$(sed -n 's/^pidfile=//p' /etc/hasher-priv/daemon.conf 2>/dev/null)
pid=$(cat "${pidfile:-/var/run/hasher-privd.pid}" 2>/dev/null) || pid=
case "$pid" in
[0-9]*) kill "$pid" 2>/dev/null || : ;;
esac

# Podman mounts cgroupfs read-only and locks the mount, so a remount is refused
# — but mounting a fresh cgroup2 instance over it is allowed and lands on the
# container's own delegated subtree. Without this, hasher-priv's join_cgroup()
# cannot write cgroup.procs; the library covers that case too, at the price of
# jobs running outside their own cgroup.
#
# Only where cgroup2 is what is already mounted: on a cgroup-v1 or hybrid system
# /sys/fs/cgroup is a tmpfs with no cgroup.procs of its own, and mounting over
# it would hide the live hierarchy — systemd's included — with no way back.
if [ ! -w /sys/fs/cgroup/cgroup.procs ] &&
	grep -q ' /sys/fs/cgroup cgroup2 ' /proc/self/mounts; then
	# Report why, not just that: a minimal container often has no mount(8) at
	# all, and "no writable cgroup2" would send the reader hunting for a
	# permission problem that is not there.
	if ! command -v mount >/dev/null; then
		echo "${0##*/}: mount(8) is missing — install the mount package for a" >&2
		echo "${0##*/}: writable cgroup2; jobs run without their own cgroup meanwhile" >&2
	elif ! mount -t cgroup2 none /sys/fs/cgroup; then
		echo "${0##*/}: could not mount cgroup2 (see above) — jobs will run" >&2
		echo "${0##*/}: without their own cgroup" >&2
	fi
	# A mount that landed somewhere still not writable is worth saying out
	# loud: the library then fakes the cgroup write instead, which is the
	# weaker of the two arrangements and otherwise happens invisibly.
	[ -w /sys/fs/cgroup/cgroup.procs ] ||
		echo "${0##*/}: cgroupfs is still read-only; jobs run without their own cgroup" >&2
fi

# A socket left behind by a dead daemon answers connect() with ECONNREFUSED
# instead of ENOENT, so clients fail with a confusing error rather than falling
# back to starting us. Nothing is listening on it, so it is safe to drop.
rm -f "$socket"

# hasher-priv ships this directory as 0710 root:hashman and its socket lives
# there; in a container /run is a fresh tmpfs, so we are the ones creating it and
# the mode has to be spelled out rather than left to a umask.
mkdir -p -m 0710 "$socket_dir"
chgrp hashman "$socket_dir" 2>/dev/null || :
chmod 0710 "$socket_dir"

# -D daemonizes. The library is loaded here and stays mapped in the per-job
# session servers, which are forks of this process.
LD_PRELOAD="$shim" hasher-privd -D

# Daemonizing returns before the socket exists, and a client that connects too
# early gets ECONNREFUSED and retries for tens of seconds. Wait for it — the
# wrapper execs the real program the moment we return.
i=0
while [ ! -S "$socket" ]; do
	i=$((i + 1))
	[ "$i" -lt 50 ] || {
		echo "${0##*/}: hasher-privd did not create $socket" >&2
		exit 1
	}
	sleep 0.1
done
