#!/bin/sh
# DSM service control for the RocketZero tunnel client.
#
# Tiến trình mang tên "rocketcore". Đây là frp (Apache 2.0) đổi tên: một
# thương hiệu duy nhất trên mọi máy đỡ làm khách hoang mang khi thấy một cái
# tên lạ chạy trong máy hoặc hiện ra ở hộp thoại tường lửa. Phần ghi công giấy
# phép nằm ở THIRD-PARTY.md và trong gói cài.

TARGET="/var/packages/rocketzero/target"
VAR="/var/packages/rocketzero/var"
CORE="$TARGET/bin/rocketcore"
CFG="$VAR/rocketcore.toml"
PIDFILE="$VAR/rocketcore.pid"

# Máy cài từ bản cũ vẫn còn tên frpc.*; chuyển sang tên mới một lần.
migrate_names() {
    [ -f "$VAR/frpc.toml" ] && [ ! -f "$CFG" ] || return 0
    sed 's#/frpc\.log#/rocketcore.log#' "$VAR/frpc.toml" >"$CFG" &&
        chmod 600 "$CFG" && rm -f "$VAR/frpc.toml"
    [ -f "$VAR/frpc.log" ] && mv "$VAR/frpc.log" "$VAR/rocketcore.log"
    rm -f "$VAR/frpc.pid"
    return 0
}

CHILDFILE="$VAR/rocketcore.child"

# Vòng trông coi: chạy tunnel khi CÓ cấu hình, nằm chờ khi chưa có.
#
# VÌ SAO CẦN: gói nay cài xong là CHƯA kích hoạt — khách tự kích hoạt trong trang
# RocketZero. Bản trước thì "start" thất bại khi thiếu cấu hình, mà gói không
# chạy thì DSM không hiện app, và khách không có đường nào vào để kích hoạt. Vòng
# lặp này làm "start" luôn thành công, nên app luôn mở được.
#
# Nó còn bỏ luôn nhu cầu quyền root sau khi kích hoạt: trang web chỉ cần ghi cấu
# hình rồi giết tiến trình con (cùng user, làm được), vòng này tự dựng lại tunnel
# với cấu hình mới trong vài giây. Không có nó thì phải gọi synopkg restart —
# thứ đòi root mà gói không có.
# Báo về "máy này đã cài gói", kể cả khi chưa kích hoạt.
#
# VÌ SAO CẦN: máy chỉ xuất hiện ở phía máy chủ khi nó kích hoạt. Người tải về,
# cài xong rồi dừng lại — vì chưa mua mã, vì kích hoạt trượt, hay vì chưa hiểu
# phải làm gì — là hoàn toàn vô hình. Mà khoảng cách giữa "đã cài" và "đã kích
# hoạt" đúng là con số nói cho ta biết khách rụng ở đâu; không có nó thì mọi cải
# tiến luồng kích hoạt đều là đoán.
#
# Gửi thưa: một lần lúc khởi động, rồi sáu tiếng một lần. Đây là số để nhìn xu
# hướng, không phải nhịp tim — gửi dày hơn chỉ tốn pin router của khách.
#
# Máy ĐÃ kích hoạt hầu như không đi qua đây, vì "wait" bên dưới đứng im hàng giờ
# khi tunnel đang chạy. Không sao: máy đã kích hoạt tự khai model, bản DSM và bản
# gói ở mỗi lượt đăng nhập tunnel. Đường này tồn tại cho đúng những máy KHÔNG có
# tunnel — tức những máy mà nếu không có nó thì ta chẳng biết gì cả.
HELLO_EVERY=21600
HELLO_STAMP="$VAR/hello.at"

say_hello() {
    [ -r "$TARGET/sysinfo.sh" ] || return 0
    _now=$(date +%s 2>/dev/null) || return 0
    _last=$(cat "$HELLO_STAMP" 2>/dev/null | tr -cd '0-9')
    [ -n "$_last" ] && [ $((_now - _last)) -lt "$HELLO_EVERY" ] && return 0

    TL_VAR="$VAR"; export TL_VAR
    . "$TARGET/sysinfo.sh"
    _ver=""
    if [ -r "$TARGET/version.sh" ]; then
        # version.sh đọc $SYNOPKG_PKGVER, biến chỉ DSM đặt lúc cài; ở đây nó rỗng
        # và với set -u thì hàm chết. Script này không bật set -u, nhưng cứ giữ
        # nếp phòng thân cho lần sau ai đó bật.
        . "$TARGET/version.sh" 2>/dev/null
        _ver=$(tl_pkg_ver 2>/dev/null || echo "")
    fi
    _id=$(cat "$VAR/linkid.txt" 2>/dev/null | tr -cd 'a-z0-9-')
    _api=$(cat "$VAR/api.txt" 2>/dev/null | tr -cd 'A-Za-z0-9:/._-')
    [ -n "$_api" ] || _api="https://roze.to"

    # -m 8: không để một máy chủ chậm giữ chân vòng trông coi. Mọi lỗi đều nuốt —
    # đếm số liệu không bao giờ được làm hỏng việc chính là giữ tunnel sống.
    curl -s -m 8 -o /dev/null -X POST "$_api/api/v1/hello" \
        -H 'Content-Type: application/json' \
        -d "{\"iid\":\"$(tl_iid)\",\"ver\":\"$_ver\",\"model\":\"$(tl_model)\",\"dsm\":\"$(tl_dsm)\",\"id\":\"$_id\"}" \
        2>/dev/null
    echo "$_now" >"$HELLO_STAMP" 2>/dev/null
    return 0
}

# Khách đổi cổng DSM SAU khi kích hoạt thì cấu hình tunnel thành lạc hậu, và cái
# hỏng không tự lộ ra: frpc vẫn nối tốt tới máy chủ, chỉ có điều nó chuyển byte
# vào một cổng không còn ai nghe. Khách thấy "máy bạn đang offline" mãi trong khi
# máy đang bật, và phía máy chủ không phân biệt được với một NAS đã tắt.
#
# Nên phải dò lại theo nhịp, không thể chỉ dò một lần lúc kích hoạt. Và phải chạy
# ở tiến trình riêng: vòng canh chính đứng chờ ở "wait" suốt thời gian tunnel còn
# sống, có thể là hàng tuần, nên nó không có nhịp nào để tự kiểm.
# TL_PORT_EVERY chỉ để bộ kiểm rút ngắn nhịp; trên máy khách nó rỗng và nhịp là
# 30 phút. Không dùng nhịp ngắn ngoài thực địa: mỗi lượt là một lần gõ curl vào
# DSM, và đây là tiến trình chạy suốt đời máy.
PORT_EVERY="${TL_PORT_EVERY:-1800}"
WATCHFILE="$VAR/portwatch.pid"

# Cổng đang ghi trong khối proxy "dsm". Không dùng grep: tệp có ba dòng localPort
# (dsm, smb-p2p, smb-relay) và hai dòng sau là 445 — sửa nhầm thì mất ổ đĩa mạng.
dsm_port_in_cfg() {
    awk '
        /^\[\[proxies\]\]/ { inblk = 0 }
        inblk && /^localPort = [0-9]+$/ { sub(/^localPort = /, ""); print; exit }
        /^name = "dsm"$/ { inblk = 1 }
    ' "$CFG" 2>/dev/null
}

set_dsm_port_in_cfg() {
    _new="$1"
    _tmp="$CFG.new.$$"
    : >"$_tmp" 2>/dev/null || return 1
    chmod 600 "$_tmp" 2>/dev/null
    awk -v p="$_new" '
        /^\[\[proxies\]\]/ { inblk = 0 }
        inblk && /^localPort = [0-9]+$/ { print "localPort = " p; inblk = 0; next }
        { print }
        /^name = "dsm"$/ { inblk = 1 }
    ' "$CFG" >>"$_tmp" 2>/dev/null || { rm -f "$_tmp"; return 1; }

    # Xem lại tệp mới TRƯỚC khi thay: ghi ra một cấu hình cụt thì tunnel không bao
    # giờ lên lại được, và lúc đó phải có người tới tận nơi. Ba dấu hiệu phải còn
    # nguyên — token đăng nhập, số khối proxy, và đúng cổng vừa đặt.
    grep -q '^metadatas.token' "$_tmp" 2>/dev/null || { rm -f "$_tmp"; return 1; }
    _a=$(grep -c '^\[\[proxies\]\]' "$CFG" 2>/dev/null)
    _b=$(grep -c '^\[\[proxies\]\]' "$_tmp" 2>/dev/null)
    [ "$_a" = "$_b" ] || { rm -f "$_tmp"; return 1; }
    _c=$(awk '
        /^\[\[proxies\]\]/ { inblk = 0 }
        inblk && /^localPort = [0-9]+$/ { sub(/^localPort = /, ""); print; exit }
        /^name = "dsm"$/ { inblk = 1 }
    ' "$_tmp" 2>/dev/null)
    [ "$_c" = "$_new" ] || { rm -f "$_tmp"; return 1; }

    mv -f "$_tmp" "$CFG" 2>/dev/null || { rm -f "$_tmp"; return 1; }
    return 0
}

watch_dsm_port() {
    [ -r "$TARGET/sysinfo.sh" ] || return 0
    TL_VAR="$VAR"; export TL_VAR
    . "$TARGET/sysinfo.sh"
    # Dò NGAY một lượt rồi mới ngủ, không ngủ trước. Khách đổi cổng trong lúc gói
    # đang tắt là chuyện thường — họ tắt gói đi để sửa mạng rồi bật lại. Ngủ trước
    # thì nửa tiếng đầu sau khi bật máy vẫn hỏng, mà đó đúng là lúc khách đang
    # ngồi xem nó có chạy không.
    _first=1
    while :; do
        [ "$_first" = 1 ] || sleep "$PORT_EVERY"
        _first=0
        [ -s "$CFG" ] || continue
        _want=$(tl_dsm_port 2>/dev/null)
        case "$_want" in ''|*[!0-9]*) continue ;; esac
        _have=$(dsm_port_in_cfg)
        # Rỗng nghĩa là cấu hình không có khối dsm như mong đợi — đừng đoán, để yên.
        [ -n "$_have" ] || continue
        [ "$_want" = "$_have" ] && continue
        set_dsm_port_in_cfg "$_want" || continue
        # Giết tiến trình con để vòng canh chính nối lại bằng cấu hình mới. Không
        # tự chạy lại ở đây: một chỗ khởi động tunnel là đủ, hai chỗ thì có ngày
        # hai bản frpc cùng chạy và tranh nhau một subdomain.
        #
        # Thử ba nhịp vì lượt dò đầu tiên chạy song song với lúc gói đang khởi
        # động: nếu bắn ngay thì pidfile có thể chưa được ghi, cú kill rơi vào chỗ
        # trống, và tunnel cứ thế chạy bằng cấu hình cũ tới nửa tiếng sau.
        _n=0
        while [ "$_n" -lt 3 ]; do
            _pid=$(cat "$CHILDFILE" 2>/dev/null | tr -cd '0-9')
            if [ -n "$_pid" ]; then
                kill "$_pid" 2>/dev/null
                break
            fi
            _n=$((_n + 1))
            sleep 2
        done
    done
}

supervise() {
    _warned=0
    while :; do
        say_hello 2>/dev/null || :
        if [ -s "$CFG" ] && grep -q '^metadatas.token' "$CFG" 2>/dev/null; then
            _warned=0
            "$CORE" -c "$CFG" >>"$VAR/rocketcore.out" 2>&1 &
            echo $! >"$CHILDFILE"
            wait $! 2>/dev/null
            rm -f "$CHILDFILE"
        elif [ -s "$CFG" ] && [ "$_warned" = 0 ]; then
            # CFG CÓ nhưng không đọc/parse được token — gần như luôn là LỆCH QUYỀN
            # (vd activate bị chạy bằng root -> toml root:root 0600, user gói không
            # đọc được). Không launch mà IM LẶNG thì cực khó lần (NAS cứ "offline").
            # Ghi MỘT dòng để lộ nguyên nhân thay vì đứng im.
            echo "$(date 2>/dev/null): config present but metadatas.token unreadable — tunnel NOT started; check ownership/permissions of $CFG (must be readable by the package user)" >>"$VAR/rocketcore.out" 2>&1
            _warned=1
        fi
        # Nhịp nghỉ này là cái phanh: tunnel hỏng ngay khi vừa chạy (cấu hình sai,
        # mất mạng) thì không có nó vòng lặp sẽ quay tít và ngốn hết CPU của NAS.
        sleep 3
    done
}

# --- Mặt phẳng SMB v2: agent tsnet (userspace) chạy SONG SONG frpc. -----------
# Web (<id>.roze.to) vẫn do frpc lo; agent này chỉ đưa SMB vào overlay. Bật khi
# gói có binary rz-overlay VÀ manager trả về key qua provision-nas; không thì im
# lặng đứng ngoài (gói/manager cũ không đổi hành vi).
OV_BIN="$TARGET/bin/rz-overlay"
OV_STATE="$VAR/overlay"
OV_CTRL="$VAR/overlay.control"       # nhớ control URL cho lần nối lại
OV_PIDFILE="$VAR/rz-overlay.pid"
OV_CHILD="$VAR/rz-overlay.child"

# In ra "CONTROL<TAB>KEY". KEY rỗng nếu đã có state (nối lại khỏi xin key mới).
# Dòng rỗng = overlay không khả dụng -> bỏ qua agent.
provision_overlay() {
    [ -x "$OV_BIN" ] || { echo ""; return; }
    _api=$(cat "$VAR/api.txt" 2>/dev/null | tr -cd 'A-Za-z0-9:/._-'); [ -n "$_api" ] || _api="https://roze.to"
    _id=$(cat "$VAR/linkid.txt" 2>/dev/null | tr -cd 'a-z0-9-')
    _tok=$(sed -n 's/^metadatas.token = "\(.*\)"/\1/p' "$CFG" 2>/dev/null | head -1)
    [ -n "$_id" ] && [ -n "$_tok" ] || { echo ""; return; }
    if [ -s "$OV_STATE/tailscaled.state" ] && [ -s "$OV_CTRL" ]; then
        printf '%s\t\n' "$(cat "$OV_CTRL")"; return          # đã có danh tính node
    fi
    _r=$(curl -s -m 12 -X POST "$_api/api/v1/overlay/provision-nas" \
         -H 'Content-Type: application/json' \
         -d "{\"subdomain\":\"$_id\",\"token\":\"$_tok\"}" 2>/dev/null)
    case "$_r" in *'"authKey"'*) ;; *) echo ""; return ;; esac   # 404/off
    _c=$(printf '%s' "$_r" | sed -n 's/.*"controlUrl":"\([^"]*\)".*/\1/p')
    _k=$(printf '%s' "$_r" | sed -n 's/.*"authKey":"\([^"]*\)".*/\1/p')
    [ -n "$_c" ] && [ -n "$_k" ] || { echo ""; return; }
    mkdir -p "$OV_STATE" 2>/dev/null; chmod 700 "$OV_STATE" 2>/dev/null
    printf '%s' "$_c" > "$OV_CTRL"; chmod 600 "$OV_CTRL"
    printf '%s\t%s\n' "$_c" "$_k"
}

supervise_overlay() {
    while :; do
        _line=$(provision_overlay)
        _ctrl=$(printf '%s' "$_line" | cut -f1)
        _key=$(printf '%s' "$_line" | cut -f2)
        if [ -z "$_ctrl" ]; then
            sleep 600            # overlay chưa khả dụng: nằm chờ rồi thử lại
            continue
        fi
        RZ_CONTROL="$_ctrl" RZ_AUTHKEY="$_key" RZ_STATE="$OV_STATE" \
            RZ_TARGET="127.0.0.1:445" \
            RZ_HOSTNAME="$(cat "$VAR/linkid.txt" 2>/dev/null | tr -cd 'a-z0-9-')" \
            "$OV_BIN" >>"$VAR/rz-overlay.out" 2>&1 &
        echo $! >"$OV_CHILD"
        wait $! 2>/dev/null
        rm -f "$OV_CHILD"
        sleep 3                  # phanh, đừng quay tít nếu agent chết ngay
    done
}

# --- Tự lành khi token tunnel không còn khớp ----------------------------------
# Token vào tunnel là chuỗi DÙNG CHUNG. Hai lúc nó lệch với máy chủ: vận hành
# xoay token (sự cố bảo mật), và máy nâng cấp gói mà GIỮ cấu hình cũ (kích hoạt
# là việc của trang web, không phải trình cài — nên nâng cấp không tự chạy lại).
# Khi lệch, frp bị từ chối MÃI với "token in login doesn't match" và KHÔNG tự
# sửa được: token nằm trong cấu hình, phải kích hoạt lại mới lấy token + địa chỉ
# máy chủ hiện hành. Trước đây phải có người tới tận nơi bấm Save. Vòng này thấy
# frpc kẹt ở lỗi XÁC THỰC thì tự chạy lại activate.sh bằng mã + ID đã lưu.
#
# CHỈ lỗi xác thực, KHÔNG lỗi mạng: mạng đứt thì kích hoạt lại cũng cần mạng nên
# vô ích, và sẽ nện máy chủ mỗi lượt rớt sóng. Có chặn nhịp (REAUTH_EVERY) để một
# giấy phép hỏng thật không biến thành vòng gọi /activate liên hồi.
REAUTHFILE="$VAR/reauth.pid"
REAUTH_EVERY="${TL_REAUTH_EVERY:-600}"     # tối đa 10 phút/lần (server cũng chặn nhịp /activate)
REAUTH_STAMP="$VAR/reauth.at"

# Đếm lỗi XÁC THỰC kể từ lần đăng nhập thành công gần nhất. "token in login" là
# câu frp in khi token sai; các biến thể credential/authorization phòng khi máy
# chủ đổi lời. Lỗi mạng (i/o deadline, dial tcp) KHÔNG tính ở đây — đó là việc
# của tunnel_fails, và kích hoạt lại không chữa được.
auth_fails() {
    [ -s "$LOGF" ] || { echo 0; return 0; }
    tail -300 "$LOGF" 2>/dev/null | awk '
        /start proxy success|login to server success/ { n = 0; next }
        /token in login|authorization failed|authentication failed|invalid credential/ { n = n + 1 }
        END { print n + 0 }'
}

supervise_reauth() {
    while :; do
        sleep 60
        # Phải ĐÃ kích hoạt (có token trong cấu hình) và còn giữ mã + ID; thiếu
        # thì không có gì để kích hoạt lại, để yên cho luồng kích hoạt lần đầu.
        [ -s "$CFG" ] && grep -q '^metadatas.token' "$CFG" 2>/dev/null || continue
        [ -s "$VAR/license.txt" ] && [ -s "$VAR/linkid.txt" ] || continue
        # Chỉ ra tay khi lỗi xác thực DAI DẲNG (đã vài lượt thử lại), không chớp nhoáng.
        _af=$(auth_fails)
        [ "${_af:-0}" -ge 6 ] || continue
        # Chặn nhịp.
        _now=$(date +%s 2>/dev/null) || continue
        _last=$(cat "$REAUTH_STAMP" 2>/dev/null | tr -cd '0-9')
        [ -n "$_last" ] && [ $((_now - _last)) -lt "$REAUTH_EVERY" ] && continue
        echo "$_now" >"$REAUTH_STAMP" 2>/dev/null
        # Trace: xác thực chết dai dẳng nên phải tự kích hoạt lại — tín hiệu sự cố
        # kết nối thực địa. sysinfo.sh không có sẵn trong hàm này nên source tại chỗ.
        if [ -r "$TARGET/sysinfo.sh" ]; then
            ( TL_VAR="$VAR"; export TL_VAR; . "$TARGET/sysinfo.sh"; \
              tl_log reauth "" "auth_fails=$_af" ) 2>/dev/null || :
        fi
        # activate.sh ghi cấu hình mới rồi tự giết tiến trình con; vòng supervise
        # dựng lại tunnel với token mới. KHÔNG tự chạy frpc ở đây — một chỗ khởi
        # động tunnel là đủ (xem watch_dsm_port). Truncate-in-place trong activate
        # giữ nguyên chủ sở hữu file nên chạy dưới quyền nào cũng không đổi owner.
        "$TARGET/activate.sh" "$(cat "$VAR/license.txt")" "$(cat "$VAR/linkid.txt")" \
            >>"$VAR/install.log" 2>&1 || :
    done
}

start_daemon() {
    migrate_names
    if status_daemon >/dev/null 2>&1; then
        echo "Đang chạy rồi"
        return 0
    fi
    supervise >/dev/null 2>&1 &
    echo $! >"$PIDFILE"
    watch_dsm_port >/dev/null 2>&1 &
    echo $! >"$WATCHFILE"
    supervise_overlay >/dev/null 2>&1 &
    echo $! >"$OV_PIDFILE"
    supervise_reauth >/dev/null 2>&1 &
    echo $! >"$REAUTHFILE"
    sleep 1
    status_daemon >/dev/null 2>&1
}

stop_daemon() {
    # Giết vòng trông coi TRƯỚC, không thì nó thấy con chết và dựng lại ngay.
    if [ -f "$PIDFILE" ]; then
        kill "$(cat "$PIDFILE")" 2>/dev/null
        rm -f "$PIDFILE"
    fi
    # Bộ canh cổng cũng phải chết trước con, không thì nó thấy con chết rồi đi
    # giết một tiến trình đã không còn — vô hại, nhưng nó còn sống sau khi khách
    # bấm Stop thì gói coi như chưa dừng.
    if [ -f "$WATCHFILE" ]; then
        kill "$(cat "$WATCHFILE")" 2>/dev/null
        rm -f "$WATCHFILE"
    fi
    # Vòng tự-lành cũng phải chết TRƯỚC con: nó thấy lỗi xác thực là chạy lại
    # activate (thứ giết con) — để nó sống qua lúc Stop thì gói coi như chưa dừng.
    if [ -f "$REAUTHFILE" ]; then
        kill "$(cat "$REAUTHFILE")" 2>/dev/null
        rm -f "$REAUTHFILE"
    fi
    if [ -f "$CHILDFILE" ]; then
        kill "$(cat "$CHILDFILE")" 2>/dev/null
        rm -f "$CHILDFILE"
    fi
    # Agent overlay: giết vòng canh trước rồi giết con, như frpc ở trên.
    if [ -f "$OV_PIDFILE" ]; then
        kill "$(cat "$OV_PIDFILE")" 2>/dev/null
        rm -f "$OV_PIDFILE"
    fi
    if [ -f "$OV_CHILD" ]; then
        kill "$(cat "$OV_CHILD")" 2>/dev/null
        rm -f "$OV_CHILD"
    fi
    # Chắc tay: tiến trình có thể đã được chạy lại ngoài pidfile. Dọn cả tên cũ
    # cho máy vừa nâng cấp từ bản trước.
    killall rocketcore 2>/dev/null
    killall frpc 2>/dev/null
    killall rz-overlay 2>/dev/null
    return 0
}

status_daemon() {
    [ -f "$PIDFILE" ] || return 1
    kill -0 "$(cat "$PIDFILE")" 2>/dev/null
}

# Tunnel có đang thông không — hỏi chính cái log của frp.
#
# Tiến trình còn sống không có nghĩa là khách vào được NAS: frp vẫn chạy và vẫn
# thử kết nối lại trong khi đường ra Internet đã đứt hàng giờ.
#
# Cách đọc: khi tunnel thông, frp im lặng (không ghi gì thêm); khi đứt, nó ghi
# lỗi và "try to reconnect" liên tục. Nên ta đếm số lần hỏng KỂ TỪ lần thành
# công gần nhất — số đó vừa cho biết đang đứt hay không, vừa cho biết đứt lâu
# chưa, mà không phải mò đọc dấu thời gian trong log (định dạng đổi theo phiên
# bản frp, và date trên DSM là busybox nên không phân tích được cho chắc).
#
# Không có log hoặc log không có dòng nào đáng kể thì coi như đang thông: thà
# báo nhầm là chạy còn hơn báo hỏng một cái tunnel đang tốt rồi khách bấm khởi
# động lại làm đứt thật.
LOGF="$VAR/rocketcore.log"; [ -f "$LOGF" ] || LOGF="$VAR/frpc.log"
RETRY_LIMIT=12

tunnel_fails() {
    [ -s "$LOGF" ] || { echo 0; return 0; }
    tail -300 "$LOGF" 2>/dev/null | awk '
        /start proxy success|login to server success/ { n = 0; next }
        /login to server error|login to server failed|connect to server error|session shutdown|try to reconnect|i\/o deadline reached|dial tcp/ { n = n + 1 }
        END { print n + 0 }'
}

case "$1" in
    start)   start_daemon ;;
    stop)    stop_daemon ;;
    restart) stop_daemon; sleep 1; start_daemon ;;
    status)
        if ! status_daemon; then echo "stopped"; exit 3; fi
        # DSM chỉ hiểu hai câu trả lời: 0 = đang chạy, 3 = đã dừng. Không có ô
        # nào cho "tiến trình sống nhưng tunnel đứt", nên phải chọn một bên.
        #
        # Chọn thế này: mới đứt, đang thử lại thì vẫn trả 0. Mạng nhà khách rớt
        # dăm giây là chuyện thường ngày; báo dừng ngay sẽ làm Package Center
        # nhấp nháy và có thể kéo theo một vòng dừng-chạy-dừng vô ích, trong khi
        # frp tự nối lại được mà không cần ai đụng vào.
        #
        # Chỉ khi đã thử lại hàng chục lần mà vẫn không vào nổi máy chủ — đứt
        # kéo dài tính bằng nhiều phút chứ không phải vài giây — mới trả 3. Tới
        # mức đó thì khởi động lại đúng là việc nên làm, và quan trọng hơn:
        # khách mở Package Center ra là thấy máy đang hỏng, thay vì thấy chữ
        # "Running" màu xanh trong khi không ai vào được NAS.
        FAILS=$(tunnel_fails)
        if [ "${FAILS:-0}" -ge "$RETRY_LIMIT" ]; then
            echo "stopped (tunnel mất kết nối, đã thử lại $FAILS lần)"
            exit 3
        fi
        if [ "${FAILS:-0}" -gt 0 ]; then
            echo "running (đang kết nối lại, lần thứ $FAILS)"
        else
            echo "running"
        fi
        exit 0
        ;;
    log)     echo "$VAR/rocketcore.log" ;;
    *)       echo "usage: $0 {start|stop|restart|status}"; exit 1 ;;
esac
