#!/bin/sh
# Generator of svctl daemon-reload: resolves the hints of the units in the
# package and the admin store (*.d directories, requires-mounts-for), their
# drop-ins and the entries of fstab and crypttab into the generated store,
# see svctl(8).
# Writes every generated or skipped edge to /var/lib/svctl/edges and the units
# to keep out of the database to /var/lib/svctl/excluded.gen. Runs under the
# svctl lock; -v reports counts.
# shellcheck disable=SC1091 # library of svctl
set -u
set -f

. /usr/lib/svctl/common.sh

FSTAB=/etc/fstab
CRYPTTAB=/etc/crypttab
VERBOSE=0
[ "${1:-}" = -v ] && VERBOSE=1

mkdir -p "$RUNDIR" "$STATE" || exit 1
W=$(mktemp -d "$RUNDIR/generator.XXXXXX") || exit 1
NEW=$STORE_GEN.new.$$
trap 'rm -rf "$W" "$NEW"' EXIT

fail() {
	say "generator: $*"
	exit 1
}

# store scan: units, their flags, their requires-mounts-for and the entries
# of their *.d directories
i=0
for s in "$STORE_PKG" "$STORE_ADM"; do
	i=$((i + 1))
	[ -d "$s" ] || continue
	find "$s" -mindepth 2 -maxdepth 3 \
		\( -path "$s/*/*/*" -path "$s/*/*.d/*" -printf "D$i\t%h\t%f\n" \) -o \
		\( -path "$s/*/*/*" -prune \) -o \
		\( -name type -printf "T$i\t%h\n" \) -o \
		\( -name requires-mounts-for -type f -printf "R$i\t%h\n" \) -o \
		\( -name 'flag-*' -printf "F$i\t%h\t%f\n" \) -o \
		\( \( -name timeout-up -o -name timeout-down -o -name timeout-kill -o -name up -o -name down -o \
		-name no-default-dependencies -o -name once -o -name dependencies -o -name dependencies.d -o -name finish -o -name producer-for -o -name consumer-for \) -printf "X$i\t%h\t%f\n" \)
done >"$W/scan" || fail "cannot read the stores"
# drop-ins: I<i> root path, i 3 /usr/lib, 4 /run, 5 /etc
i=2
for r in "$DROPIN_PKG" "$DROPIN_RUN" "$DROPIN_ADM"; do
	i=$((i + 1))
	[ -d "$r" ] || continue
	find "$r" -mindepth 2 -maxdepth 3 ! -type d -printf "I$i\t$r\t%P\n"
done >"$W/dropin" || fail "cannot read the drop-ins"

if [ -d "$REPO" ]; then
	timeout "$((REPO_WAIT / 1000))" s6-rc-set-status "$SET" >"$W/rx" 2>/dev/null || : >"$W/rx"
else
	: >"$W/rx"
fi
# units svctl mask named before they existed count as masked
if [ -s "$PREMASKED" ]; then
	sed 's#$#/masked#' "$PREMASKED" >>"$W/rx"
fi
# units kept out of the database count with their rx before
if [ -s "$EXCLUDED" ]; then
	awk -F '\t' '{ print $1 "/" $2 }' "$EXCLUDED" >>"$W/rx"
fi

# the paths of requires-mounts-for, "unit<TAB>path", from the store that wins
# and from the drop-ins of the unit
LC_ALL=C awk -F '\t' '
	$1 ~ /^T/ { u = $2; sub(/.*\//, "", u); i = substr($1, 2) + 0; if (i > win[u]) win[u] = i; next }
	$1 ~ /^R/ { u = $2; sub(/.*\//, "", u); n++; ru[n] = u; ri[n] = substr($1, 2) + 0; rf[n] = $2 "/requires-mounts-for" }
	$1 ~ /^I/ && $3 ~ /^[^\/]+\/requires-mounts-for$/ { u = $3; sub(/\/.*/, "", u); n++; ru[n] = u; ri[n] = -1; rf[n] = $2 "/" $3 }
	END {
		for (k = 1; k <= n; k++) {
			if (!(ru[k] in win) || (ri[k] >= 0 && win[ru[k]] != ri[k])) continue
			while ((getline l < rf[k]) > 0) if (l != "" && l !~ /^#/) print ru[k] "\t" l
			close(rf[k])
		}
	}' "$W/scan" "$W/dropin" >"$W/rmf" || fail "cannot read requires-mounts-for"

set --
[ -f "$CRYPTTAB" ] && set -- "$@" "$CRYPTTAB"
[ -f "$FSTAB" ] && set -- "$@" "$FSTAB"
LC_ALL=C awk -f "$LIBDIR/fstab.awk" -v crypttab="$CRYPTTAB" -v fstab="$FSTAB" -v libexec="$LIBEXEC" -v rmf="$W/rmf" "$@" </dev/null >"$W/fs" ||
	fail "cannot read $FSTAB or $CRYPTTAB"

# SVCTL_GEN_HOLD: a file of "a<TAB>b" edges from hints to leave out
LC_ALL=C awk -f "$LIBDIR/plan.awk" -v rxfile="$W/rx" -v scanfile="$W/scan" -v dropinfile="$W/dropin" -v fsfile="$W/fs" \
	-v holdfile="${SVCTL_GEN_HOLD:-}" -v hull="$LIBEXEC/hull" "$W/rx" "$W/scan" "$W/dropin" "$W/fs" >"$W/plan" || fail "planning failed"

rm -rf "$NEW"
mkdir -p "$NEW" || fail "cannot create $NEW"
awk -F '\t' '$1 == "COPY" { print $3 }' "$W/plan" | xargs -r -d '\n' cp -a -t "$NEW" -- ||
	fail "cannot copy units into $NEW"
find "$NEW" -mindepth 2 -maxdepth 2 \( -name wanted-by.d -o -name required-by.d -o -name before.d -o -name after.d \) \
	-exec rm -rf {} + || fail "cannot remove hints in $NEW"
# the list file dependencies counts only without dependencies.d: turned into it
find "$NEW" -mindepth 2 -maxdepth 2 -name dependencies -type f | while read -r f; do
	[ ! -e "${f%/*}/dependencies.d" ] || continue
	mkdir "${f%/*}/dependencies.d" || exit 1
	sed -n 's/^[[:space:]]*\([^#[:space:]][^[:space:]]*\).*/\1/p' "$f" | while read -r d; do : >"${f%/*}/dependencies.d/$d"; done
	rm -f "$f"
done || fail "cannot convert dependencies in $NEW"
awk -F '\t' -v d="$NEW" '$1 == "RMDEP" { print d "/" $2 "/dependencies.d/" $3; print d "/" $2 "/contents.d/" $3 }' "$W/plan" |
	xargs -r -d '\n' rm -f -- || fail "cannot remove edges in $NEW"
awk -F '\t' -v d="$NEW" '
	$1 == "NEW" || $1 == "COPY" { print d "/" $2 "/dependencies.d" }
	$1 == "BUNDLE" { print d "/" $2 "/contents.d" }' "$W/plan" | xargs -r -d '\n' mkdir -p -- ||
	fail "cannot create directories in $NEW"
awk -F '\t' -v d="$NEW" -v x="$W/exec" '
	$1 == "FILE" {
		f = d "/" $2 "/" $3; c = $4
		if ($3 == "finish") { gsub(/\\n/, "\n", c); print f > x }
		if (c == "") printf "" > f; else print c > f
		close(f)
	}
	$1 == "BUNDLE" { f = d "/" $2 "/type"; print "bundle" > f; close(f) }
	$1 == "WRAP" {
		f = d "/" $2 "/" $3; s = $5; c = $4
		while ((getline l < s) > 0) c = c "\n" l
		close(s)
		print c > f; close(f)
	}' "$W/plan" || fail "cannot write files in $NEW"
[ ! -s "$W/exec" ] || xargs -r -d '\n' chmod 755 -- <"$W/exec" || fail "cannot make the finish scripts in $NEW executable"
awk -F '\t' -v d="$NEW" '
	$1 == "DEP" { print d "/" $2 "/dependencies.d/" $3 }
	$1 == "BUNDLE" { print d "/" $2 "/contents.d/" $3 }' "$W/plan" | xargs -r -d '\n' touch -- ||
	fail "cannot write edges in $NEW"

awk -F '\t' 'BEGIN { OFS = "\t" }
	$1 == "EDGE" { print "edge", $2, $3, $4, $5, "" }
	$1 == "SKIP" { print "skip", $2, $3, $4, $5, $6 }
	$1 == "WANT" { print "want", $2, $3, "wanted-by.d", $3, "" }
	$1 == "ADD" { print "add", $2, $3, "generator", $4, "" }' "$W/plan" | sort >"$W/edges"

awk -F '\t' '$1 == "WARN" { print "svctl: " $2 }' "$W/plan" >&2
awk -F '\t' 'BEGIN { OFS = "\t" } $1 == "EXCL" { print $2, $3 }' "$W/plan" | sort >"$W/excl"

mkdir -p "${STORE_GEN%/*}" || fail "cannot create ${STORE_GEN%/*}"
if [ -d "$STORE_GEN" ]; then
	mv "$STORE_GEN" "$STORE_GEN.old.$$" || fail "cannot replace $STORE_GEN"
fi
mv "$NEW" "$STORE_GEN" || fail "cannot replace $STORE_GEN"
rm -rf "$STORE_GEN.old.$$"
mv "$W/edges" "$EDGES" || fail "cannot write $EDGES"
mv "$W/excl" "$EXCL_GEN" || fail "cannot write $EXCL_GEN"

if [ "$VERBOSE" = 1 ]; then
	awk -F '\t' '{ c[$1]++ } END {
		printf "svctl: generator: %d edges, %d wanted longruns, %d skipped, %d copies, %d added files, %d units from fstab and crypttab, %d excluded\n", c["EDGE"], c["WANT"], c["SKIP"], c["COPY"], c["ADD"], c["NEW"], c["EXCL"]
	}' "$W/plan" >&2
fi
exit 0
