netx/packaging/linux/netxctl
oliver eb0eccdffe UME TLS off by default on Linux and Windows; truthful admin login hints.
- Deployments are on internal networks with self-signed UME certificates:
  installers write NETX_UME_VERIFY_TLS=false and
  NETX_ALLOW_INSECURE_DEFAULTS=1 when absent (Linux also on update), so
  binding 0.0.0.0 no longer stops NetX at the startup security check.
  Admin password and JWT secret stay unique per install.
- Linux: after install, check app_user to report whether the generated
  initial password applies or the admin already exists (reused external
  DB); new 'netxctl admin-password'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:50:38 +08:00

531 lines
20 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# NetX Linux control command. Installed as /usr/bin/netxctl
# -> <prefix>/current/packaging/linux/netxctl
set -euo pipefail
SELF="$(readlink -f "${BASH_SOURCE[0]}")"
HERE="$(dirname "${SELF}")"
# shellcheck source=lib.sh
. "${HERE}/lib.sh"
REL_DIR="$(cd "${HERE}/../.." && pwd)"
if [[ "$(basename "$(dirname "${REL_DIR}")")" == "releases" ]]; then
PREFIX="$(dirname "$(dirname "${REL_DIR}")")"
else
PREFIX="${NETX_PREFIX:-/opt/netx}"
fi
CUR="${PREFIX}/current"
NETX_DATA="/var/lib/netx"
if [[ -f "${PREFIX}/netx.conf" ]]; then
_d="$(sed -n 's/^NETX_DATA=//p' "${PREFIX}/netx.conf" | tail -n1)"
[[ -n "${_d}" ]] && NETX_DATA="${_d}"
fi
NETX_DATA="${NETX_DATA_OVERRIDE:-${NETX_DATA}}"
ENV_FILE="${NETX_DATA}/.env"
NETX_LOG_DIR="${NETX_DATA}/logs"
NETX_PY="${CUR}/python/bin/python3"
NETX_ADMIN_PY="${CUR}/packaging/linux/netx_admin.py"
PGBIN="${CUR}/pgsql/bin"
PGDATA_DIR="${NETX_DATA}/pgdata"
ARCH="x86_64"
usage() {
cat <<EOF
$(t "用法" "Usage"): netxctl <command> [options]
start | stop | restart | status
logs [api|worker|biz0|pg|netxctl] [-f] [-n N]
set-port <port> $(t "修改 Web/API 端口" "change the Web/API port")
set-bind <host> $(t "修改监听地址(0.0.0.0 = 所有网卡)" "change the listen address (0.0.0.0 = all)")
set-pg-port <port> $(t "修改内置 PostgreSQL 端口" "change the bundled PostgreSQL port")
reconfigure-db $(t "重新配置数据库(内置/外置)" "reconfigure the database (bundled/external)")
check-update $(t "检查新版本" "check for a newer release")
update [--yes] [--file <netx-x.y.z-linux-x86_64.tar.gz>] [--force]
auto-update on|off $(t "每日自动更新" "daily automatic update")
backup [<file.tar.gz>] $(t "备份数据库与密钥" "back up database + secrets")
admin-password $(t "查看管理员登录密码情况" "show the admin login (initial password or not)")
uninstall [--purge] [--yes]
version
run $(t "前台运行(systemd 使用)" "foreground supervisor (used by systemd)")
EOF
}
need_root() { is_root || die "$(t "需要 root 权限:sudo netxctl $*" "root required: sudo netxctl $*")"; }
need_install() {
[[ -x "${NETX_PY}" ]] || die "$(t "未找到 NetX 安装" "NetX installation not found"): ${CUR}"
if [[ ! -r "${ENV_FILE}" ]]; then
is_root || [[ "$(id -un)" == "${NETX_USER}" ]] || die "$(t "需要 root 权限:sudo netxctl" "root required: sudo netxctl")"
die "$(t "未找到配置文件" "config not found"): ${ENV_FILE}"
fi
}
cfg() { env_get "${ENV_FILE}" "$1"; }
db_mode() { local m; m="$(cfg NETX_DB_MODE | tr 'A-Z' 'a-z')"; [[ "${m}" == "bundled" ]] && echo bundled || echo external; }
api_port() { local p; p="$(cfg NETX_PORT)"; echo "${p:-8890}"; }
api_bind() { local h; h="$(cfg NETX_HOST)"; echo "${h:-0.0.0.0}"; }
pg_port() { local p; p="$(cfg NETX_BUNDLED_PG_PORT)"; echo "${p:-15432}"; }
health_url() { echo "http://$(client_host "$(api_bind)"):$(api_port)/health"; }
cur_version() { netx_admin json-get "${CUR}/version.json" version 2>/dev/null || echo "?"; }
wait_healthy() {
local timeout="${1:-120}"
if netx_admin wait-health "$(health_url)" --timeout "${timeout}"; then
return 0
fi
err "$(t "NetX 未在 ${timeout} 秒内就绪" "NetX not healthy after ${timeout}s"): $(health_url)"
show_start_failure "${NETX_DATA}"
return 1
}
# ------------------------------------------------------------------ run (foreground)
CHILD_PIDS=()
CHILD_NAMES=()
STOPPING=0
PG_OWNED=0
spawn() {
local name="$1"
shift
"$@" >>"${NETX_LOG_DIR}/${name}.log" 2>&1 &
CHILD_PIDS+=("$!")
CHILD_NAMES+=("${name}")
echo "$(date '+%F %T') started ${name} pid=$!"
}
stop_children() {
local pid i alive
for pid in "${CHILD_PIDS[@]}"; do kill -TERM "${pid}" 2>/dev/null || true; done
for ((i = 0; i < 60; i++)); do
alive=0
for pid in "${CHILD_PIDS[@]}"; do kill -0 "${pid}" 2>/dev/null && alive=1; done
[[ "${alive}" == "0" ]] && break
sleep 0.5
done
for pid in "${CHILD_PIDS[@]}"; do kill -KILL "${pid}" 2>/dev/null || true; done
if [[ "${PG_OWNED}" == "1" ]]; then
echo "$(date '+%F %T') stopping bundled PostgreSQL"
pg_stop
fi
}
cmd_run() {
if is_root; then
exec "${NETX_PY}" -I "${NETX_ADMIN_PY}" run-as "${NETX_USER}" "${SELF}" run
fi
need_install
mkdir -p "${NETX_LOG_DIR}"
load_env_file "${ENV_FILE}"
local kv
while IFS= read -r kv; do export "${kv%%=*}=${kv#*=}"; done < <(data_env_pairs "${NETX_DATA}")
export NETX_UI_DIST_DIR="${CUR}/web/dist"
export NETX_RUN_INLINE_SCHEDULERS=false
export PYTHONPATH="${CUR}"
export PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
cd "${CUR}"
if [[ "$(db_mode)" == "bundled" ]]; then
local want have
want="$(netx_admin json-get "${CUR}/version.json" pg_major 2>/dev/null || echo "")"
have="$(pg_data_major || true)"
[[ -n "${have}" ]] || die "bundled_pg_not_initialised: ${PGDATA_DIR}"
if [[ -n "${want}" && "${have}" != "${want}" ]]; then
die "pg_major_mismatch: data=${have} bundled=${want}"
fi
if ! pg_is_running; then
if ! netx_admin port-free "$(pg_port)" --host 127.0.0.1; then
die "port_in_use: 127.0.0.1:$(pg_port) (bundled PostgreSQL) — $(t "可用 netxctl set-pg-port 换端口" "use netxctl set-pg-port")"
fi
echo "$(date '+%F %T') starting bundled PostgreSQL on 127.0.0.1:$(pg_port)"
pg_start || die "pg_start_failed (see ${NETX_LOG_DIR}/pg.log)"
PG_OWNED=1
fi
fi
trap 'STOPPING=1' TERM INT
spawn api "${NETX_PY}" -m netx_api.main
spawn worker "${NETX_PY}" -m netx_api.worker
local n="${NETX_BIZ_STATE_WORKER_REPLICAS:-2}" i
[[ "${n}" =~ ^[0-9]+$ ]] || n=2
((n >= 1)) || n=1
for ((i = 0; i < n; i++)); do
spawn "biz${i}" "${NETX_PY}" -m netx_api.biz_state_worker
done
local rc=0 sleeper
while [[ "${STOPPING}" == "0" ]]; do
for i in "${!CHILD_PIDS[@]}"; do
if ! kill -0 "${CHILD_PIDS[$i]}" 2>/dev/null; then
wait "${CHILD_PIDS[$i]}" 2>/dev/null && rc=$? || rc=$?
echo "$(date '+%F %T') ${CHILD_NAMES[$i]} exited (rc=${rc}); stopping the rest" >&2
rc=1
STOPPING=1
break
fi
done
[[ "${STOPPING}" == "0" ]] || break
sleep 2 &
sleeper=$!
wait "${sleeper}" 2>/dev/null || kill "${sleeper}" 2>/dev/null || true
done
echo "$(date '+%F %T') shutting down"
stop_children
exit "${rc}"
}
# ------------------------------------------------------------------ lifecycle
cmd_start() {
need_root start; need_install
if svc_running "${PREFIX}" "${NETX_DATA}" && netx_admin health "$(health_url)"; then
ok "$(t "NetX 已在运行" "NetX already running"): http://$(primary_ip):$(api_port)/"
return 0
fi
info "$(t "启动 NetX" "Starting NetX")"
svc_start "${PREFIX}" "${NETX_DATA}"
wait_healthy 120
ok "$(t "已启动" "Started"): http://$(primary_ip):$(api_port)/"
}
cmd_stop() {
need_root stop; need_install
info "$(t "停止 NetX" "Stopping NetX")"
svc_stop "${PREFIX}" "${NETX_DATA}"
ok "$(t "已停止" "Stopped")"
}
cmd_restart() { need_root restart; cmd_stop; cmd_start; }
cmd_status() {
need_install
local state="stopped" svc="nohup"
svc_unit_installed && svc="systemd"
if svc_running "${PREFIX}" "${NETX_DATA}"; then state="running"; fi
local health="down"
netx_admin health "$(health_url)" && health="ok"
echo "version : $(cur_version)"
echo "service : ${svc} (${state})"
echo "health : ${health} $(health_url)"
echo "listen : $(api_bind):$(api_port)"
echo "db mode : $(db_mode)$( [[ "$(db_mode)" == bundled ]] && echo " (127.0.0.1:$(pg_port))")"
echo "prefix : ${PREFIX}"
echo "data : ${NETX_DATA}"
if have_systemd && systemctl list-unit-files netx-update.timer >/dev/null 2>&1 && systemctl is-enabled --quiet netx-update.timer 2>/dev/null; then
echo "auto-update : on"
elif [[ -f /etc/cron.d/netx-update ]]; then
echo "auto-update : on (cron)"
else
echo "auto-update : off"
fi
[[ "${health}" == "ok" ]]
}
cmd_logs() {
local name="api" follow=0 lines=200
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--follow) follow=1; shift ;;
-n) lines="${2:-200}"; shift 2 ;;
*) name="$1"; shift ;;
esac
done
local f="${NETX_LOG_DIR}/${name}.log"
[[ -f "${f}" ]] || die "log_not_found: ${f}"
if [[ "${follow}" == "1" ]]; then tail -n "${lines}" -F "${f}"; else tail -n "${lines}" "${f}"; fi
}
cmd_version() { cur_version; }
cmd_admin_password() {
need_root admin-password; need_install
print_admin_login "${ENV_FILE}"
}
# ------------------------------------------------------------------ ports
# apply_env_change <description> KEY=VAL... ; restarts if running, rolls back on failure.
apply_env_change() {
local what="$1"
shift
local was=0
svc_running "${PREFIX}" "${NETX_DATA}" && was=1
cp -p "${ENV_FILE}" "${ENV_FILE}.bak"
[[ "${was}" == "1" ]] && svc_stop "${PREFIX}" "${NETX_DATA}"
if [[ -n "${PG_CONF_CHANGE:-}" ]]; then
cp -p "${PGDATA_DIR}/postgresql.conf" "${PGDATA_DIR}/postgresql.conf.bak"
netx_admin pg-conf "${PGDATA_DIR}/postgresql.conf" --set "${PG_CONF_CHANGE}"
fi
netx_admin env-set "${ENV_FILE}" "$@"
if [[ "${was}" == "1" ]]; then
svc_start "${PREFIX}" "${NETX_DATA}"
if ! wait_healthy 120; then
warn "$(t "启动失败,恢复原配置" "start failed; restoring previous settings")"
svc_stop "${PREFIX}" "${NETX_DATA}"
mv -f "${ENV_FILE}.bak" "${ENV_FILE}"
[[ -n "${PG_CONF_CHANGE:-}" ]] && mv -f "${PGDATA_DIR}/postgresql.conf.bak" "${PGDATA_DIR}/postgresql.conf"
svc_start "${PREFIX}" "${NETX_DATA}"
die "${what}_failed"
fi
fi
rm -f "${ENV_FILE}.bak" "${PGDATA_DIR}/postgresql.conf.bak"
return 0
}
firewall_hint() {
local p="$1"
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
echo " $(t "防火墙放行" "open firewall"): firewall-cmd --permanent --add-port=${p}/tcp && firewall-cmd --reload"
elif command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then
echo " $(t "防火墙放行" "open firewall"): ufw allow ${p}/tcp"
fi
}
cmd_set_port() {
need_root set-port; need_install
local p
p="$(netx_admin valid-port "${1:-}")" || exit 1
[[ "${p}" == "$(api_port)" ]] && { ok "$(t "端口未变" "port unchanged"): ${p}"; return 0; }
netx_admin port-free "${p}" --host "$(api_bind)" || die "port_in_use: ${p}"
apply_env_change set_port "NETX_PORT=${p}"
ok "$(t "Web/API 端口已改为" "Web/API port is now") ${p}: http://$(primary_ip):${p}/"
firewall_hint "${p}"
}
cmd_set_bind() {
need_root set-bind; need_install
local h
[[ -n "${1:-}" ]] || die "usage: netxctl set-bind <0.0.0.0|127.0.0.1|ip>"
h="$(netx_admin norm-bind -- "$1")" || exit 1
local pairs=("NETX_HOST=${h}")
# Same prerequisites NetX enforces for a non-loopback bind (security_bootstrap.py).
if ! is_loopback "${h}"; then
[[ -n "$(cfg NETX_BOOTSTRAP_ADMIN_PASSWORD)" && "$(cfg NETX_BOOTSTRAP_ADMIN_PASSWORD)" != "admin123" ]] ||
pairs+=("NETX_BOOTSTRAP_ADMIN_PASSWORD=$(netx_admin gen-secret password)")
[[ -n "$(cfg NETX_UME_VERIFY_TLS)" ]] || pairs+=("NETX_UME_VERIFY_TLS=false")
[[ -n "$(cfg NETX_ALLOW_INSECURE_DEFAULTS)" ]] || pairs+=("NETX_ALLOW_INSECURE_DEFAULTS=1")
fi
apply_env_change set_bind "${pairs[@]}"
ok "$(t "监听地址已改为" "listen address is now") ${h}:$(api_port)"
}
cmd_set_pg_port() {
need_root set-pg-port; need_install
[[ "$(db_mode)" == "bundled" ]] || die "$(t "当前是外置数据库,请用 netxctl reconfigure-db" "external database in use; run netxctl reconfigure-db")"
local p old url
p="$(netx_admin valid-port "${1:-}")" || exit 1
old="$(pg_port)"
[[ "${p}" == "${old}" ]] && { ok "$(t "端口未变" "port unchanged"): ${p}"; return 0; }
netx_admin port-free "${p}" --host 127.0.0.1 || die "port_in_use: 127.0.0.1:${p}"
url="$(netx_admin url-port "$(cfg NETX_DATABASE_URL)" "${p}")"
PG_CONF_CHANGE="port=${p}" apply_env_change set_pg_port "NETX_BUNDLED_PG_PORT=${p}" "NETX_DATABASE_URL=${url}"
ok "$(t "内置 PostgreSQL 端口已改为" "bundled PostgreSQL port is now") 127.0.0.1:${p}"
}
cmd_reconfigure_db() {
need_root reconfigure-db; need_install
exec "${CUR}/install.sh" --mode reinstall --prefix "${PREFIX}" --data "${NETX_DATA}" "$@"
}
# ------------------------------------------------------------------ update
cmd_check_update() {
need_install
local out
out="$(netx_admin check-update --env "${ENV_FILE}" --prefix "${PREFIX}" --arch "${ARCH}")" || exit $?
local f="${NETX_DATA}/updates/latest.json"
mkdir -p "${NETX_DATA}/updates"
printf '%s\n' "${out}" >"${f}"
local latest avail src
latest="$(netx_admin json-get "${f}" latest)"
avail="$(netx_admin json-get "${f}" update_available)"
src="$(netx_admin json-get "${f}" source)"
echo "$(t "当前版本" "current"): $(cur_version)"
echo "$(t "最新版本" "latest") : ${latest} (${src})"
if [[ "${avail}" == "True" ]]; then
ok "$(t "有新版本,执行 sudo netxctl update 升级" "update available: sudo netxctl update")"
return 0
fi
ok "$(t "已是最新版本" "already up to date")"
}
cmd_update() {
need_root update; need_install
local yes=0 file="" force=0 quiet=0
while [[ $# -gt 0 ]]; do
case "$1" in
-y|--yes) yes=1; shift ;;
--file) file="${2:-}"; shift 2 ;;
--force) force=1; shift ;;
--quiet) quiet=1; shift ;;
*) die "unknown option: $1" ;;
esac
done
local work="${NETX_DATA}/updates/work" pkg
if [[ -n "${file}" ]]; then
pkg="$(netx_admin extract "$(readlink -f "${file}")" --workdir "${work}" --arch "${ARCH}")" || exit 1
else
local f="${NETX_DATA}/updates/latest.json" out
mkdir -p "${NETX_DATA}/updates"
if [[ "${quiet}" == "1" ]]; then
out="$(netx_admin check-update --env "${ENV_FILE}" --prefix "${PREFIX}" --arch "${ARCH}" --quiet)" || exit $?
else
out="$(netx_admin check-update --env "${ENV_FILE}" --prefix "${PREFIX}" --arch "${ARCH}")" || exit $?
fi
printf '%s\n' "${out}" >"${f}"
if [[ "$(netx_admin json-get "${f}" update_available)" != "True" && "${force}" != "1" ]]; then
[[ "${quiet}" == "1" ]] || ok "$(t "已是最新版本" "already up to date") ($(cur_version))"
return 0
fi
local latest
latest="$(netx_admin json-get "${f}" latest)"
if [[ "${yes}" != "1" ]]; then
local ans
read -r -p "$(t "升级 $(cur_version) → ${latest}?[y/N] " "Update $(cur_version) -> ${latest}? [y/N] ")" ans
[[ "${ans}" =~ ^[Yy] ]] || { echo "$(t "已取消" "cancelled")"; return 1; }
fi
pkg="$(netx_admin fetch-update --env "${ENV_FILE}" --info "${out}" --workdir "${work}" --arch "${ARCH}")" || exit 1
fi
local newver cmp
newver="$(netx_admin json-get "${pkg}/version.json" version)"
cmp="$(netx_admin ver-cmp "${newver}" "$(cur_version)")"
if [[ "${cmp}" != "1" && "${force}" != "1" ]]; then
rm -rf "${work}"
die "$(t "包版本 ${newver} 不高于当前 $(cur_version);如确需重装加 --force" "package ${newver} is not newer than $(cur_version); use --force to reinstall it")"
fi
local rc=0
"${pkg}/install.sh" --mode update --yes --prefix "${PREFIX}" --data "${NETX_DATA}" || rc=$?
rm -rf "${work}"
return "${rc}"
}
cmd_auto_update() {
need_root auto-update; need_install
case "${1:-}" in
on)
if have_systemd; then
sed -e "s#@PREFIX@#${PREFIX}#g" "${CUR}/packaging/linux/systemd/netx-update.service.in" >/etc/systemd/system/netx-update.service
cp "${CUR}/packaging/linux/systemd/netx-update.timer" /etc/systemd/system/netx-update.timer
systemctl daemon-reload
systemctl enable --now netx-update.timer
elif [[ -d /etc/cron.d ]]; then
printf '30 3 * * * root %s update --yes --quiet >>%s/logs/update.log 2>&1\n' "${PREFIX}/current/packaging/linux/netxctl" "${NETX_DATA}" >/etc/cron.d/netx-update
else
die "$(t "系统既没有 systemd 也没有 cron,无法自动更新" "neither systemd nor cron available")"
fi
ok "$(t "已开启每日自动更新" "daily auto-update enabled")"
;;
off)
if have_systemd; then
systemctl disable --now netx-update.timer 2>/dev/null || true
rm -f /etc/systemd/system/netx-update.timer /etc/systemd/system/netx-update.service
systemctl daemon-reload
fi
rm -f /etc/cron.d/netx-update
ok "$(t "已关闭自动更新" "auto-update disabled")"
;;
*) die "usage: netxctl auto-update on|off" ;;
esac
}
# ------------------------------------------------------------------ backup / uninstall
cmd_backup() {
need_root backup; need_install
local out="${1:-${NETX_DATA}/backups/netx-backup-$(date +%Y%m%d-%H%M%S).tar.gz}"
local tmp started=0
tmp="$(mktemp -d)"
chmod 0755 "${tmp}"
chown "${NETX_USER}:${NETX_USER}" "${tmp}"
local url
url="$(cfg NETX_DATABASE_URL)"
if [[ "$(db_mode)" == "bundled" ]] && ! pg_is_running; then
pg_start && started=1
fi
info "pg_dump → ${out}"
if ! as_netx env PGPASSWORD="$(netx_admin url-part "${url}" password)" LC_ALL=C "${PGBIN}/pg_dump" -Fc \
-h "$(netx_admin url-part "${url}" host)" -p "$(netx_admin url-part "${url}" port)" \
-U "$(netx_admin url-part "${url}" user)" -d "$(netx_admin url-part "${url}" db)" -f "${tmp}/netx.dump"; then
[[ "${started}" == "1" ]] && pg_stop
rm -rf "${tmp}"
die "pg_dump_failed"
fi
[[ "${started}" == "1" ]] && pg_stop
cp -p "${ENV_FILE}" "${tmp}/env"
[[ -d "${NETX_DATA}/data/auth" ]] && cp -rp "${NETX_DATA}/data/auth" "${tmp}/auth"
netx_admin tar-create "${out}" "${tmp}"
rm -rf "${tmp}"
ok "$(t "备份完成(含 .env 与密钥,请妥善保管)" "backup written (contains .env and secrets)"): ${out}"
}
cmd_uninstall() {
need_root uninstall
local purge=0 yes=0
while [[ $# -gt 0 ]]; do
case "$1" in
--purge) purge=1; shift ;;
-y|--yes) yes=1; shift ;;
*) die "unknown option: $1" ;;
esac
done
if [[ "${yes}" != "1" ]]; then
local ans
if [[ "${purge}" == "1" ]]; then
read -r -p "$(t "将删除程序和全部数据(${NETX_DATA}),输入 DELETE 确认: " "This removes NetX AND all data in ${NETX_DATA}. Type DELETE: ")" ans
[[ "${ans}" == "DELETE" ]] || { echo "$(t "已取消" "cancelled")"; return 1; }
else
read -r -p "$(t "卸载程序(保留 ${NETX_DATA})?[y/N] " "Uninstall NetX (keep ${NETX_DATA})? [y/N] ")" ans
[[ "${ans}" =~ ^[Yy] ]] || { echo "$(t "已取消" "cancelled")"; return 1; }
fi
fi
[[ -x "${NETX_PY}" ]] && svc_stop "${PREFIX}" "${NETX_DATA}"
if have_systemd; then
systemctl disable --now netx-update.timer 2>/dev/null || true
systemctl disable netx.service 2>/dev/null || true
rm -f /etc/systemd/system/netx.service /etc/systemd/system/netx-update.service /etc/systemd/system/netx-update.timer
systemctl daemon-reload || true
fi
rm -f /etc/cron.d/netx-update /etc/logrotate.d/netx
local link
for link in /usr/bin/netxctl /usr/local/bin/netxctl; do
if [[ -L "${link}" && "$(readlink "${link}")" == "${PREFIX}"/* ]]; then rm -f "${link}"; fi
done
# Run from a copy-safe place: this script lives under PREFIX.
cd /
rm -rf "${PREFIX:?}/releases" "${PREFIX:?}/current" "${PREFIX:?}/netx.conf"
rmdir "${PREFIX}" 2>/dev/null || true
if [[ "${purge}" == "1" ]]; then
rm -rf "${NETX_DATA:?}"
if id "${NETX_USER}" >/dev/null 2>&1; then
userdel "${NETX_USER}" 2>/dev/null || deluser "${NETX_USER}" 2>/dev/null || true
fi
ok "$(t "已卸载并删除全部数据" "NetX and all data removed")"
else
ok "$(t "已卸载,数据保留在" "NetX removed; data kept in") ${NETX_DATA}"
fi
}
# ------------------------------------------------------------------ main
cmd="${1:-}"
[[ $# -gt 0 ]] && shift
case "${cmd}" in
run) cmd_run ;;
start) cmd_start ;;
stop) cmd_stop ;;
restart) cmd_restart ;;
status) cmd_status ;;
logs) cmd_logs "$@" ;;
set-port) cmd_set_port "$@" ;;
set-bind) cmd_set_bind "$@" ;;
set-pg-port) cmd_set_pg_port "$@" ;;
reconfigure-db) cmd_reconfigure_db "$@" ;;
check-update) cmd_check_update ;;
update) cmd_update "$@" ;;
auto-update) cmd_auto_update "$@" ;;
backup) cmd_backup "$@" ;;
uninstall) cmd_uninstall "$@" ;;
version|--version|-V) cmd_version ;;
admin-password) cmd_admin_password ;;
""|-h|--help|help) usage ;;
*) err "unknown command: ${cmd}"; usage; exit 2 ;;
esac