#!/bin/bash
# XFCloud 隧道服务端原生运行控制
# 飞牛 fnOS 只通过本脚本做 start/stop/status；源码就位由 install/upgrade_callback 负责。

# 不可变源（fnOS 安装目录，root 拥有）：仅在运行目录缺失时兜底复制。
APP_DIR="${TRIM_APPDEST}/server"
# 持久化数据根目录（/vol{n}/@appdata/xfserver）。
DATA_ROOT="${TRIM_PKGVAR}"
# 实际运行目录：全部源码的可写副本，归 xfserver 所有。
# Node 的 ROOT 解析到此目录，服务端在线更新与 frps 在线拉取都可直接写入。
WORK_DIR="${DATA_ROOT}/server"
LOG_FILE="${DATA_ROOT}/app.log"
PID_FILE="${DATA_ROOT}/app.pid"

# 飞牛应用中心提供的 Node.js 22 运行时。
export PATH="/var/apps/nodejs_v22/target/bin:$PATH"

# 服务端数据/运行时目录。
export DATA_DIR="${DATA_ROOT}/data/server"
export SERVER_RUNTIME_DIR="${DATA_ROOT}/runtime/server"
export MANAGER_HOST="0.0.0.0"
export MANAGER_PORT="${TRIM_SERVICE_PORT:-8080}"
export NODE_ENV="production"

# 安装/配置向导保存的持久配置（管理端口、管理员初始密码），覆盖默认值。
# 文件由回调脚本以 0600 权限写入 @appconf；此处只做安全的键值读取，不 source。
# ADMIN_PASSWORD 仅在数据目录首次建库（创建 admin 账号）时生效，之后改密以数据库为准。
CONF_FILE="${TRIM_PKGETC}/xfserver.env"
if [ -r "$CONF_FILE" ]; then
    while IFS='=' read -r key value; do
        case "$key" in
            SERVER_PORT)
                case "$value" in
                    ''|*[!0-9]*) ;;
                    *)
                        if [ "$value" -ge 1 ] 2>/dev/null && [ "$value" -le 65535 ] 2>/dev/null; then
                            export MANAGER_PORT="$value"
                        fi
                        ;;
                esac
                ;;
            ADMIN_PASSWORD)
                if [ -n "$value" ]; then
                    export ADMIN_PASSWORD="$value"
                fi
                ;;
        esac
    done < "$CONF_FILE"
fi

# 让 Node 启动时回写应用中心管理用的 pid 文件，
# 看门狗在线更新重启后的新进程同样会回写。
export APP_PID_FILE="${PID_FILE}"

# Node 入口的绝对路径：用作 pgrep/pkill 的唯一匹配特征（路径含 xfserver）。
# 注意：必须用绝对路径启动，否则进程命令行里只有相对的 server/app.js，
# 停止/卸载时按绝对路径匹配会漏掉真正占用端口的 Node 进程。
APP_ENTRY="${WORK_DIR}/server/app.js"
# 需要清理的子进程匹配模式（Node 主进程 / 在线更新看门狗 / frps）。
MATCH_NODE="${APP_ENTRY}"
MATCH_WATCHDOG="${DATA_ROOT}/data/server/updater.mjs"
MATCH_FRPS="${WORK_DIR}/bin/frps"

# 注意：不导出 FRPS_BIN。服务端默认解析到 ${WORK_DIR}/bin/frps（可写、可 chmod），
# 界面的"从服务端拉取/上传 frps"功能才能正常覆盖；导出会触发应用内的路径锁定保护。

log_msg() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}

check_process() {
    local pid=$1
    kill -0 "${pid}" 2>/dev/null
}

# 运行目录缺失时（如数据目录被整体清空）从安装源兜底复制一次。
# 正常安装/升级路径下源码由 install_callback/upgrade_callback（root）就位，不会走到这里。
ensure_work_dir() {
    if [ -f "${WORK_DIR}/server/app.js" ]; then
        return 0
    fi
    if [ ! -f "${APP_DIR}/server/app.js" ]; then
        echo "应用文件缺失：${APP_DIR}/server/app.js" > "$TRIM_TEMP_LOGFILE"
        return 1
    fi
    log_msg "work dir missing, copying ${APP_DIR} -> ${WORK_DIR}"
    mkdir -p "${WORK_DIR}"
    cp -a "${APP_DIR}/." "${WORK_DIR}/" 2>>"$LOG_FILE"
    [ -f "${WORK_DIR}/bin/frps" ] && chmod 700 "${WORK_DIR}/bin/frps" 2>/dev/null || true
}

# 列出工作目录为本应用的 Node 进程 PID（兼容旧版相对入口启动）。
node_pids_by_cwd() {
    command -v pgrep >/dev/null 2>&1 || return 0
    [ -d /proc ] || return 0
    local p link
    for p in $(pgrep -f "experimental-sqlite" 2>/dev/null); do
        link=$(readlink "/proc/$p/cwd" 2>/dev/null || true)
        [ "$link" = "$WORK_DIR" ] && echo "$p"
    done
}

# 是否存在工作目录为本应用的 Node 进程。
node_running_by_cwd() {
    [ -n "$(node_pids_by_cwd)" ]
}

status() {
    if [ -f "${PID_FILE}" ]; then
        local pid
        pid=$(head -n 1 "${PID_FILE}" | tr -d '[:space:]')
        if [ -n "$pid" ] && check_process "${pid}"; then
            return 0
        fi
        rm -f "${PID_FILE}"
    fi
    # pid 文件失效时兜底：按入口绝对路径或工作目录查找仍存活的 Node 进程。
    if command -v pgrep >/dev/null 2>&1; then
        if pgrep -f "$MATCH_NODE" >/dev/null 2>&1; then
            return 0
        fi
    fi
    if node_running_by_cwd; then
        return 0
    fi
    return 1
}

# 优雅→强制终止给定 PID 列表。
terminate_pids() {
    local pid count=""
    for pid in $1; do
        kill -TERM "$pid" 2>/dev/null || true
    done
    local i
    for i in $(seq 1 8); do
        count=""
        for pid in $1; do
            check_process "$pid" && count="$count $pid"
        done
        [ -z "$count" ] && return 0
        sleep 1
    done
    for pid in $count; do
        kill -KILL "$pid" 2>/dev/null || true
    done
}

# 收集本应用全部需要停止的进程 PID（Node 主进程、看门狗），含 PID 文件、命令行与工作目录匹配。
collect_node_pids() {
    local pids=""
    local pid p
    add_pid() {
        case " $pids " in
            *" $1 "*) ;;
            *) pids="$pids $1" ;;
        esac
    }
    if [ -f "${PID_FILE}" ]; then
        pid=$(head -n 1 "${PID_FILE}" | tr -d '[:space:]')
        [ -n "$pid" ] && add_pid "$pid"
    fi
    if command -v pgrep >/dev/null 2>&1; then
        for pattern in "$MATCH_NODE" "$MATCH_WATCHDOG"; do
            for p in $(pgrep -f "$pattern" 2>/dev/null); do
                add_pid "$p"
            done
        done
        # 旧版以相对入口启动，按工作目录精确补漏。
        for p in $(node_pids_by_cwd); do
            add_pid "$p"
        done
    fi
    echo "$pids"
}

start_process() {
    # 当前版本实例已在运行（PID 文件存活，或绝对入口进程存在）则不重复启动。
    if [ -f "${PID_FILE}" ]; then
        local cur
        cur=$(head -n 1 "${PID_FILE}" | tr -d '[:space:]')
        if [ -n "$cur" ] && check_process "$cur"; then
            return 0
        fi
        rm -f "${PID_FILE}"
    fi
    if command -v pgrep >/dev/null 2>&1 && pgrep -f "$MATCH_NODE" >/dev/null 2>&1; then
        return 0
    fi

    mkdir -p "$DATA_DIR" "$SERVER_RUNTIME_DIR"

    # 升级场景：旧版（相对路径启动）遗留进程仍占用端口时，先清掉再启动，避免端口冲突。
    local leftover
    leftover=$(node_pids_by_cwd)
    if [ -n "$leftover" ]; then
        log_msg "cleaning leftover app processes before start: $leftover"
        terminate_pids "$leftover"
        sleep 1
    fi

    ensure_work_dir || return 1

    log_msg "Starting XFCloud server (node $(node -v 2>/dev/null), port ${MANAGER_PORT}) ..."
    cd "$WORK_DIR" || return 1
    # node:sqlite 仍需 --experimental-sqlite 标志（Node 22）。
    # 用子 shell + exec、绝对入口启动：$! 即 Node 真实 PID，且命令行含唯一绝对路径，
    # 停止/卸载时即使 PID 文件失效也能按路径精确匹配到该进程。
    ( exec node --experimental-sqlite "$APP_ENTRY" ) >> "$LOG_FILE" 2>&1 &
    local node_pid=$!
    printf "%s" "$node_pid" > "$PID_FILE"
    sleep 1
    if ! check_process "$node_pid"; then
        log_msg "启动失败，最近日志："
        tail -n 30 "$LOG_FILE" >> "$LOG_FILE" 2>/dev/null || true
        rm -f "$PID_FILE"
        echo "XFCloud 服务端启动失败，请查看应用日志 ${LOG_FILE}" > "$TRIM_TEMP_LOGFILE"
        return 1
    fi
    return 0
}

stop_process() {
    log_msg "Stopping XFCloud server ..."

    # 1. 收集 Node 主进程与看门狗（PID 文件 + 命令行 + 工作目录匹配，取并集）并优雅→强制停止。
    local pids
    pids=$(collect_node_pids)
    [ -n "$pids" ] && terminate_pids "$pids"
    rm -f "${PID_FILE}"

    # 2. Node 终止后，frps 与看门狗可能被 init 收养继续运行，按本应用绝对路径精确清理。
    if command -v pgrep >/dev/null 2>&1; then
        local pattern child
        for pattern in "$MATCH_FRPS" "$MATCH_WATCHDOG" "$MATCH_NODE"; do
            for child in $(pgrep -f "$pattern" 2>/dev/null); do
                kill -TERM "$child" 2>/dev/null || true
            done
        done
        sleep 1
        for pattern in "$MATCH_FRPS" "$MATCH_WATCHDOG" "$MATCH_NODE"; do
            for child in $(pgrep -f "$pattern" 2>/dev/null); do
                kill -KILL "$child" 2>/dev/null || true
            done
        done
    fi
    return 0
}

case $1 in
start)
    start_process
    ;;
stop)
    stop_process
    ;;
status)
    if status; then
        exit 0
    else
        exit 3
    fi
    ;;
*)
    echo "Unknown command: $1" > "$TRIM_TEMP_LOGFILE"
    exit 1
    ;;
esac
