#!/bin/sh

. /etc/PG.conf

APPNAME=tcblock
MYROOT="${PGPATH}/app/${APPNAME}"
RAMROOT="/usr/ramdisk/app/${APPNAME}"
CONFDIR="${PGETC}/App/${APPNAME}"
WEBMAINROOT="/usr/ramdisk/admin/cgi-bin/App/${APPNAME}"
HTMLDIR="/usr/ramdisk/admin/html/App/${APPNAME}"
APPCONF="${CONFDIR}/tcblock.conf"


# 🔴 crond 到底读哪个目录,必须问 crond 自己,绝不能猜(2026-08-07 血的教训)
#
# busybox 各版本的默认 cron 目录**不一样**:
#   busybox v1.34.1 (192.168.6.3)     → Default:/var/spool/cron/crontabs
#   busybox v1.30.1 (192.168.100.254) → Default:/etc/crontabs
# 老代码硬猜 /var/spool/cron/crontabs,在 v1.30.1 机型上等于把任务写进了一个
# crond 根本不看的目录 → **定时同步永远不执行,而且完全静默**:
# 192.168.100.254 装机后整整 11 天零自动同步(sync.log 只有装机那次),
# 封堵群组里还留着 11 万条旧 IP,页面一切正常,没有任何报错。
#
# 探测顺序:① 正在跑的 crond 的 -c 参数(最权威,它就是事实)
#           ② crond --help 里编译进去的 Default:
#           ③ 兜底老路径
get_crond_dir()
{
	# ① 已运行 crond 的 -c 参数;用 comm 精确锁定进程,避免匹配到含 crond 字样的命令行
	d=$(ps -eo comm,args 2>/dev/null | awk '$1=="crond"{print $0}' \
		| grep -o '\-c[[:space:]][[:space:]]*[^[:space:]][^[:space:]]*' | head -1 \
		| sed 's/^-c[[:space:]][[:space:]]*//')
	[ -n "${d}" ] && { echo "${d}"; return; }

	# ② 问 crond 要编译默认值(--help 走 stderr,故 2>&1)
	d=$(/usr/sbin/crond --help 2>&1 | sed -n 's/.*Default:\([^ 	]*\).*/\1/p' | head -1 | tr -d '[:space:]')
	[ -n "${d}" ] && { echo "${d}"; return; }

	# ③ 兜底
	echo "/var/spool/cron/crontabs"
}

get_crontab_file()
{
	cd_dir=$(get_crond_dir)
	mkdir -p "${cd_dir}" 2>/dev/null
	echo "${cd_dir}/root"
}

# 本 APP 可能在历史版本里把任务写进了错误的目录,升级后要一并清掉,
# 否则等哪天 crond 换目录,老任务会诈尸(指向的还是 ramdisk 里的旧路径)。
STALE_CRON_DIRS="/var/spool/cron/crontabs /etc/crontabs /var/cron/tabs"


# ⚠️ 并发安全(2026-08-07 实测踩到):CGI 保存配置会调 `appctrl reload`(→setup_cron),
# 而在线升级同时在跑 `appctrl stop`+`start`(→remove_cron/setup_cron)。
# 原来两者都用固定名 ${cf}.tmp 且"读旧内容→写回"非原子,并发时互相覆盖,
# 结果 crontab 里出现两份重复的 tcblock 行(实测 4 条 tcblock cron)。
# 现在:临时文件带 $$ 唯一化 + 最后 awk 去重兜底 + mv 原子替换。
CRONTMP=""

# 去重后原子落盘($1 = 待写入的临时文件)
commit_cron()
{
	_cf=$(get_crontab_file)
	awk '!seen[$0]++' "$1" > "$1.u" 2>/dev/null && mv -f "$1.u" "$1"
	mv -f "$1" "${_cf}"
}


remove_cron()
{
	cf=$(get_crontab_file)
	if [ -f "${cf}" ]; then
		t="${cf}.tcb.$$"
		grep -v "tcblock_cron" "${cf}" 2>/dev/null | grep -v "tcblock_update" > "${t}" 2>/dev/null
		commit_cron "${t}"
	fi
	purge_stale_cron
}


# 把历史版本写错位置的本 APP 任务清干净(只删本 APP 的行,不动别人的)
purge_stale_cron()
{
	live=$(get_crond_dir)
	for d in ${STALE_CRON_DIRS}; do
		[ "${d}" = "${live}" ] && continue
		f="${d}/root"
		[ -f "${f}" ] || continue
		grep -q "tcblock_cron\|tcblock_update" "${f}" 2>/dev/null || continue
		t="${f}.tcb.$$"
		grep -v "tcblock_cron" "${f}" 2>/dev/null | grep -v "tcblock_update" > "${t}" 2>/dev/null
		mv -f "${t}" "${f}" 2>/dev/null
	done
}


# setup_cron() 已于 v20260807.100008 整体移除:自 090000 起改用 tcblock_daemon 调度,
# 该函数不再被任何路径调用,且内部还有 `. "${APPCONF}"`(source 配置=root 执行注入面)。
# 保留 remove_cron/purge_stale_cron 是为了清掉历史版本写进各 busybox cron 目录的残留任务。



tcblock_start()
{
	mkdir -p "${WEBMAINROOT}" "${HTMLDIR}" "${CONFDIR}" "${RAMROOT}/bin" "${RAMROOT}/tmp"
	# 一次性迁移旧版持久同步缓存到 ramdisk。保留群组归属/MD5，避免升级后
	# 首轮同步把既有专用群组误判为“他人群组”；后续运行只写 ramdisk。
	mkdir -p "${RAMROOT}/state"

	# 部署 CGI / HTML / bin 到 ramdisk 运行区
	cp -f "${MYROOT}/app.inf" "${RAMROOT}/app.inf"
	cp -f "${MYROOT}/app.png" "${RAMROOT}/app.png"
	cp -f "${MYROOT}/appctrl" "${RAMROOT}/appctrl"
	cp -rf ${MYROOT}/web/cgi/* "${WEBMAINROOT}/"
	cp -rf ${MYROOT}/web/html/* "${HTMLDIR}/"
	cp -rf ${MYROOT}/bin/* "${RAMROOT}/bin/"

	# 权限强制对齐 755(2026-08-07):官方 APP(ddnsx/smartdns/szsync)全是 755,
	# 本 APP 曾因打包机(macOS)源文件是 700,整包装成 700 —— 全网关唯一一个,
	# httpd 目前以 root 跑所以没炸,但只要 CGI 降权就整个 APP 不可用。
	# 用 chmod 755 而不是 chmod +x:+x 只补执行位,补不回 group/other 的读位。
	# 放在部署环节 = 即使包里权限不对也能自愈,不依赖打包机的 umask。
	chmod 755 "${RAMROOT}" "${RAMROOT}/bin" "${WEBMAINROOT}" "${HTMLDIR}" 2>/dev/null
	chmod 755 ${WEBMAINROOT}/* 2>/dev/null
	chmod 755 ${HTMLDIR}/* 2>/dev/null
	chmod 755 ${RAMROOT}/bin/* 2>/dev/null
	chmod 755 "${RAMROOT}/appctrl" "${RAMROOT}/app.inf" "${RAMROOT}/app.png" 2>/dev/null
	# 持久化目录同样对齐(在线升级覆盖进来的文件也可能带错权限)
	chmod 755 "${MYROOT}" "${MYROOT}/bin" "${MYROOT}/web" "${MYROOT}/web/cgi" "${MYROOT}/web/html" 2>/dev/null
	chmod 755 ${MYROOT}/bin/* ${MYROOT}/web/cgi/* ${MYROOT}/web/html/* 2>/dev/null
	chmod 755 "${MYROOT}/appctrl" "${MYROOT}/app.inf" "${MYROOT}/app.png" 2>/dev/null

	# 🔴 确保 crond 在跑(2026-08-07 查实的真因,别删这段)
	#
	# Panabit **开机不启动 crond** —— /etc/rc.local、/etc/rc.d/*、/etc/init.d/* 里
	# 一处启动 crond 的地方都没有(实测 grep 无结果)。
	# 证据:某次重启 boot=18:07:50,官方 APP 的常驻进程全在 18:08:05~18:08:14 起来
	# (说明 `appctrl start all` 开机执行得好好的),而 crond 的启动时间是 22:50:42
	# —— 晚了 4 小时 43 分,是后来被 szsync 的 appctrl 顺手拉起来的。
	#
	# 为什么官方 APP 不受影响:它们全部是常驻 daemon(ddnsx 的 ddnsctl -s &、
	# pingwatch &、webauth 的 authd、smartdns …),根本不用 cron。
	# 全网关只有 szsync 和本 APP 用 cron —— crond 不跑,定时同步一次都不会执行,
	# 表现就是"重启后不自动同步,得手动点立即同步"。
	#
	# ⚠️ 这跟 app.inf 的 app_type 无关:系统级 /usr/ramdisk/bin/appctrl 的
	# app_action() 只判断 `[ -e <dir>/appctrl ]` 就执行,从没读过 app.inf。
	# 也别指望用"cron 自愈行"兜底 —— crond 自己都没起来,那行同样不会被执行。
	# ⚠️ 判活必须只匹配"进程名",不能匹配命令行:
	# szsync 用的 `ps aux | grep -q '[c]rond'` 是按整条命令行匹配的,任何命令行里
	# 带 crond 字样的进程(哪怕是一条 ssh 上来执行的运维命令)都会让它误判成
	# "crond 已在跑"从而跳过启动 —— 2026-08-07 实测被这个坑到,照抄的这行没生效。
	crond_alive()
	{
		if command -v pgrep >/dev/null 2>&1; then
			pgrep -x crond >/dev/null 2>&1 && return 0
			return 1
		fi
		ps -eo comm 2>/dev/null | grep -qx crond && return 0
		return 1
	}
	# tcblock 自 v20260807.090000 起由 tcblock_daemon 调度；不再启动或依赖 crond。
	: 

	# 统一采用常驻守护进程做间隔门控。旧 cron 任务全部移除，避免不同
	# BusyBox cron 目录/是否带 crond 导致静默失效或与 daemon 重复同步。
	remove_cron
	start_daemon
}


tcblock_stop()
{
	stop_daemon
	# Do not kill processes by matching command lines; an SSH diagnostic command
	# can contain these names. The daemon lock prevents new syncs.
	remove_cron
	rm -rf "${WEBMAINROOT}"
	rm -rf "${HTMLDIR}"
	rm -rf "${RAMROOT}"
}


DAEMON_PID="${RAMROOT}/state/daemon.pid"

daemon_alive()
{
	[ -f "${DAEMON_PID}" ] || return 1
	p=$(cat "${DAEMON_PID}" 2>/dev/null)
	[ -n "${p}" ] || return 1
	kill -0 "${p}" 2>/dev/null || return 1
	# PID reuse: verify this PID is this application's daemon.
	cmd=$(tr '\000' ' ' < "/proc/${p}/cmdline" 2>/dev/null)
	case "${cmd}" in
		*"/tcblock_daemon daemon "*) return 0 ;;
		*) return 1 ;;
	esac
}

start_daemon()
{
	daemon_alive && return 0
	mkdir -p "${RAMROOT}/state"
	dbin="${RAMROOT}/bin/tcblock_daemon"
	[ -x "${dbin}" ] || dbin="${MYROOT}/bin/tcblock_daemon"
	( trap '' HUP; exec "${dbin}" daemon ) >/dev/null 2>&1 &
	sleep 1
	if ! daemon_alive; then
		mkdir -p "${CONFDIR}"
		echo "$(date '+%Y-%m-%d %H:%M:%S')|FAIL|daemon_start_failed:${dbin}" >> "${CONFDIR}/sync.log"
	fi
}

stop_daemon()
{
	# Only terminate the PID-file daemon after /proc identity verification.
	# Never grep arbitrary process command lines and kill them.
	if daemon_alive; then
		p=$(cat "${DAEMON_PID}" 2>/dev/null)
		kill -9 "${p}" 2>/dev/null
	fi
	rm -f "${DAEMON_PID}"
}

tcblock_enable()  { echo "enable"  > "${CONFDIR}/app_status"; start_daemon; }
tcblock_disable() { echo "disable" > "${CONFDIR}/app_status"; stop_daemon; remove_cron; }
# 🔴 status 必须反映"真的在跑",不能只回显期望状态
# 老实现只 cat app_status:daemon 挂掉后应用中心照样显示「已启用」,
# 同步早停了却毫无迹象 —— 又是一例静默失效。
# 现在:期望 enable 且 daemon 确实活着才报 enable,否则 disable
# (用户看到 disable 去点一下「启用」即触发 start_daemon,顺带成了自愈入口)。
# 保持纯查询、无副作用:不在 status 里拉起 daemon,以免应用中心刷新列表时反复起进程。
tcblock_status()
{
	want="disable"
	[ -f "${CONFDIR}/app_status" ] && want=$(cat "${CONFDIR}/app_status" 2>/dev/null | tr -cd 'a-z')
	if [ "${want}" = "enable" ] && daemon_alive; then
		echo "enable"
	else
		echo "disable"
	fi
}


case "$1" in
"start")   tcblock_start   ;;
"stop")    tcblock_stop    ;;
"reload")  remove_cron; start_daemon      ;;
"enable")  tcblock_enable  ;;
"disable") tcblock_disable ;;
"status")  tcblock_status  ;;
"unload")  ;;
*) echo "usage: appctrl {start|stop|enable|disable|status}" ;;
esac
