#!/bin/sh
# /etc/uci-defaults/30_shater-core
#
# One-time, idempotent setup for shater-core. Runs on first boot (and via the
# default postinst on a live opkg/apk install). Must exit 0 so it is cleared and
# not retried. Everything here is safe to run more than once.

RT_TABLES=/etc/iproute2/rt_tables

# Append "<id>  <name>" to rt_tables only if neither the id nor the name is
# already present. Purely cosmetic (readable `ip rule`/`ip route` output);
# shaterd allocates the rest of the reserved block numerically.
seed_rt_table() {
	local id="$1" name="$2"
	[ -f "$RT_TABLES" ] || return 0
	grep -qE "^[[:space:]]*${id}[[:space:]]" "$RT_TABLES" && return 0
	grep -qE "[[:space:]]${name}[[:space:]]*\$" "$RT_TABLES" && return 0
	# A file without a trailing newline would otherwise get our entry glued onto
	# its last line, corrupting BOTH (and the glued line then defeats the grep
	# guards above, so every re-run would append again — breaking idempotency).
	[ -n "$(tail -c 1 "$RT_TABLES")" ] && printf '\n' >> "$RT_TABLES"
	printf '%s\t%s\n' "$id" "$name" >> "$RT_TABLES"
}

# Reserved routing-table base 0x2000 == 8192.
seed_rt_table 8192 shater

# Persistent home for sing-box's cache DB (experimental.cache_file, D16). The
# daemon also creates this on start; seeding it here means the very first apply
# lands the cache on /etc/shater/cache.db (survives reboot) instead of tmpfs.
mkdir -p /etc/shater

# NOTE: enable/restart of the init scripts is deliberately NOT done here inline
# — see the detached bring-up block at the end of this file. On a live
# opkg/apk install this script runs synchronously INSIDE the package manager's
# transaction, and any /etc/init.d/* invocation in that window risks blocking
# the transaction on rc.common's per-service flock (procd_lock).

# Seed the built-in preset packs (disabled) so the LuCI Rules page renders their
# toggles. Idempotent: only creates a section that does not yet exist.
seed_preset() {
	local sid="$1" name="$2" s n
	uci -q get "shater.$sid" >/dev/null 2>&1 && return 0
	# A pack section may already exist under a DIFFERENT section id (created by
	# the LuCI seeding or an older release) — match by pack name, not just id,
	# or we would duplicate the toggle.
	for s in $(uci -q show shater 2>/dev/null | sed -n "s/^shater\.\([^.=]*\)=preset$/\1/p"); do
		n=$(uci -q get "shater.$s.name")
		[ "$n" = "$name" ] && return 0
	done
	uci set "shater.$sid=preset"
	uci set "shater.$sid.name=$name"
	uci set "shater.$sid.enabled=0"
}
if uci -q get shater.globals >/dev/null 2>&1 || [ -f /etc/config/shater ]; then
	seed_preset block_ads block-ads
	seed_preset ru_bypass ru-bypass
	seed_preset private   private
	uci -q commit shater
fi

# Bring the UCI schema forward on upgrade (idempotent; refuses a newer schema).
[ -x /usr/bin/shaterd ] && /usr/bin/shaterd migrate >/dev/null 2>&1

# Apply our sysctl knobs NOW (boot applies them via procd's sysctl service, but
# on a live opkg/apk install nothing else re-reads sysctl.d — without this, an
# install->enable->apply flow hits the rp_filter "rules match but nothing works"
# failure until the first reboot). Idempotent; unknown keys are ignored.
[ -f /etc/sysctl.d/99-shater.conf ] && sysctl -p /etc/sysctl.d/99-shater.conf >/dev/null 2>&1

# Bring the services up (enable + restart) — but DETACHED, never from this
# process. Why not inline: on a live `opkg install` / `apk add` this script is
# sourced synchronously by base-files' default_postinst, i.e. INSIDE the package
# manager's transaction. For a USE_PROCD init script EVERY rc.common action
# (even plain `enable`) first sources /lib/functions/procd.sh, whose
# `_procd_wrapper` -> `procd_lock` takes a BLOCKING exclusive flock on
# /var/lock/procd_<name>.lock. If any process spawned during the same
# transaction still holds that lock, the postinst blocks on the flock while the
# package manager waits on the postinst — a deadlock that hangs `apk add`
# forever (observed on BananaWRT 25.12; opkg holds its lock the same way).
# So the bring-up runs as a fully detached background job that first waits for
# the package manager to finish its transaction, then enables + restarts.
#
# Why still bring up at all (and not just `enable`): the admin panel is served
# BY the daemon, so on a live install nothing would run until the next reboot —
# the box would be uninstallable-then-unconfigurable in one step. `restart`
# (not `start`) also makes an upgraded init script / daemon binary take effect
# immediately. Safe in every context: with globals.enabled=0 the daemon is
# inert (no engine, no nft, no routing), and at FIRST boot (uci-defaults run by
# procd, no apk/opkg alive — the wait falls through instantly) this simply
# pre-starts what the S99 symlink would start moments later (procd dedupes the
# identical instance).
#
# Detach rules (both known package-manager gotchas):
#   * ALL fds go to /dev/null — apk waits for EOF on the postinst's stdout pipe,
#     so a background child keeping that pipe open would hang the transaction
#     just as surely as the flock does.
#   * The job stays short-lived (bounded wait + idempotent service calls): it
#     may inherit the package manager's own open lock fds, which must not be
#     kept alive long after the transaction.
#   * `setsid` (own session, survives any process-group signalling) when
#     available, plain background otherwise; busybox `timeout` as a last-resort
#     bound so a wedged service call can never leave an immortal orphan.
SHATER_BRINGUP='
	i=0
	while [ "$i" -lt 120 ]; do
		pidof apk >/dev/null 2>&1 || pidof opkg >/dev/null 2>&1 || break
		sleep 1
		i=$((i + 1))
	done
	[ -x /etc/init.d/shater ] && /etc/init.d/shater enable
	[ -x /etc/init.d/shater-cron ] && /etc/init.d/shater-cron enable
	# The boot-time fail-closed armor. `enable` only — it is a one-shot that loads
	# the persisted holding plane at START=21, and running it NOW would install a
	# block on a live box moments before the daemon replaces it anyway. It has to
	# be enabled here regardless of whether the stack is on: the file it loads only
	# exists while the daemon wants it to, so an enabled-but-unarmed service is a
	# no-op, and enabling it later would mean the first boot after an upgrade is
	# the one boot still exposed.
	[ -x /etc/init.d/shater-armor ] && /etc/init.d/shater-armor enable
	[ -x /etc/init.d/shater ] && /etc/init.d/shater restart
	[ -x /etc/init.d/shater-cron ] && /etc/init.d/shater-cron restart
	exit 0
'
SHATER_TMO=""
command -v timeout >/dev/null 2>&1 && SHATER_TMO="timeout 300"
if command -v setsid >/dev/null 2>&1; then
	setsid $SHATER_TMO /bin/sh -c "$SHATER_BRINGUP" </dev/null >/dev/null 2>&1 &
else
	$SHATER_TMO /bin/sh -c "$SHATER_BRINGUP" </dev/null >/dev/null 2>&1 &
fi

exit 0
