#!/usr/bin/env bash
# =====================================================================
# Установка и обновление HopeDC на сервере (Ubuntu/Debian 22.04+, RHEL‑совместимые,
# Astra Linux SE 1.8). Запуск на самом сервере:
#   sudo bash install_hopedc.sh [путь_к_hopedc-core_vNNN.gz | hopedc-core]
# Если путь не указан — ищет hopedc-core_vNNN.gz (самую новую), hopedc-core.gz или
# hopedc-core рядом со скриптом, затем в /tmp.
#
# Режимы:
#   • ПЕРВАЯ УСТАНОВКА (службы hopedc ещё нет): пользователь hopedc (на Astra — root),
#     /opt/hopedc/hopedc-core, данные /var/lib/hopedc, ключ /etc/hopedc/master.key,
#     systemd‑юнит hopedc.service, порты в файрволе, служба обновлений из панели.
#   • ОБНОВЛЕНИЕ (служба уже есть): юнит, каталог данных, пользователь и ключ НЕ меняются;
#     заменяется бинарник (прежний остаётся рядом: .prev и .bak.<время>), дополняются
#     недостающие настройки, служба перезапускается. Данные и ключ сохраняются.
#
# Параметры (переменные окружения перед командой, например: DC_SELF_IP=10.0.0.5 sudo -E bash …):
#   DC_SELF_IP          IP этого контроллера (по умолчанию — адрес основного интерфейса)
#   DATA_DIR            каталог данных при первой установке (по умолчанию /var/lib/hopedc)
#   UI_PORT / API_PORT  веб‑панель / API и репликация (по умолчанию 9443 / 9444)
#   JOIN_CLUSTER=1      новый узел кластера: не создавать демо‑домен, получить данные от пира
#   HOSTED_DOMAINS=…    региональный сервер: домены, которые держит узел (через запятую)
#   KEEP_TIMESYNC=1     не отключать chrony/systemd-timesyncd (по умолчанию время держит HopeDC)
#   KEEP_RESOLVED=1     не отключать заглушку systemd-resolved на :53 (нужна для DNS домена)
#   NO_FIREWALL=1       не открывать порты в ufw/firewalld
#   JOIN_CONFIG=файл    параметры присоединения к кластеру (v188, раздел «Контроллеры» панели):
#                       передаются службе и сразу удаляются
# Журнал установки: /var/log/hopedc-install.log
# =====================================================================
set -u
umask 022
LOG=/var/log/hopedc-install.log
exec > >(tee -a "$LOG") 2>&1
echo; echo "===== HopeDC install $(date '+%F %T') ====="

[ "$(id -u)" = "0" ] || { echo "!!! Запустите от root: sudo bash $0 …"; exit 1; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SVC=hopedc
UNIT=/etc/systemd/system/hopedc.service
ASTRA=0; [ -f /etc/astra_version ] && ASTRA=1
sec() { echo; echo "== $* =="; }
die() { echo "!!! $*"; echo "Служба не изменена. Журнал: $LOG"; exit 1; }
sc() { timeout 60 systemctl "$@" </dev/null; }

# ---------------------------------------------------------------- 1. пакет
sec "1. пакет"
SRC="${1:-}"
if [ -z "$SRC" ]; then
  for d in "$SCRIPT_DIR" /tmp; do
    # Архив .gz ищем ПЕРВЫМ (самую новую версию), иначе старый распакованный файл
    # от прежнего запуска установился бы вместо только что доставленного архива.
    SRC="$(ls -1 "$d"/hopedc-core_v*.gz 2>/dev/null | sort -V | tail -1)"
    [ -z "$SRC" ] && for f in "$d/hopedc-core.gz" "$d/hopedc-core"; do [ -f "$f" ] && { SRC="$f"; break; }; done
    [ -n "$SRC" ] && break
  done
fi
[ -n "$SRC" ] && [ -f "$SRC" ] || die "Не найден пакет hopedc-core_vNNN.gz. Положите его рядом со скриптом или в /tmp, либо укажите путь: sudo bash $0 /path/hopedc-core_vNNN.gz"
WORK="$(mktemp -d /var/tmp/hopedc-inst.XXXXXX)" || die "не создать временный каталог"
chmod 0755 "$WORK"
trap 'rm -rf "$WORK"' EXIT
NEW="$WORK/hopedc-core"
case "$SRC" in
  *.gz) gunzip -c "$SRC" > "$NEW" || die "не удалось распаковать $SRC" ;;
  *)    cp "$SRC" "$NEW" || die "не удалось скопировать $SRC" ;;
esac
chmod 0755 "$NEW"
echo "файл: $SRC"
echo "sha256: $(sha256sum "$NEW" | cut -d' ' -f1)   md5: $(md5sum "$NEW" | cut -d' ' -f1)"
[ "$(head -c4 "$NEW" | od -An -tx1 | tr -d ' \n')" = "7f454c46" ] || die "это не исполняемый файл Linux"
command -v file >/dev/null && file -b "$NEW" | cut -c1-100
# v200: один бинарник для всех дистрибутивов; нужен glibc 2.30+ (Debian 11+, Ubuntu 20.04+, RHEL/Alma/Rocky 9+, Astra 1.8)
SYSGLIBC=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$')
if ldd "$NEW" 2>&1 | grep -q "GLIBC_[0-9.]*' not found"; then
  die "в системе glibc ${SYSGLIBC:-?}, а HopeDC нужен glibc 2.30 или новее (Debian 11+, Ubuntu 20.04+, RHEL/AlmaLinux/Rocky 9+, Astra Linux SE 1.8). RHEL/CentOS 8, CentOS 7, Debian 10 и Astra 1.7 не поддерживаются"
fi
if ldd "$NEW" 2>&1 | grep -q "not found"; then ldd "$NEW"; die "не хватает системных библиотек (см. ldd выше)"; fi
# Только сборки v147+ понимают --version; старая на --version запустилась бы целиком как контроллер.
grep -qaF HOPEDC_SUPPORTS_VERSION_PROBE_1 "$NEW" || die "пакет старше v147 — этот установщик ставит HopeDC v147 и новее (старые версии — прежними скриптами развёртывания)"

# ---------------------------------------------------------------- 2. режим
sec "2. режим"
if [ -f "$UNIT" ]; then
  MODE=upgrade
  SU=$(systemctl show -p User --value $SVC 2>/dev/null); [ -z "$SU" ] && SU=root
  ENVS=$(systemctl show -p Environment --value $SVC 2>/dev/null | tr ' ' '\n')
  DD=$(printf '%s\n' "$ENVS" | sed -n 's/^DC_DATA_DIR=//p' | tail -1); [ -z "$DD" ] && DD=/var/lib/hopedc
  BIN=$(systemctl show -p ExecStart --value $SVC 2>/dev/null | sed -n 's/.*path=\([^ ;]*\).*/\1/p' | head -1); [ -z "$BIN" ] && BIN=/opt/hopedc/hopedc-core
  API_PORT=$(printf '%s\n' "$ENVS" | sed -n 's/^DC_API_PORT=//p' | tail -1); API_PORT="${API_PORT:-9444}"
  UI_PORT=$(printf '%s\n' "$ENVS" | sed -n 's/^DC_UI_PORT=//p' | tail -1); UI_PORT="${UI_PORT:-9443}"
  echo "ОБНОВЛЕНИЕ существующей установки: служба $SVC (пользователь $SU), бинарник $BIN, данные $DD"
  # --version у прежнего бинарника спрашиваем, только если он его понимает (v147+).
  if [ -f "$BIN" ] && grep -qaF HOPEDC_SUPPORTS_VERSION_PROBE_1 "$BIN"; then echo "текущая версия: $(timeout 10 "$BIN" --version 2>/dev/null </dev/null)"
  elif [ -f "$BIN" ]; then echo "текущая версия: до v147"; fi
else
  MODE=install
  if [ "$ASTRA" = "1" ]; then SU=root; else SU=hopedc; fi
  DD="${DATA_DIR:-/var/lib/hopedc}"
  BIN=/opt/hopedc/hopedc-core
  API_PORT="${API_PORT:-9444}"; UI_PORT="${UI_PORT:-9443}"
  echo "ПЕРВАЯ УСТАНОВКА: пользователь службы $SU, бинарник $BIN, данные $DD$([ "$ASTRA" = 1 ] && echo ', Astra Linux (МКЦ)')"
fi
SG=$(id -gn "$SU" 2>/dev/null || echo "$SU")
IPSELF="${DC_SELF_IP:-}"
[ -z "$IPSELF" ] && IPSELF=$(ip -4 route get 1.1.1.1 2>/dev/null | sed -n 's/.*src \([0-9.]*\).*/\1/p' | head -1)
[ -z "$IPSELF" ] && IPSELF=$(hostname -I 2>/dev/null | awk '{print $1}')
echo "IP контроллера: ${IPSELF:-не определён}"

if [ "$ASTRA" = "1" ]; then
  echo "Astra: $(cat /etc/astra_version 2>/dev/null); целостность сессии: $(pdp-id -i 2>/dev/null || echo ?)"
  [ "$(pdp-id -i 2>/dev/null)" = "63" ] || echo "(!) сессия не на высоком уровне целостности (63) — метки МКЦ не будут заданы; войдите на высоком уровне"
  grep -q '^DIGSIG_ELF_MODE=1' /etc/digsig/digsig_initramfs.conf 2>/dev/null && echo "(!) ЗПС в строгом режиме: бинарник должен быть подписан ключом, заверенным ГК «Астра»"
fi

# ---------------------------------------------------------------- 3. пробный запуск
sec "3. пробный запуск новой версии"
if [ "$SU" = "root" ] || ! id "$SU" >/dev/null 2>&1; then
  VER_OUT="$(cd /var/tmp && timeout 15 env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin "$NEW" --version 2>&1 </dev/null)"; RC=$?
else
  VER_OUT="$(cd /var/tmp && timeout 15 runuser -u "$SU" -- env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin "$NEW" --version 2>&1 </dev/null)"; RC=$?
fi
[ $RC -eq 0 ] || die "новая версия не запускается на этом сервере (код $RC): $VER_OUT"
echo "$VER_OUT"

# ---------------------------------------------------------------- 4. каталоги, пользователь, ключ
sec "4. пользователь, каталоги, мастер‑ключ"
if [ "$MODE" = "install" ] && [ "$SU" != "root" ]; then
  id "$SU" >/dev/null 2>&1 || useradd --system --home-dir "$DD" --shell /usr/sbin/nologin "$SU"
  SG=$(id -gn "$SU")
fi
LOGD=/var/log/hopedc; [ "$ASTRA" = "1" ] && LOGD="$DD/logs"
install -d -m 0755 "$(dirname "$BIN")"
if [ "$MODE" = "install" ]; then
  install -d -m 0750 -o "$SU" -g "$SG" "$DD" "$LOGD"
fi
install -d -m 0700 -o "$SU" -g "$SG" /etc/hopedc
if [ -s /etc/hopedc/master.key ]; then
  echo "мастер‑ключ: /etc/hopedc/master.key (есть)"
else
  MOVED=0
  for F in "$DD.keys/master.key" "$DD/.master-key/master.key"; do
    if [ $MOVED = 0 ] && [ -s "$F" ] && [ ! -L "$F" ]; then cp -a "$F" /etc/hopedc/master.key && rm -f "$F" && MOVED=1 && echo "мастер‑ключ перенесён из $F"; fi
  done
  if [ $MOVED = 0 ]; then
    ( umask 077; head -c 32 /dev/urandom | od -An -tx1 -v | tr -d ' \n' > /etc/hopedc/master.key )
    echo "создан мастер‑ключ /etc/hopedc/master.key"
  fi
fi
chown "$SU:$SG" /etc/hopedc/master.key && chmod 0600 /etc/hopedc/master.key
if [ "$ASTRA" = "1" ]; then
  for d in "$(dirname "$BIN")" /etc/hopedc "$DD" "$LOGD"; do pdpl-file 0:63:0:iinh "$d" >/dev/null 2>&1 || echo "(!) метка МКЦ не задана: $d"; done
  pdpl-file 0:63:0:0 /etc/hopedc/master.key >/dev/null 2>&1 || true
  if [ -f /etc/parsec/fs-ilev.conf ]; then
    for d in "$DD" /etc/hopedc "$(dirname "$BIN")"; do grep -qE "^max[[:space:]]+$d\$" /etc/parsec/fs-ilev.conf || printf 'max\t%s\n' "$d" >> /etc/parsec/fs-ilev.conf; done
  fi
fi

# ---------------------------------------------------------------- 5. юнит systemd
sec "5. служба systemd"
if [ "$MODE" = "install" ]; then
  EXTRA=""
  [ "${JOIN_CLUSTER:-0}" = "1" ] && EXTRA="${EXTRA}Environment=DC_JOIN_CLUSTER=1
"
  [ -n "${HOSTED_DOMAINS:-}" ] && EXTRA="${EXTRA}Environment=DC_HOSTED_DOMAINS=${HOSTED_DOMAINS}
"
  if [ "$SU" = "root" ]; then USERLINES="User=root"; CAPS="# служба работает от root (Astra: метки МКЦ)"
  else USERLINES="User=$SU
Group=$SG"; CAPS="AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_SYS_TIME
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SYS_TIME
NoNewPrivileges=true"; fi
  cat > "$UNIT" <<UNITEOF
[Unit]
Description=HopeDC Core (Domain Controller)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
$USERLINES
WorkingDirectory=$DD
Environment=DC_SELF_IP=$IPSELF
Environment=DC_DATA_DIR=$DD
Environment=DC_LOG_DIR=$LOGD
Environment=DC_MASTER_KEY_FILE=/etc/hopedc/master.key
Environment=DC_UI_PORT=$UI_PORT
Environment=DC_API_PORT=$API_PORT
Environment=DC_WEB_EXTRA_SAN=$IPSELF
Environment=DC_WAL_COMPACT_MB=64
${EXTRA}ExecStart=$BIN
Restart=on-failure
RestartSec=5
TimeoutStartSec=180
LimitNOFILE=65536
# Порты <1024 (53, 88, 389, 445…) и подстройка системных часов встроенной службой NTP.
$CAPS

[Install]
WantedBy=multi-user.target
UNITEOF
  chmod 0644 "$UNIT"
  echo "создан $UNIT"
else
  # Обновление: юнит не трогаем; дополняем только отсутствующие настройки (как скрипты развёртывания).
  install -d -m 0755 /etc/systemd/system/hopedc.service.d
  if ! systemctl show -p Environment --value $SVC 2>/dev/null | grep -q 'DC_UI_PORT='; then
    printf '[Service]\nEnvironment=DC_UI_PORT=%s\nEnvironment=DC_API_PORT=%s\nEnvironment=DC_WEB_EXTRA_SAN=%s\n' "$UI_PORT" "$API_PORT" "$IPSELF" > /etc/systemd/system/hopedc.service.d/10-https.conf
    echo "добавлено: hopedc.service.d/10-https.conf (панель :$UI_PORT, API :$API_PORT)"
  fi
  if [ "$SU" != "root" ] && ! systemctl show -p AmbientCapabilities --value $SVC 2>/dev/null | grep -qi sys_time; then
    printf '[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE CAP_SYS_TIME\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SYS_TIME\n' > /etc/systemd/system/hopedc.service.d/20-ntp.conf
    echo "добавлено: hopedc.service.d/20-ntp.conf (CAP_SYS_TIME для службы NTP)"
  fi
  echo "юнит сохранён без изменений: $UNIT"
fi
sc daemon-reload

# ---------------------------------------------------------------- 6. бинарник
sec "6. установка бинарника"
sc stop $SVC 2>/dev/null || true
TS=$(date +%s)
if [ -f "$BIN" ]; then
  cp -a "$BIN" "$BIN.bak.$TS" && cp -a "$BIN" "$BIN.prev" && echo "прежний бинарник: $BIN.prev (и $BIN.bak.$TS)"
  [ "$ASTRA" = "1" ] && { pdpl-file 0:63:0:0 "$BIN.prev" >/dev/null 2>&1; pdpl-file 0:63:0:0 "$BIN.bak.$TS" >/dev/null 2>&1; }
  ls -1t "$BIN".bak.* 2>/dev/null | tail -n +4 | xargs -r rm -f
fi
install -m 0755 "$NEW" "$BIN.new" || die "не удалось записать $BIN.new"
[ "$ASTRA" = "1" ] && { pdpl-file 0:63:0:0 "$BIN.new" >/dev/null 2>&1 || echo "(!) метка МКЦ бинарника не задана"; }
mv -f "$BIN.new" "$BIN"
"$BIN" --eula > "$(dirname "$BIN")/EULA.txt" 2>/dev/null && chmod 0644 "$(dirname "$BIN")/EULA.txt"
rm -f "$(dirname "$BIN")/THIRD_PARTY_NOTICES.txt"
echo "установлено: $BIN ($("$BIN" --version 2>/dev/null))"
if [ -n "${JOIN_CONFIG:-}" ] && [ -f "$JOIN_CONFIG" ]; then
  install -m 0600 -o "$SU" -g "$SG" "$JOIN_CONFIG" "$DD/cluster_join.json" && echo "присоединение к кластеру: параметры переданы службе"
  JOINED=1
  rm -f "$JOIN_CONFIG"
fi

# ---------------------------------------------------------------- 7. система: время, DNS, файрвол
sec "7. время, DNS, файрвол"
if [ "${KEEP_TIMESYNC:-0}" != "1" ]; then
  # Часы контроллера держит встроенная служба NTP HopeDC — две службы не должны бороться за часы.
  sc disable --now chrony chronyd systemd-timesyncd ntp ntpsec >/dev/null 2>&1 || true
  timedatectl set-ntp false >/dev/null 2>&1 || true
  echo "системные службы времени отключены (время домена — HopeDC → NTP Сервер; KEEP_TIMESYNC=1 — оставить)"
fi
if [ "${KEEP_RESOLVED:-0}" != "1" ] && ss -lun 2>/dev/null | grep -qE '127\.0\.0\.5[34][^ ]*:53 '; then
  # Заглушка systemd-resolved занимает :53 — DNS домена HopeDC не поднимется. Отключаем только
  # заглушку (сам resolved и разрешение имён на сервере продолжают работать).
  install -d -m 0755 /etc/systemd/resolved.conf.d
  printf '[Resolve]\nDNSStubListener=no\n' > /etc/systemd/resolved.conf.d/hopedc.conf
  [ -L /etc/resolv.conf ] && [ -f /run/systemd/resolve/resolv.conf ] && ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
  sc restart systemd-resolved >/dev/null 2>&1 || true
  echo "заглушка systemd-resolved на :53 отключена (KEEP_RESOLVED=1 — не трогать)"
fi
systemctl is-active --quiet smbd 2>/dev/null && echo "(!) работает Samba (smbd) — порт 445 занят, SMB/SYSVOL HopeDC не поднимется: остановите smbd, если сервер — контроллер HopeDC"
if [ "${NO_FIREWALL:-0}" != "1" ]; then
  PORTS="53/udp 53/tcp 88/tcp 88/udp 464/tcp 464/udp 123/udp 135/tcp 49152/tcp 389/tcp 389/udp 636/tcp 445/tcp 67/udp 3268/tcp 3269/tcp ${UI_PORT}/tcp ${API_PORT}/tcp 80/tcp 4460/tcp"
  if command -v ufw >/dev/null 2>&1; then for p in $PORTS; do ufw allow "$p" >/dev/null 2>&1 || true; done; echo "ufw: открыты $PORTS"; fi
  if command -v firewall-cmd >/dev/null 2>&1; then for p in $PORTS; do firewall-cmd --permanent --add-port="$p" >/dev/null 2>&1 || true; done; firewall-cmd --reload >/dev/null 2>&1 || true; echo "firewalld: открыты $PORTS"; fi
fi

# ---------------------------------------------------------------- 8. обновления из панели
sec "8. обновления из панели (hopedc-update)"
cat > "$WORK/upd.sh" <<'HOPEDC_UPDATER_EOF'
#!/bin/bash
# HopeDC v147: установка механизма обновлений из панели (запускать от root).
# Служба (без root) готовит пакет в <данные>/updates/staged и кладёт apply.request;
# path-юнит hopedc-update.path запускает /usr/local/bin/hopedc-update-apply, который
# проверяет пакет, сохраняет текущий бинарник, ставит новый, перезапускает службу и
# при неудаче откатывает прежнюю версию. Идемпотентно — можно запускать повторно.
set -u
SVC=hopedc
SU=$(systemctl show -p User --value $SVC 2>/dev/null); [ -z "$SU" ] && SU=root
SG=$(id -gn "$SU" 2>/dev/null || echo "$SU")
ENVS=$(systemctl show -p Environment --value $SVC 2>/dev/null | tr ' ' '\n')
DD=$(printf '%s\n' "$ENVS" | sed -n 's/^DC_DATA_DIR=//p' | tail -1); [ -z "$DD" ] && DD=/var/lib/hopedc
PORT=$(printf '%s\n' "$ENVS" | sed -n 's/^DC_API_PORT=//p' | tail -1); [ -z "$PORT" ] && PORT=9444
BIN=$(systemctl show -p ExecStart --value $SVC 2>/dev/null | sed -n 's/.*path=\([^ ;]*\).*/\1/p' | head -1); [ -z "$BIN" ] && BIN=/opt/hopedc/hopedc-core
case "$SU$SG$DD$PORT$BIN" in *[!A-Za-z0-9_./-]*) echo "!!! необычные значения службы ($SU $SG $DD $PORT $BIN) — механизм обновлений не установлен"; exit 0;; esac
UPD="$DD/updates"
install -d -m 0750 -o "$SU" -g "$SG" "$UPD" "$UPD/staged"
# Astra (МКЦ): каталоги пакетов — с высокой целостностью, как весь каталог данных HopeDC.
if command -v pdpl-file >/dev/null 2>&1; then pdpl-file 0:63:0:iinh "$UPD" >/dev/null 2>&1; pdpl-file 0:63:0:iinh "$UPD/staged" >/dev/null 2>&1; fi

cat > /usr/local/bin/hopedc-update-apply <<'APPLY'
#!/bin/bash
# Применение обновления HopeDC. Запускается root-овым юнитом hopedc-update.service,
# когда служба кладёт запрос apply.request (раздел панели «Обновления»).
#
# Службе не доверяем: всё из её каталога копируется к себе БЕЗ разыменования ссылок и
# проверяется уже в копии. Новый бинарник пробно запускается от имени пользователя службы.
set -u
DATA="${HOPEDC_UPD_DATA:-__UPD__}"
BIN="${HOPEDC_UPD_BIN:-__BIN__}"
SVC="${HOPEDC_UPD_SVC:-hopedc}"
SVC_USER="${HOPEDC_UPD_USER:-__SU__}"
PORT="${HOPEDC_UPD_PORT:-__PORT__}"
RUN="${HOPEDC_UPD_RUN:-/run/hopedc-update}"
SYSTEMCTL="${HOPEDC_UPD_SYSTEMCTL:-systemctl}"
WAIT_API="${HOPEDC_UPD_WAIT_API:-180}"
REQ="$DATA/apply.request"
PKG="$DATA/staged/hopedc-core"

mkdir -p "$RUN" && chmod 0755 "$RUN"
[ -e "$REQ" ] || [ -L "$REQ" ] || exit 0

WORK="$(mktemp -d /var/tmp/hopedc-upd.XXXXXX)" || exit 1
chmod 0755 "$WORK"
trap 'rm -rf "$WORK"' EXIT
VER=""

# st <phase> [code] [detail] — статус для панели (JSON, только безопасные символы).
st() {
  local d
  d="$(printf '%s' "${3:-}" | tr -d '"\\' | tr '\r\n\t' '   ' | tr -cd '[:print:]' | cut -c1-240)"
  printf '{"phase":"%s","version":"%s","code":"%s","detail":"%s","ts":%s}\n' \
    "$1" "$VER" "${2:-}" "$d" "$(date +%s)" > "$RUN/status.json.tmp"
  mv -f "$RUN/status.json.tmp" "$RUN/status.json"
  logger -t hopedc-update "$1 ${2:-} $d" 2>/dev/null || true
}

# Astra Linux (МКЦ): новый файл получает низкую метку целостности — переносим метку
# прежнего бинарника (обычно 0:63:0:0), иначе МКЦ перестанет защищать HopeDC.
copy_label() {
  local src="$1" dst="$2"
  command -v pdpl-file >/dev/null 2>&1 || return 0
  if command -v python3 >/dev/null 2>&1; then
    python3 - "$src" "$dst" <<'PY' && return 0
import os, sys
try:
    v = os.getxattr(sys.argv[1], "security.PDPL", follow_symlinks=False)
except OSError:
    sys.exit(0)
os.setxattr(sys.argv[2], "security.PDPL", v, follow_symlinks=False)
PY
  fi
  pdp-ls -Md "$src" 2>/dev/null | grep -qiE 'Высокий|high|:63:' && pdpl-file 0:63:0:0 "$dst" >/dev/null 2>&1
  return 0
}

# Забираем запрос и сразу удаляем, чтобы path-юнит не сработал повторно.
cp -P "$REQ" "$WORK/req" 2>/dev/null
rm -f "$REQ"
if [ -L "$WORK/req" ] || [ ! -f "$WORK/req" ]; then st failed bad_request; exit 1; fi
WANT="$(grep -m1 -E '^sha256=[0-9a-f]{64}$' "$WORK/req" | cut -d= -f2)"
VER="$(grep -m1 -E '^version=[0-9A-Za-z.+-]{1,40}$' "$WORK/req" | cut -d= -f2)"
WHO="$(grep -m1 -E '^user=[0-9A-Za-z.@_-]{1,64}$' "$WORK/req" | cut -d= -f2)"
[ -n "$WANT" ] || { st failed bad_request; exit 1; }

st verifying
cp -P "$PKG" "$WORK/new" 2>/dev/null
if [ -L "$WORK/new" ] || [ ! -f "$WORK/new" ]; then st failed no_staged; exit 1; fi
[ "$(sha256sum "$WORK/new" | cut -d' ' -f1)" = "$WANT" ] || { st failed checksum; exit 1; }
[ "$(head -c4 "$WORK/new" | od -An -tx1 | tr -d ' \n')" = "7f454c46" ] || { st failed not_elf; exit 1; }
chmod 0755 "$WORK/new"
# Только сборки v147+ (с меткой): старая на --version запустилась бы целиком как контроллер.
grep -qaF HOPEDC_SUPPORTS_VERSION_PROBE_1 "$WORK/new" || { st failed too_old; exit 1; }

# Пробный запуск от имени службы: бинарник стартует и называет себя и свою версию.
if [ "$SVC_USER" = "root" ] || [ "$(id -u)" != "0" ]; then
  OUT="$(cd /var/tmp && timeout 15 env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin "$WORK/new" --version 2>&1 </dev/null)"; RC=$?
else
  OUT="$(cd /var/tmp && timeout 15 runuser -u "$SVC_USER" -- env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin "$WORK/new" --version 2>&1 </dev/null)"; RC=$?
fi
if [ $RC -ne 0 ]; then st failed not_runnable "$OUT"; exit 1; fi
GOTVER="$(printf '%s\n' "$OUT" | sed -n 's/^hopedc-core v\{0,1\}\([0-9][0-9A-Za-z.+-]*\).*/\1/p' | head -1)"
[ -n "$GOTVER" ] || { st failed not_hopedc "$OUT"; exit 1; }
VER="$GOTVER"
logger -t hopedc-update "установка v$VER (запросил ${WHO:-?})" 2>/dev/null || true

st applying
TS="$(date +%s)"
if [ -f "$BIN" ]; then
  cp -a "$BIN" "$BIN.bak.$TS" || { st failed install; exit 1; }
  cp -a "$BIN" "$BIN.prev"   || { st failed install; exit 1; }
  copy_label "$BIN" "$BIN.bak.$TS"; copy_label "$BIN" "$BIN.prev"
fi
if ! install -m 0755 "$WORK/new" "$BIN.new"; then st failed install; exit 1; fi
[ -f "$BIN" ] && copy_label "$BIN" "$BIN.new"
if ! mv -f "$BIN.new" "$BIN"; then st failed install; exit 1; fi
# Храним три последних бэкапа.
ls -1t "$BIN".bak.* 2>/dev/null | tail -n +4 | xargs -r rm -f

st restarting
$SYSTEMCTL restart "$SVC" </dev/null >/dev/null 2>&1

# Здорова: служба active, API отвечает на 127.0.0.1:$PORT и за 10 с нет перезапусков.
api_up() {
  if command -v curl >/dev/null 2>&1; then
    local c; c="$(curl -sk -o /dev/null -m 5 -w '%{http_code}' "https://127.0.0.1:$PORT/api/auth/bootstrap" 2>/dev/null)"
    [ -n "$c" ] && [ "$c" != "000" ] && return 0
    c="$(curl -s -o /dev/null -m 5 -w '%{http_code}' "http://127.0.0.1:$PORT/api/auth/bootstrap" 2>/dev/null)"
    [ -n "$c" ] && [ "$c" != "000" ]
  else
    ss -ltn 2>/dev/null | grep -q ":$PORT "
  fi
}
healthy() {
  local i r0 r1
  for i in $(seq 1 60); do
    sleep 1
    [ "$($SYSTEMCTL is-active "$SVC" 2>/dev/null)" = "active" ] && break
  done
  [ "$($SYSTEMCTL is-active "$SVC" 2>/dev/null)" = "active" ] || return 1
  for i in $(seq 1 "$WAIT_API"); do
    api_up && break
    [ "$($SYSTEMCTL is-active "$SVC" 2>/dev/null)" = "active" ] || return 1
    sleep 1
  done
  api_up || return 1
  r0="$($SYSTEMCTL show -p NRestarts --value "$SVC" 2>/dev/null)"
  sleep 10
  r1="$($SYSTEMCTL show -p NRestarts --value "$SVC" 2>/dev/null)"
  [ "$($SYSTEMCTL is-active "$SVC" 2>/dev/null)" = "active" ] && [ "$r0" = "$r1" ]
}

if healthy; then
  st done done "$VER"
  exit 0
fi

# Откат на прежнюю версию.
if [ -f "$BIN.prev" ] && install -m 0755 "$BIN.prev" "$BIN.new" && copy_label "$BIN.prev" "$BIN.new" && mv -f "$BIN.new" "$BIN"; then
  st restarting rolling_back
  $SYSTEMCTL restart "$SVC" </dev/null >/dev/null 2>&1
  if healthy; then st rolled_back rolled_back; else st rollback_failed rollback_failed; fi
else
  st rollback_failed rollback_failed
fi
exit 1
APPLY
sed -i -e "s#__UPD__#$UPD#" -e "s#__BIN__#$BIN#" -e "s#__SU__#$SU#" -e "s#__PORT__#$PORT#" /usr/local/bin/hopedc-update-apply
chmod 0755 /usr/local/bin/hopedc-update-apply

cat > /etc/systemd/system/hopedc-update.service <<'UNIT'
[Unit]
Description=Apply HopeDC update prepared in the admin panel
[Service]
Type=oneshot
TimeoutStartSec=420
ExecStart=/usr/local/bin/hopedc-update-apply
UNIT

cat > /etc/systemd/system/hopedc-update.path <<UNIT
[Unit]
Description=Watch for HopeDC update requests (admin panel → Updates)
[Path]
PathExists=$UPD/apply.request
Unit=hopedc-update.service
[Install]
WantedBy=multi-user.target
UNIT

systemctl daemon-reload </dev/null >/dev/null 2>&1
systemctl enable --now hopedc-update.path </dev/null >/dev/null 2>&1 || true
echo "обновления из панели: hopedc-update.path $(systemctl is-active hopedc-update.path 2>/dev/null) (служба $SVC от $SU, бинарник $BIN, запросы $UPD/apply.request, API :$PORT)"
HOPEDC_UPDATER_EOF
bash "$WORK/upd.sh" </dev/null

# ---------------------------------------------------------------- 9. запуск и проверка
sec "9. запуск"
T0=$(date '+%F %T')
sc enable $SVC >/dev/null 2>&1
sc restart $SVC >/dev/null 2>&1 || true
UP=0
for _ in $(seq 1 90); do
  C=$(curl -sk -o /dev/null -m 3 -w '%{http_code}' "https://127.0.0.1:$API_PORT/api/auth/bootstrap" 2>/dev/null || true)
  [ -n "$C" ] && [ "$C" != "000" ] && { UP=1; break; }
  [ "$(systemctl is-active $SVC 2>/dev/null)" = "failed" ] && break
  sleep 2
done
echo "active: $(systemctl is-active $SVC 2>/dev/null)"
[ "$UP" = "1" ] && echo "API отвечает: https://127.0.0.1:$API_PORT" || echo "(!) API не ответил — см. журнал ниже"
echo "== слушающие порты =="
ss -ltnu 2>/dev/null | grep -E ":(53|88|389|445|464|636|3268|${UI_PORT}|${API_PORT}) " | awk '{print "  "$1" "$5}' | sort -u
echo "== журнал запуска (ошибки и предупреждения) =="
journalctl -u $SVC --since "$T0" --no-pager -o cat </dev/null 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g' | grep -E "HopeDC v|ERROR|WARN|⚠|мастер-ключ|МКЦ|Лицензионное" | head -15
if [ "$ASTRA" = "1" ]; then
  PID=$(systemctl show -p MainPID --value $SVC); echo "метка процесса: $(ps -o label= -p "$PID" 2>/dev/null)"; pdp-ls -M "$BIN" 2>/dev/null
fi

echo
if [ "$(systemctl is-active $SVC 2>/dev/null)" = "active" ]; then
  echo "ГОТОВО ($MODE)."
else
  echo "!!! Служба не активна — причина в журнале: journalctl -u $SVC -n 50"
  [ -f "$BIN.prev" ] && echo "    Вернуть прежнюю версию: sudo install -m 0755 $BIN.prev $BIN && sudo systemctl restart $SVC"
fi
echo "  Веб‑панель: https://${IPSELF:-<IP сервера>}:${UI_PORT}/"
if [ "${JOINED:-0}" = "1" ]; then
  echo "  Вход: admin / пароль, показанный один раз в журнале установки на корневом сервере (смените при первом входе)."
else
  [ "$MODE" = "install" ] && echo "  Вход: admin / Admin123! — при первом входе смените пароль и примите Лицензионное соглашение."
fi
echo "  Сохраните копию мастер‑ключа вне сервера: панель → Резервное копирование → «Скачать мастер‑ключ»"
echo "  или: sudo $BIN key export --out /root/dc.hdckey --data-dir $DD"
echo "  Следующие версии: панель → Настройки → Обновления, или снова: sudo bash $0 /tmp/hopedc-core_vNNN.gz"
echo "  Журнал установки: $LOG"
exit 0
